From f9dfb34e43ff334abc02fb3394624520615de9e3 Mon Sep 17 00:00:00 2001 From: stasex Date: Thu, 19 Oct 2023 14:09:35 +0300 Subject: [PATCH 01/21] added new unit test and new model for search product name --- .../Models/ProductSearch/ProductName.cs | 6 ++ .../ShoppingAssistantApi.Tests.csproj | 1 + .../Tests/ProductTests.cs | 61 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 ShoppingAssistantApi.Application/Models/ProductSearch/ProductName.cs create mode 100644 ShoppingAssistantApi.Tests/Tests/ProductTests.cs diff --git a/ShoppingAssistantApi.Application/Models/ProductSearch/ProductName.cs b/ShoppingAssistantApi.Application/Models/ProductSearch/ProductName.cs new file mode 100644 index 0000000..559ab3d --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/ProductSearch/ProductName.cs @@ -0,0 +1,6 @@ +namespace ShoppingAssistantApi.Application.Models.ProductSearch; + +public class ProductName +{ + public string Name { get; set; } +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Tests/ShoppingAssistantApi.Tests.csproj b/ShoppingAssistantApi.Tests/ShoppingAssistantApi.Tests.csproj index c36f3b4..85fc768 100644 --- a/ShoppingAssistantApi.Tests/ShoppingAssistantApi.Tests.csproj +++ b/ShoppingAssistantApi.Tests/ShoppingAssistantApi.Tests.csproj @@ -13,6 +13,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ShoppingAssistantApi.Tests/Tests/ProductTests.cs b/ShoppingAssistantApi.Tests/Tests/ProductTests.cs new file mode 100644 index 0000000..d5b786c --- /dev/null +++ b/ShoppingAssistantApi.Tests/Tests/ProductTests.cs @@ -0,0 +1,61 @@ +using System.Collections.ObjectModel; +using Microsoft.VisualBasic; +using MongoDB.Bson; +using Moq; +using Newtonsoft.Json; +using ShoppingAssistantApi.Application.Models.Dtos; +using ShoppingAssistantApi.Application.IServices; +using ShoppingAssistantApi.Application.Models.OpenAi; +using ShoppingAssistantApi.Application.Models.ProductSearch; +using ShoppingAssistantApi.Domain.Entities; +using ShoppingAssistantApi.Domain.Enums; +using ShoppingAssistantApi.Tests.TestExtentions; +using ShoppingAssistantApi.Infrastructure.Services; + +namespace ShoppingAssistantApi.Tests.Tests; + +public class ProductTests : TestsBase +{ + private Mock _openAiServiceMock; + + private Mock _productServiceMock; + + public ProductTests(TestingFactory factory) : base(factory) + { + _openAiServiceMock = new Mock(); + _productServiceMock = new Mock(); + } + + + [Fact] + public async Task GetProductFromSearch_ReturnsProductList() + { + var message = new Message + { + Id = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), + Text = "what are the best graphics cards you know?", + CreatedById = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), + Role = "user" + }; + var cancellationToken = CancellationToken.None; + + var productServiceMock = new Mock(); + var expectedProductList = new List { "NVIDIA GeForce RTX 3080", "AMD Radeon RX 6900 XT" }; + productServiceMock.Setup(x => x.GetProductFromSearch(message, cancellationToken)) + .ReturnsAsync(expectedProductList); + + var openAiServiceMock = new Mock(); + var expectedOpenAiMessage = new OpenAiMessage + { + Role = OpenAiRole.User, + Content = "[\n { \"Name\": \"NVIDIA GeForce RTX 3080\" },\n { \"Name\": \"AMD Radeon RX 6900 XT\" }\n]" + }; + openAiServiceMock.Setup(x => x.GetChatCompletion(It.IsAny(), cancellationToken)) + .ReturnsAsync(expectedOpenAiMessage); + + var productList = JsonConvert.DeserializeObject>(expectedOpenAiMessage.Content).Select(info => info.Name).ToList(); + + Assert.Equal(expectedProductList, productList); + } + +} \ No newline at end of file From 504e67546665ecef35775d33d7e71bd25862550f Mon Sep 17 00:00:00 2001 From: stasex Date: Thu, 19 Oct 2023 14:13:57 +0300 Subject: [PATCH 02/21] added file for services --- .../IServices/IProductService.cs | 12 +++++++ .../Services/ProductServices.cs | 31 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 ShoppingAssistantApi.Application/IServices/IProductService.cs create mode 100644 ShoppingAssistantApi.Infrastructure/Services/ProductServices.cs diff --git a/ShoppingAssistantApi.Application/IServices/IProductService.cs b/ShoppingAssistantApi.Application/IServices/IProductService.cs new file mode 100644 index 0000000..6be1f0d --- /dev/null +++ b/ShoppingAssistantApi.Application/IServices/IProductService.cs @@ -0,0 +1,12 @@ +using System.Collections.ObjectModel; +using ShoppingAssistantApi.Application.Models.OpenAi; +using ShoppingAssistantApi.Domain.Entities; + +namespace ShoppingAssistantApi.Application.IServices; + +public interface IProductService +{ + Task> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken); + + Task> GetProductFromSearch(Message message, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductServices.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductServices.cs new file mode 100644 index 0000000..a193be9 --- /dev/null +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductServices.cs @@ -0,0 +1,31 @@ +using System.Collections.ObjectModel; +using ShoppingAssistantApi.Application.IRepositories; +using ShoppingAssistantApi.Application.IServices; +using ShoppingAssistantApi.Domain.Entities; + +namespace ShoppingAssistantApi.Infrastructure.Services; + +public class ProductServices +{ + /*private readonly IWishlistsRepository _wishlistsRepository; + + private readonly IOpenAiService _openAiService; + private readonly IProductService _productService; + + public ProductServices(IOpenAiService openAiService, IProductService productService) + { + _openAiService = openAiService; + _productService = productService; + } + + public async Task> StartNewSearchAndReturnWishlist(Message message, + CancellationToken cancellationToken) + { + return null; + } + + public async Task> GetProductFromSearch(Message message, CancellationToken cancellationToken) + { + return null; + }*/ +} \ No newline at end of file From dd9fdc04f0b4804ea46ff1d4ce15ae93439fb006 Mon Sep 17 00:00:00 2001 From: stasex Date: Thu, 19 Oct 2023 14:16:02 +0300 Subject: [PATCH 03/21] changed file name --- .../Services/{ProductServices.cs => ProductService.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename ShoppingAssistantApi.Infrastructure/Services/{ProductServices.cs => ProductService.cs} (97%) diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductServices.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs similarity index 97% rename from ShoppingAssistantApi.Infrastructure/Services/ProductServices.cs rename to ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index a193be9..af4e45a 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductServices.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -5,7 +5,7 @@ using ShoppingAssistantApi.Domain.Entities; namespace ShoppingAssistantApi.Infrastructure.Services; -public class ProductServices +public class ProductService { /*private readonly IWishlistsRepository _wishlistsRepository; From 7b2e8d1645fb44a6753faa8dca0a2eb35f3e304e Mon Sep 17 00:00:00 2001 From: stasex Date: Thu, 19 Oct 2023 15:58:05 +0300 Subject: [PATCH 04/21] added new tests for product search, namely for checking recommendations and creating a wishlist --- .../IServices/IProductService.cs | 2 + .../Tests/ProductTests.cs | 102 +++++++++++++++--- 2 files changed, 89 insertions(+), 15 deletions(-) diff --git a/ShoppingAssistantApi.Application/IServices/IProductService.cs b/ShoppingAssistantApi.Application/IServices/IProductService.cs index 6be1f0d..89ce557 100644 --- a/ShoppingAssistantApi.Application/IServices/IProductService.cs +++ b/ShoppingAssistantApi.Application/IServices/IProductService.cs @@ -9,4 +9,6 @@ public interface IProductService Task> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken); Task> GetProductFromSearch(Message message, CancellationToken cancellationToken); + + Task> GetRecommendationsForProductFromSearch(Message message, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ShoppingAssistantApi.Tests/Tests/ProductTests.cs b/ShoppingAssistantApi.Tests/Tests/ProductTests.cs index d5b786c..458169a 100644 --- a/ShoppingAssistantApi.Tests/Tests/ProductTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/ProductTests.cs @@ -1,16 +1,12 @@ -using System.Collections.ObjectModel; -using Microsoft.VisualBasic; -using MongoDB.Bson; +using MongoDB.Bson; using Moq; -using Newtonsoft.Json; -using ShoppingAssistantApi.Application.Models.Dtos; +using Newtonsoft.Json.Linq; using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.Models.OpenAi; using ShoppingAssistantApi.Application.Models.ProductSearch; using ShoppingAssistantApi.Domain.Entities; using ShoppingAssistantApi.Domain.Enums; using ShoppingAssistantApi.Tests.TestExtentions; -using ShoppingAssistantApi.Infrastructure.Services; namespace ShoppingAssistantApi.Tests.Tests; @@ -26,6 +22,54 @@ public class ProductTests : TestsBase _productServiceMock = new Mock(); } + [Fact] + public async Task StartNewSearchAndReturnWishlist_CreatesWishlistObject() + { + var message = new Message + { + Id = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), + Text = "what are the best graphics cards you know?", + CreatedById = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), + Role = "user" + }; + var cancellationToken = CancellationToken.None; + + var expectedProductList = new List { "NVIDIA GeForce RTX 3080", "AMD Radeon RX 6900 XT" }; + _productServiceMock.Setup(x => x.GetProductFromSearch(message, cancellationToken)) + .ReturnsAsync(expectedProductList); + + Wishlist createdWishList = null; + var expectedOpenAiMessage = new OpenAiMessage + { + Role = OpenAiRole.User, + Content = "{ \"Name\": [{ \"Name\": \"NVIDIA GeForce RTX 3080\" }, { \"Name\": \"AMD Radeon RX 6900 XT\" }] }" + }; + _openAiServiceMock.Setup(x => x.GetChatCompletion(It.IsAny(), cancellationToken)) + .ReturnsAsync(expectedOpenAiMessage); + _productServiceMock + .Setup(x => x.StartNewSearchAndReturnWishlist(It.IsAny(), cancellationToken)) + .ReturnsAsync(() => + { + createdWishList = new Wishlist + { + Name = "Test Wishlist", + CreatedById = ObjectId.GenerateNewId(), + Id = ObjectId.GenerateNewId(), + Type = "Test Type" + }; + return new List(); + }); + + await _productServiceMock.Object.StartNewSearchAndReturnWishlist(message, cancellationToken); + + var openAiContent = JObject.Parse(expectedOpenAiMessage.Content); + var productNames = openAiContent["Name"].ToObject>(); + var productList = productNames.Select(info => info.Name).ToList(); + + Assert.Equal(expectedProductList, productList); + Assert.True(openAiContent.ContainsKey("Name")); + Assert.NotNull(createdWishList); + } [Fact] public async Task GetProductFromSearch_ReturnsProductList() @@ -38,24 +82,52 @@ public class ProductTests : TestsBase Role = "user" }; var cancellationToken = CancellationToken.None; - - var productServiceMock = new Mock(); + var expectedProductList = new List { "NVIDIA GeForce RTX 3080", "AMD Radeon RX 6900 XT" }; - productServiceMock.Setup(x => x.GetProductFromSearch(message, cancellationToken)) + _productServiceMock.Setup(x => x.GetProductFromSearch(message, cancellationToken)) .ReturnsAsync(expectedProductList); - - var openAiServiceMock = new Mock(); + var expectedOpenAiMessage = new OpenAiMessage { Role = OpenAiRole.User, - Content = "[\n { \"Name\": \"NVIDIA GeForce RTX 3080\" },\n { \"Name\": \"AMD Radeon RX 6900 XT\" }\n]" + Content = "{ \"Name\": [{ \"Name\": \"NVIDIA GeForce RTX 3080\" }, { \"Name\": \"AMD Radeon RX 6900 XT\" }] }" }; - openAiServiceMock.Setup(x => x.GetChatCompletion(It.IsAny(), cancellationToken)) + _openAiServiceMock.Setup(x => x.GetChatCompletion(It.IsAny(), cancellationToken)) .ReturnsAsync(expectedOpenAiMessage); - var productList = JsonConvert.DeserializeObject>(expectedOpenAiMessage.Content).Select(info => info.Name).ToList(); - + var openAiContent = JObject.Parse(expectedOpenAiMessage.Content); + var productNames = openAiContent["Name"].ToObject>(); + var productList = productNames.Select(info => info.Name).ToList(); + Assert.Equal(expectedProductList, productList); + Assert.True(openAiContent.ContainsKey("Name")); } + [Fact] + public async Task GetRecommendationsForProductFromSearch_ReturnsRecommendations() + { + var message = new Message + { + Id = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), + Text = "get recommendations for this product", + CreatedById = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), + Role = "user" + }; + var cancellationToken = CancellationToken.None; + + var expectedOpenAiMessage = new OpenAiMessage + { + Role = OpenAiRole.User, + Content = "{ \"Recommendation\": [\"Recommendation 1\", \"Recommendation 2\"] }" + }; + _openAiServiceMock.Setup(x => x.GetChatCompletion(It.IsAny(), cancellationToken)) + .ReturnsAsync(expectedOpenAiMessage); + + var recommendations = await _productServiceMock.Object.GetRecommendationsForProductFromSearch(message, cancellationToken); + + var openAiContent = JObject.Parse(expectedOpenAiMessage.Content); + Assert.NotNull(openAiContent); + Assert.True(openAiContent.ContainsKey("Recommendation")); + + } } \ No newline at end of file From 1f865a318fe116d830230f0bf8623c35f7c738a1 Mon Sep 17 00:00:00 2001 From: stasex Date: Fri, 20 Oct 2023 02:28:22 +0300 Subject: [PATCH 05/21] implemented the interface in ProductService.cs --- .../ShoppingAssistantApi.Application.csproj | 1 + .../Services/ProductService.cs | 134 ++++++++++++++++-- 2 files changed, 121 insertions(+), 14 deletions(-) diff --git a/ShoppingAssistantApi.Application/ShoppingAssistantApi.Application.csproj b/ShoppingAssistantApi.Application/ShoppingAssistantApi.Application.csproj index 46083d4..e7b7e9f 100644 --- a/ShoppingAssistantApi.Application/ShoppingAssistantApi.Application.csproj +++ b/ShoppingAssistantApi.Application/ShoppingAssistantApi.Application.csproj @@ -16,4 +16,5 @@ + diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index af4e45a..7753a8d 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -1,31 +1,137 @@ using System.Collections.ObjectModel; +using System.Linq.Expressions; +using Newtonsoft.Json.Linq; using ShoppingAssistantApi.Application.IRepositories; using ShoppingAssistantApi.Application.IServices; +using ShoppingAssistantApi.Application.Models.CreateDtos; +using ShoppingAssistantApi.Application.Models.OpenAi; +using ShoppingAssistantApi.Application.Models.ProductSearch; using ShoppingAssistantApi.Domain.Entities; +using ShoppingAssistantApi.Domain.Enums; namespace ShoppingAssistantApi.Infrastructure.Services; -public class ProductService +public class ProductService : IProductService { - /*private readonly IWishlistsRepository _wishlistsRepository; + private readonly IWishlistsRepository _wishlistsRepository; + + private readonly IWishlistsService _wishlistsService; private readonly IOpenAiService _openAiService; - private readonly IProductService _productService; - public ProductServices(IOpenAiService openAiService, IProductService productService) + public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService, IWishlistsRepository wishlistsRepository) { _openAiService = openAiService; - _productService = productService; - } - - public async Task> StartNewSearchAndReturnWishlist(Message message, - CancellationToken cancellationToken) - { - return null; + _wishlistsService = wishlistsService; + _wishlistsRepository = wishlistsRepository; } - public async Task> GetProductFromSearch(Message message, CancellationToken cancellationToken) + public async Task> StartNewSearchAndReturnWishlist(Message message, + CancellationToken cancellationToken) { - return null; - }*/ + List messages = new List() + { + new OpenAiMessage() + { + Role = OpenAiRole.User, + Content = PromptForProductSearch(message.Text) + } + }; + + var chatRequest = new ChatCompletionRequest + { + Messages = messages + }; + + var openAiMessage = await _openAiService.GetChatCompletion(chatRequest, cancellationToken); + + var openAiContent = JObject.Parse(openAiMessage.Content); + var productNames = openAiContent["Name"]?.ToObject>() ?? new List(); + + + WishlistCreateDto newWishlist = new WishlistCreateDto() + { + Type = "Product", + FirstMessageText = message.Text + }; + + var resultWishList = _wishlistsService.StartPersonalWishlistAsync(newWishlist, cancellationToken); + + return productNames.Select(productName => productName.Name).ToList(); + } + + public async Task> GetProductFromSearch(Message message, CancellationToken cancellationToken) + { + List messages = new List() + { + new OpenAiMessage() + { + Role = OpenAiRole.User, + Content = PromptForProductSearch(message.Text) + } + }; + + var chatRequest = new ChatCompletionRequest + { + Messages = messages + }; + + var openAiMessage = await _openAiService.GetChatCompletion(chatRequest, cancellationToken); + + var openAiContent = JObject.Parse(openAiMessage.Content); + var productNames = openAiContent["Name"]?.ToObject>() ?? new List(); + + return productNames.Select(productName => productName.Name).ToList(); + } + + public async Task> GetRecommendationsForProductFromSearch(Message message, CancellationToken cancellationToken) + { + List messages = new List() + { + new OpenAiMessage() + { + Role = OpenAiRole.User, + Content = PromptForRecommendationsForProductSearch(message.Text) + } + }; + + var chatRequest = new ChatCompletionRequest + { + Messages = messages + }; + + var openAiMessage = await _openAiService.GetChatCompletion(chatRequest, cancellationToken); + + var openAiContent = JObject.Parse(openAiMessage.Content); + var recommendations = openAiContent["Recommendation"]?.ToObject>() ?? new List(); + + return recommendations; + } + + public string PromptForProductSearch(string message) + { + string promptForSearch = "Return information in JSON. " + + "\nProvide information, only that indicated in the type of answer, namely only the name. " + + "\nAsk additional questions to the user if there is not enough information. " + + "\nIf there are several answer options, list them. " + + "\nYou don't need to display questions and products together! " + + "\nDo not output any text other than JSON!!! " + + $"\n\nQuestion: {message} " + + $"\nType of answer: Question:[] " + + $"\n\nif there are no questions, then just display the products " + + $"\nType of answer: Name:"; + return promptForSearch; + } + + public string PromptForRecommendationsForProductSearch(string message) + { + string promptForSearch = "Return information in JSON. " + + "\nProvide only information indicated in the type of answer, namely only the recommendation. " + + "\nIf there are several answer options, list them. " + + "\nDo not output any text other than JSON." + + $"\n\nGive recommendations for this question: {message} " + + "\nType of answer: " + + "\n\nRecommendation :"; + return promptForSearch; + } } \ No newline at end of file From 6ce15d15ae527dc94f856a482a3274d40567dd0b Mon Sep 17 00:00:00 2001 From: stasex Date: Fri, 20 Oct 2023 02:29:10 +0300 Subject: [PATCH 06/21] commit with the required nuget package --- .../ShoppingAssistantApi.Infrastructure.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/ShoppingAssistantApi.Infrastructure/ShoppingAssistantApi.Infrastructure.csproj b/ShoppingAssistantApi.Infrastructure/ShoppingAssistantApi.Infrastructure.csproj index 6b6f722..ab6f623 100644 --- a/ShoppingAssistantApi.Infrastructure/ShoppingAssistantApi.Infrastructure.csproj +++ b/ShoppingAssistantApi.Infrastructure/ShoppingAssistantApi.Infrastructure.csproj @@ -11,6 +11,7 @@ + From fc6ce2e6a9037c13e58bcd39d88879e4ad6c18e3 Mon Sep 17 00:00:00 2001 From: stasex Date: Fri, 20 Oct 2023 23:30:09 +0300 Subject: [PATCH 07/21] made an implementation to start the search and tested it --- .../Mutations/ProductMutation.cs | 15 ++++ .../Queries/ProductQuery.cs | 7 ++ .../IServices/IProductService.cs | 4 +- .../Services/ProductService.cs | 39 +++++----- .../ProductTests.cs | 74 +++++++++++-------- .../ShoppingAssistantApi.UnitTests.csproj | 7 +- 6 files changed, 92 insertions(+), 54 deletions(-) create mode 100644 ShoppingAssistantApi.Api/Mutations/ProductMutation.cs create mode 100644 ShoppingAssistantApi.Api/Queries/ProductQuery.cs rename {ShoppingAssistantApi.Tests/Tests => ShoppingAssistantApi.UnitTests}/ProductTests.cs (70%) diff --git a/ShoppingAssistantApi.Api/Mutations/ProductMutation.cs b/ShoppingAssistantApi.Api/Mutations/ProductMutation.cs new file mode 100644 index 0000000..24ad5e3 --- /dev/null +++ b/ShoppingAssistantApi.Api/Mutations/ProductMutation.cs @@ -0,0 +1,15 @@ +using ShoppingAssistantApi.Application.IServices; +using ShoppingAssistantApi.Application.Models.Dtos; +using ShoppingAssistantApi.Application.Models.ProductSearch; +using ShoppingAssistantApi.Domain.Entities; +using ShoppingAssistantApi.Infrastructure.Services; + +namespace ShoppingAssistantApi.Api.Mutations; + +[ExtendObjectType(OperationTypeNames.Mutation)] +public class ProductMutation +{ + public IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist( + Message message, CancellationToken cancellationToken, [Service] IProductService productService) + => productService.StartNewSearchAndReturnWishlist(message, cancellationToken); +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Api/Queries/ProductQuery.cs b/ShoppingAssistantApi.Api/Queries/ProductQuery.cs new file mode 100644 index 0000000..11f9fc1 --- /dev/null +++ b/ShoppingAssistantApi.Api/Queries/ProductQuery.cs @@ -0,0 +1,7 @@ +namespace ShoppingAssistantApi.Api.Queries; + +[ExtendObjectType(OperationTypeNames.Query)] +public class ProductQuery +{ + +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Application/IServices/IProductService.cs b/ShoppingAssistantApi.Application/IServices/IProductService.cs index 89ce557..7bd64d6 100644 --- a/ShoppingAssistantApi.Application/IServices/IProductService.cs +++ b/ShoppingAssistantApi.Application/IServices/IProductService.cs @@ -1,12 +1,14 @@ using System.Collections.ObjectModel; +using ShoppingAssistantApi.Application.Models.Dtos; using ShoppingAssistantApi.Application.Models.OpenAi; +using ShoppingAssistantApi.Application.Models.ProductSearch; using ShoppingAssistantApi.Domain.Entities; namespace ShoppingAssistantApi.Application.IServices; public interface IProductService { - Task> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken); + IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken); Task> GetProductFromSearch(Message message, CancellationToken cancellationToken); diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index 7753a8d..3f230ff 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -1,9 +1,11 @@ using System.Collections.ObjectModel; using System.Linq.Expressions; +using System.Runtime.CompilerServices; using Newtonsoft.Json.Linq; using ShoppingAssistantApi.Application.IRepositories; using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.Models.CreateDtos; +using ShoppingAssistantApi.Application.Models.Dtos; using ShoppingAssistantApi.Application.Models.OpenAi; using ShoppingAssistantApi.Application.Models.ProductSearch; using ShoppingAssistantApi.Domain.Entities; @@ -13,21 +15,17 @@ namespace ShoppingAssistantApi.Infrastructure.Services; public class ProductService : IProductService { - private readonly IWishlistsRepository _wishlistsRepository; - private readonly IWishlistsService _wishlistsService; private readonly IOpenAiService _openAiService; - public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService, IWishlistsRepository wishlistsRepository) + public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService) { _openAiService = openAiService; _wishlistsService = wishlistsService; - _wishlistsRepository = wishlistsRepository; } - public async Task> StartNewSearchAndReturnWishlist(Message message, - CancellationToken cancellationToken) + public async IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken) { List messages = new List() { @@ -37,27 +35,28 @@ public class ProductService : IProductService Content = PromptForProductSearch(message.Text) } }; - + var chatRequest = new ChatCompletionRequest { Messages = messages }; - var openAiMessage = await _openAiService.GetChatCompletion(chatRequest, cancellationToken); - - var openAiContent = JObject.Parse(openAiMessage.Content); - var productNames = openAiContent["Name"]?.ToObject>() ?? new List(); - - - WishlistCreateDto newWishlist = new WishlistCreateDto() + await foreach (var response in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) { - Type = "Product", - FirstMessageText = message.Text - }; + var openAiContent = JObject.Parse(response); + var productNames = openAiContent["Name"]?.ToObject>() ?? new List(); - var resultWishList = _wishlistsService.StartPersonalWishlistAsync(newWishlist, cancellationToken); - - return productNames.Select(productName => productName.Name).ToList(); + WishlistCreateDto newWishlist = new WishlistCreateDto() + { + Type = "Product", + FirstMessageText = message.Text + }; + + var resultWishlistTask = _wishlistsService.StartPersonalWishlistAsync(newWishlist, cancellationToken); + var resultWishlist = await resultWishlistTask; + + yield return (productNames, resultWishlist); + } } public async Task> GetProductFromSearch(Message message, CancellationToken cancellationToken) diff --git a/ShoppingAssistantApi.Tests/Tests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs similarity index 70% rename from ShoppingAssistantApi.Tests/Tests/ProductTests.cs rename to ShoppingAssistantApi.UnitTests/ProductTests.cs index 458169a..57fa61b 100644 --- a/ShoppingAssistantApi.Tests/Tests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -1,25 +1,31 @@ -using MongoDB.Bson; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson; using Moq; using Newtonsoft.Json.Linq; using ShoppingAssistantApi.Application.IServices; +using ShoppingAssistantApi.Application.Models.CreateDtos; +using ShoppingAssistantApi.Application.Models.Dtos; using ShoppingAssistantApi.Application.Models.OpenAi; using ShoppingAssistantApi.Application.Models.ProductSearch; using ShoppingAssistantApi.Domain.Entities; using ShoppingAssistantApi.Domain.Enums; -using ShoppingAssistantApi.Tests.TestExtentions; +using ShoppingAssistantApi.Infrastructure.Services; namespace ShoppingAssistantApi.Tests.Tests; -public class ProductTests : TestsBase +public class ProductTests { private Mock _openAiServiceMock; - + private Mock _productServiceMock; - - public ProductTests(TestingFactory factory) : base(factory) + + public Mock _wishListServiceMock; + + public ProductTests() { _openAiServiceMock = new Mock(); _productServiceMock = new Mock(); + _wishListServiceMock = new Mock(); } [Fact] @@ -33,42 +39,50 @@ public class ProductTests : TestsBase Role = "user" }; var cancellationToken = CancellationToken.None; - - var expectedProductList = new List { "NVIDIA GeForce RTX 3080", "AMD Radeon RX 6900 XT" }; - _productServiceMock.Setup(x => x.GetProductFromSearch(message, cancellationToken)) - .ReturnsAsync(expectedProductList); - Wishlist createdWishList = null; var expectedOpenAiMessage = new OpenAiMessage { Role = OpenAiRole.User, Content = "{ \"Name\": [{ \"Name\": \"NVIDIA GeForce RTX 3080\" }, { \"Name\": \"AMD Radeon RX 6900 XT\" }] }" }; - _openAiServiceMock.Setup(x => x.GetChatCompletion(It.IsAny(), cancellationToken)) - .ReturnsAsync(expectedOpenAiMessage); - _productServiceMock - .Setup(x => x.StartNewSearchAndReturnWishlist(It.IsAny(), cancellationToken)) - .ReturnsAsync(() => + + var openAiServiceMock = new Mock(); + var wishlistsServiceMock = new Mock(); + + openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) + .Returns((ChatCompletionRequest request, CancellationToken token) => { - createdWishList = new Wishlist - { - Name = "Test Wishlist", - CreatedById = ObjectId.GenerateNewId(), - Id = ObjectId.GenerateNewId(), - Type = "Test Type" - }; - return new List(); + var asyncEnumerable = new List { expectedOpenAiMessage.Content }.ToAsyncEnumerable(); + return asyncEnumerable; }); - await _productServiceMock.Object.StartNewSearchAndReturnWishlist(message, cancellationToken); - - var openAiContent = JObject.Parse(expectedOpenAiMessage.Content); - var productNames = openAiContent["Name"].ToObject>(); - var productList = productNames.Select(info => info.Name).ToList(); + wishlistsServiceMock.Setup(x => x.StartPersonalWishlistAsync(It.IsAny(), cancellationToken)) + .ReturnsAsync(new WishlistDto + { + Id = "someID", + Name = "MacBook", + Type = "Product", + CreatedById = "someId" + }); + + var productService = new ProductService(openAiServiceMock.Object, wishlistsServiceMock.Object); + + List productNames = null; + WishlistDto createdWishList = null; + + var result = productService.StartNewSearchAndReturnWishlist(message, cancellationToken); + + await foreach (var (productList, wishlist) in result) + { + productNames = productList; + createdWishList = wishlist; + } + + var openAiContent = JObject.Parse(expectedOpenAiMessage.Content); - Assert.Equal(expectedProductList, productList); Assert.True(openAiContent.ContainsKey("Name")); Assert.NotNull(createdWishList); + Assert.NotNull(productNames); } [Fact] diff --git a/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj b/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj index 9274a65..e5361a9 100644 --- a/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj +++ b/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj @@ -12,6 +12,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -23,9 +24,9 @@ - - - + + + From 60484e4a6e1bae0301bd63c1e34045097ae50a7a Mon Sep 17 00:00:00 2001 From: stasex Date: Fri, 20 Oct 2023 23:46:55 +0300 Subject: [PATCH 08/21] made an implementation for request for recommendations and tested it --- .../IServices/IProductService.cs | 5 ++-- .../Services/ProductService.cs | 18 ++++++----- .../ProductTests.cs | 30 ++++++++++++------- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/ShoppingAssistantApi.Application/IServices/IProductService.cs b/ShoppingAssistantApi.Application/IServices/IProductService.cs index 7bd64d6..c2fddd2 100644 --- a/ShoppingAssistantApi.Application/IServices/IProductService.cs +++ b/ShoppingAssistantApi.Application/IServices/IProductService.cs @@ -11,6 +11,7 @@ public interface IProductService IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken); Task> GetProductFromSearch(Message message, CancellationToken cancellationToken); - - Task> GetRecommendationsForProductFromSearch(Message message, CancellationToken cancellationToken); + + IAsyncEnumerable GetRecommendationsForProductFromSearchStream(Message message, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index 3f230ff..5a11d68 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -83,7 +83,7 @@ public class ProductService : IProductService return productNames.Select(productName => productName.Name).ToList(); } - public async Task> GetRecommendationsForProductFromSearch(Message message, CancellationToken cancellationToken) + public async IAsyncEnumerable GetRecommendationsForProductFromSearchStream(Message message, CancellationToken cancellationToken) { List messages = new List() { @@ -93,18 +93,22 @@ public class ProductService : IProductService Content = PromptForRecommendationsForProductSearch(message.Text) } }; - + var chatRequest = new ChatCompletionRequest { Messages = messages }; - var openAiMessage = await _openAiService.GetChatCompletion(chatRequest, cancellationToken); + await foreach (var response in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) + { + var openAiContent = JObject.Parse(response); + var recommendations = openAiContent["Recommendation"]?.ToObject>() ?? new List(); - var openAiContent = JObject.Parse(openAiMessage.Content); - var recommendations = openAiContent["Recommendation"]?.ToObject>() ?? new List(); - - return recommendations; + foreach (var recommendation in recommendations) + { + yield return recommendation; + } + } } public string PromptForProductSearch(string message) diff --git a/ShoppingAssistantApi.UnitTests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs index 57fa61b..86fe6da 100644 --- a/ShoppingAssistantApi.UnitTests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -46,17 +46,14 @@ public class ProductTests Content = "{ \"Name\": [{ \"Name\": \"NVIDIA GeForce RTX 3080\" }, { \"Name\": \"AMD Radeon RX 6900 XT\" }] }" }; - var openAiServiceMock = new Mock(); - var wishlistsServiceMock = new Mock(); - - openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) + _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) .Returns((ChatCompletionRequest request, CancellationToken token) => { var asyncEnumerable = new List { expectedOpenAiMessage.Content }.ToAsyncEnumerable(); return asyncEnumerable; }); - wishlistsServiceMock.Setup(x => x.StartPersonalWishlistAsync(It.IsAny(), cancellationToken)) + _wishListServiceMock.Setup(x => x.StartPersonalWishlistAsync(It.IsAny(), cancellationToken)) .ReturnsAsync(new WishlistDto { Id = "someID", @@ -65,7 +62,7 @@ public class ProductTests CreatedById = "someId" }); - var productService = new ProductService(openAiServiceMock.Object, wishlistsServiceMock.Object); + var productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); List productNames = null; WishlistDto createdWishList = null; @@ -128,20 +125,31 @@ public class ProductTests Role = "user" }; var cancellationToken = CancellationToken.None; - + var expectedOpenAiMessage = new OpenAiMessage { Role = OpenAiRole.User, Content = "{ \"Recommendation\": [\"Recommendation 1\", \"Recommendation 2\"] }" }; - _openAiServiceMock.Setup(x => x.GetChatCompletion(It.IsAny(), cancellationToken)) - .ReturnsAsync(expectedOpenAiMessage); - var recommendations = await _productServiceMock.Object.GetRecommendationsForProductFromSearch(message, cancellationToken); + _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) + .Returns((ChatCompletionRequest request, CancellationToken token) => + { + var asyncEnumerable = new List { expectedOpenAiMessage.Content }.ToAsyncEnumerable(); + return asyncEnumerable; + }); + + var recommendations = new List(); + var productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); + await foreach (var recommendation in productService.GetRecommendationsForProductFromSearchStream(message, cancellationToken)) + { + recommendations.Add(recommendation); + } + var openAiContent = JObject.Parse(expectedOpenAiMessage.Content); Assert.NotNull(openAiContent); Assert.True(openAiContent.ContainsKey("Recommendation")); - + Assert.Equal(new List { "Recommendation 1", "Recommendation 2" }, recommendations); } } \ No newline at end of file From 9db3baca89043a9ef5e080c3db94493293884ac1 Mon Sep 17 00:00:00 2001 From: stasex Date: Sat, 21 Oct 2023 01:15:30 +0300 Subject: [PATCH 09/21] made an implementation for a request that may contain questions and tested it --- .../Queries/ProductQuery.cs | 15 +++- .../IServices/IProductService.cs | 4 +- .../Models/ProductSearch/Question.cs | 6 ++ .../Services/ProductService.cs | 50 +++++++++++--- .../ProductTests.cs | 68 +++++++++++++++---- 5 files changed, 120 insertions(+), 23 deletions(-) create mode 100644 ShoppingAssistantApi.Application/Models/ProductSearch/Question.cs diff --git a/ShoppingAssistantApi.Api/Queries/ProductQuery.cs b/ShoppingAssistantApi.Api/Queries/ProductQuery.cs index 11f9fc1..cbaa775 100644 --- a/ShoppingAssistantApi.Api/Queries/ProductQuery.cs +++ b/ShoppingAssistantApi.Api/Queries/ProductQuery.cs @@ -1,7 +1,20 @@ -namespace ShoppingAssistantApi.Api.Queries; +using HotChocolate.Authorization; +using ShoppingAssistantApi.Application.IServices; +using ShoppingAssistantApi.Domain.Entities; + +namespace ShoppingAssistantApi.Api.Queries; [ExtendObjectType(OperationTypeNames.Query)] public class ProductQuery { + [Authorize] + public IAsyncEnumerable GetProductFromSearch(Message message, CancellationToken cancellationToken, + [Service] IProductService productService) + => productService.GetProductFromSearch(message, cancellationToken); + [Authorize] + public IAsyncEnumerable GetRecommendationsForProductFromSearchStream(Message message, CancellationToken cancellationToken, + [Service] IProductService productService) + => productService.GetRecommendationsForProductFromSearchStream(message, cancellationToken); + } \ No newline at end of file diff --git a/ShoppingAssistantApi.Application/IServices/IProductService.cs b/ShoppingAssistantApi.Application/IServices/IProductService.cs index c2fddd2..8f929f4 100644 --- a/ShoppingAssistantApi.Application/IServices/IProductService.cs +++ b/ShoppingAssistantApi.Application/IServices/IProductService.cs @@ -9,8 +9,8 @@ namespace ShoppingAssistantApi.Application.IServices; public interface IProductService { IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken); - - Task> GetProductFromSearch(Message message, CancellationToken cancellationToken); + + IAsyncEnumerable GetProductFromSearch(Message message, CancellationToken cancellationToken); IAsyncEnumerable GetRecommendationsForProductFromSearchStream(Message message, CancellationToken cancellationToken); diff --git a/ShoppingAssistantApi.Application/Models/ProductSearch/Question.cs b/ShoppingAssistantApi.Application/Models/ProductSearch/Question.cs new file mode 100644 index 0000000..e1f0c46 --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/ProductSearch/Question.cs @@ -0,0 +1,6 @@ +namespace ShoppingAssistantApi.Application.Models.ProductSearch; + +public class Question +{ + public string QuestionText { get; set; } +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index 5a11d68..ecaf0fb 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -59,30 +59,47 @@ public class ProductService : IProductService } } - public async Task> GetProductFromSearch(Message message, CancellationToken cancellationToken) + public async IAsyncEnumerable GetProductFromSearch(Message message, [EnumeratorCancellation] CancellationToken cancellationToken) { List messages = new List() { new OpenAiMessage() { Role = OpenAiRole.User, - Content = PromptForProductSearch(message.Text) + Content = PromptForProductSearchWithQuestion(message.Text) } }; - + var chatRequest = new ChatCompletionRequest { Messages = messages }; - var openAiMessage = await _openAiService.GetChatCompletion(chatRequest, cancellationToken); + await foreach (var response in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) + { + var openAiContent = JObject.Parse(response); + var productNames = openAiContent["Name"]?.ToObject>(); - var openAiContent = JObject.Parse(openAiMessage.Content); - var productNames = openAiContent["Name"]?.ToObject>() ?? new List(); - - return productNames.Select(productName => productName.Name).ToList(); + if (productNames != null && productNames.Any()) + { + foreach (var productName in productNames) + { + yield return productName.Name; + } + } + else + { + var questions = openAiContent["AdditionalQuestion"]?.ToObject>() ?? new List(); + + foreach (var question in questions) + { + yield return question.QuestionText; + } + } + } } + public async IAsyncEnumerable GetRecommendationsForProductFromSearchStream(Message message, CancellationToken cancellationToken) { List messages = new List() @@ -137,4 +154,21 @@ public class ProductService : IProductService "\n\nRecommendation :"; return promptForSearch; } + + public string PromptForProductSearchWithQuestion(string message) + { + string promptForSearch = "Return information in JSON. " + + "\nAsk additional questions to the user if there is not enough information." + + "\nIf there are several answer options, list them. " + + "\nYou don't need to display questions and products together!" + + "\nDo not output any text other than JSON!!!" + + $"\n\nQuestion: {message}" + + "\n\nif you can ask questions to clarify the choice, then ask them" + + "\nType of answer:" + + "\nAdditionalQuestion:[]" + + "\n\nif there are no questions, then just display the products" + + "\nType of answer:" + + "\nName:"; + return promptForSearch; + } } \ No newline at end of file diff --git a/ShoppingAssistantApi.UnitTests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs index 86fe6da..1ddee69 100644 --- a/ShoppingAssistantApi.UnitTests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -76,14 +76,13 @@ public class ProductTests } var openAiContent = JObject.Parse(expectedOpenAiMessage.Content); - - Assert.True(openAiContent.ContainsKey("Name")); + Assert.NotNull(createdWishList); Assert.NotNull(productNames); } [Fact] - public async Task GetProductFromSearch_ReturnsProductList() + public async Task GetProductFromSearch_ReturnsProductListWithName() { var message = new Message { @@ -93,27 +92,72 @@ public class ProductTests Role = "user" }; var cancellationToken = CancellationToken.None; - - var expectedProductList = new List { "NVIDIA GeForce RTX 3080", "AMD Radeon RX 6900 XT" }; - _productServiceMock.Setup(x => x.GetProductFromSearch(message, cancellationToken)) - .ReturnsAsync(expectedProductList); - + var expectedOpenAiMessage = new OpenAiMessage { Role = OpenAiRole.User, Content = "{ \"Name\": [{ \"Name\": \"NVIDIA GeForce RTX 3080\" }, { \"Name\": \"AMD Radeon RX 6900 XT\" }] }" }; - _openAiServiceMock.Setup(x => x.GetChatCompletion(It.IsAny(), cancellationToken)) - .ReturnsAsync(expectedOpenAiMessage); + + _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) + .Returns(new List { expectedOpenAiMessage.Content }.ToAsyncEnumerable()); + + var productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); + + var productList = new List(); + + await foreach (var product in productService.GetProductFromSearch(message, cancellationToken)) + { + productList.Add(product); + } var openAiContent = JObject.Parse(expectedOpenAiMessage.Content); var productNames = openAiContent["Name"].ToObject>(); - var productList = productNames.Select(info => info.Name).ToList(); - + var expectedProductList = productNames.Select(info => info.Name).ToList(); + Assert.Equal(expectedProductList, productList); + Assert.NotNull(openAiContent); Assert.True(openAiContent.ContainsKey("Name")); } + [Fact] + public async Task GetProductFromSearch_ReturnsProductListWithQuestion() + { + var message = new Message + { + Id = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), + Text = "what are the best graphics cards you know?", + CreatedById = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), + Role = "user" + }; + var cancellationToken = CancellationToken.None; + + var expectedOpenAiMessage = new OpenAiMessage + { + Role = OpenAiRole.User, + Content = "{ \"AdditionalQuestion\": [{ \"QuestionText\": \"What specific MacBook model are you using?\" }," + + " { \"QuestionText\": \"Do you have any preferences for brand or capacity?\" }] }" + }; + + _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) + .Returns(new List { expectedOpenAiMessage.Content }.ToAsyncEnumerable()); + + var productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); + + var productList = new List(); + + await foreach (var product in productService.GetProductFromSearch(message, cancellationToken)) + { + productList.Add(product); + } + + var openAiContent = JObject.Parse(expectedOpenAiMessage.Content); + var productNames = openAiContent["AdditionalQuestion"].ToObject>(); + + Assert.NotNull(openAiContent); + Assert.True(openAiContent.ContainsKey("AdditionalQuestion")); + } + [Fact] public async Task GetRecommendationsForProductFromSearch_ReturnsRecommendations() { From fcc5f02c48c150d26534875b977a5758bd23697a Mon Sep 17 00:00:00 2001 From: shchoholiev Date: Sat, 21 Oct 2023 13:40:10 +0000 Subject: [PATCH 10/21] Added models for product search. --- .../IServices/IProductService.cs | 5 +- .../Models/ProductSearch/MessagePart.cs | 6 ++ .../Models/ProductSearch/ServerSentEvent.cs | 10 ++++ .../Models/ProductSearch/Suggestion.cs | 6 ++ .../Enums/SearchEventType.cs | 24 ++++++++ .../Services/ProductService.cs | 7 +++ .../ProductTests.cs | 58 +++++++++---------- 7 files changed, 84 insertions(+), 32 deletions(-) create mode 100644 ShoppingAssistantApi.Application/Models/ProductSearch/MessagePart.cs create mode 100644 ShoppingAssistantApi.Application/Models/ProductSearch/ServerSentEvent.cs create mode 100644 ShoppingAssistantApi.Application/Models/ProductSearch/Suggestion.cs create mode 100644 ShoppingAssistantApi.Domain/Enums/SearchEventType.cs diff --git a/ShoppingAssistantApi.Application/IServices/IProductService.cs b/ShoppingAssistantApi.Application/IServices/IProductService.cs index 8f929f4..8bd24b6 100644 --- a/ShoppingAssistantApi.Application/IServices/IProductService.cs +++ b/ShoppingAssistantApi.Application/IServices/IProductService.cs @@ -1,6 +1,5 @@ -using System.Collections.ObjectModel; +using ShoppingAssistantApi.Application.Models.CreateDtos; using ShoppingAssistantApi.Application.Models.Dtos; -using ShoppingAssistantApi.Application.Models.OpenAi; using ShoppingAssistantApi.Application.Models.ProductSearch; using ShoppingAssistantApi.Domain.Entities; @@ -10,6 +9,8 @@ public interface IProductService { IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken); + IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken); + IAsyncEnumerable GetProductFromSearch(Message message, CancellationToken cancellationToken); IAsyncEnumerable GetRecommendationsForProductFromSearchStream(Message message, diff --git a/ShoppingAssistantApi.Application/Models/ProductSearch/MessagePart.cs b/ShoppingAssistantApi.Application/Models/ProductSearch/MessagePart.cs new file mode 100644 index 0000000..feacc20 --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/ProductSearch/MessagePart.cs @@ -0,0 +1,6 @@ +namespace ShoppingAssistantApi.Application.Models.ProductSearch; + +public class MessagePart +{ + public string Text { get; set; } +} diff --git a/ShoppingAssistantApi.Application/Models/ProductSearch/ServerSentEvent.cs b/ShoppingAssistantApi.Application/Models/ProductSearch/ServerSentEvent.cs new file mode 100644 index 0000000..e7be1cb --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/ProductSearch/ServerSentEvent.cs @@ -0,0 +1,10 @@ +using ShoppingAssistantApi.Domain.Enums; + +namespace ShoppingAssistantApi.Application.Models.ProductSearch; + +public class ServerSentEvent +{ + public SearchEventType Event { get; set; } + + public string Data { get; set; } +} diff --git a/ShoppingAssistantApi.Application/Models/ProductSearch/Suggestion.cs b/ShoppingAssistantApi.Application/Models/ProductSearch/Suggestion.cs new file mode 100644 index 0000000..ccefea2 --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/ProductSearch/Suggestion.cs @@ -0,0 +1,6 @@ +namespace ShoppingAssistantApi.Application.Models.ProductSearch; + +public class Suggestion +{ + public string Text { get; set; } +} diff --git a/ShoppingAssistantApi.Domain/Enums/SearchEventType.cs b/ShoppingAssistantApi.Domain/Enums/SearchEventType.cs new file mode 100644 index 0000000..3388692 --- /dev/null +++ b/ShoppingAssistantApi.Domain/Enums/SearchEventType.cs @@ -0,0 +1,24 @@ +namespace ShoppingAssistantApi.Domain.Enums; + +public enum SearchEventType +{ + Wishlist = 0, + Message = 1, + Suggestion = 2, + Product = 3 +} + +public static class SearchEventTypeExtensions +{ + public static string ToSseEventString(this SearchEventType eventType) + { + return eventType switch + { + SearchEventType.Wishlist => "wishlist", + SearchEventType.Message => "message", + SearchEventType.Suggestion => "suggestion", + SearchEventType.Product => "product", + _ => throw new ArgumentOutOfRangeException(nameof(eventType), eventType, null), + }; + } +} diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index ecaf0fb..c108f78 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -25,6 +25,13 @@ public class ProductService : IProductService _wishlistsService = wishlistsService; } + public IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken) + { + // get all messages from wishlist + + throw new NotImplementedException(); + } + public async IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken) { List messages = new List() diff --git a/ShoppingAssistantApi.UnitTests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs index 1ddee69..78b1376 100644 --- a/ShoppingAssistantApi.UnitTests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -17,20 +17,43 @@ public class ProductTests { private Mock _openAiServiceMock; - private Mock _productServiceMock; + private IProductService _productService; public Mock _wishListServiceMock; public ProductTests() { _openAiServiceMock = new Mock(); - _productServiceMock = new Mock(); _wishListServiceMock = new Mock(); + _productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); } [Fact] public async Task StartNewSearchAndReturnWishlist_CreatesWishlistObject() { + // Arrange + var expectedOpenAiMessage = new OpenAiMessage + { + Role = OpenAiRole.User, + Content = "{ \"Name\": [{ \"Name\": \"NVIDIA GeForce RTX 3080\" }, { \"Name\": \"AMD Radeon RX 6900 XT\" }] }" + }; + + _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), CancellationToken.None)) + .Returns((ChatCompletionRequest request, CancellationToken token) => + { + var asyncEnumerable = new List { expectedOpenAiMessage.Content }.ToAsyncEnumerable(); + return asyncEnumerable; + }); + + _wishListServiceMock.Setup(x => x.StartPersonalWishlistAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(new WishlistDto + { + Id = "someID", + Name = "MacBook", + Type = "Product", // Use enum + CreatedById = "someId" + }); + var message = new Message { Id = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), @@ -38,45 +61,20 @@ public class ProductTests CreatedById = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), Role = "user" }; - var cancellationToken = CancellationToken.None; - - var expectedOpenAiMessage = new OpenAiMessage - { - Role = OpenAiRole.User, - Content = "{ \"Name\": [{ \"Name\": \"NVIDIA GeForce RTX 3080\" }, { \"Name\": \"AMD Radeon RX 6900 XT\" }] }" - }; - - _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) - .Returns((ChatCompletionRequest request, CancellationToken token) => - { - var asyncEnumerable = new List { expectedOpenAiMessage.Content }.ToAsyncEnumerable(); - return asyncEnumerable; - }); - - _wishListServiceMock.Setup(x => x.StartPersonalWishlistAsync(It.IsAny(), cancellationToken)) - .ReturnsAsync(new WishlistDto - { - Id = "someID", - Name = "MacBook", - Type = "Product", - CreatedById = "someId" - }); - - var productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); List productNames = null; WishlistDto createdWishList = null; - var result = productService.StartNewSearchAndReturnWishlist(message, cancellationToken); + // Act + var result = _productService.StartNewSearchAndReturnWishlist(message, CancellationToken.None); await foreach (var (productList, wishlist) in result) { productNames = productList; createdWishList = wishlist; } - - var openAiContent = JObject.Parse(expectedOpenAiMessage.Content); + // Assert Assert.NotNull(createdWishList); Assert.NotNull(productNames); } From 962ab03c4c4f3e5e0a36be47287fbc4abed19c02 Mon Sep 17 00:00:00 2001 From: Mykhailo Bilodid Date: Sat, 21 Oct 2023 19:19:13 +0300 Subject: [PATCH 11/21] SA-33 comments and some changes added --- .../IServices/IProductService.cs | 5 ++-- .../Entities/Wishlist.cs | 2 -- .../Services/ProductService.cs | 5 ++-- .../PersistanceExtentions/DbInitialaizer.cs | 30 ------------------- 4 files changed, 6 insertions(+), 36 deletions(-) diff --git a/ShoppingAssistantApi.Application/IServices/IProductService.cs b/ShoppingAssistantApi.Application/IServices/IProductService.cs index 8bd24b6..03a8545 100644 --- a/ShoppingAssistantApi.Application/IServices/IProductService.cs +++ b/ShoppingAssistantApi.Application/IServices/IProductService.cs @@ -7,10 +7,11 @@ namespace ShoppingAssistantApi.Application.IServices; public interface IProductService { - IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken); - IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken); + // TODO remove all methods below + IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken); + IAsyncEnumerable GetProductFromSearch(Message message, CancellationToken cancellationToken); IAsyncEnumerable GetRecommendationsForProductFromSearchStream(Message message, diff --git a/ShoppingAssistantApi.Domain/Entities/Wishlist.cs b/ShoppingAssistantApi.Domain/Entities/Wishlist.cs index 4dee7fc..9c9bfc6 100644 --- a/ShoppingAssistantApi.Domain/Entities/Wishlist.cs +++ b/ShoppingAssistantApi.Domain/Entities/Wishlist.cs @@ -7,6 +7,4 @@ public class Wishlist : EntityBase public string Name { get; set; } public string Type { get; set; } - - public ICollection? Messages { get; set; } } diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index c108f78..7bc99ed 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -24,14 +24,15 @@ public class ProductService : IProductService _openAiService = openAiService; _wishlistsService = wishlistsService; } - + public IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken) { - // get all messages from wishlist + // Documentation: https://shchoholiev.atlassian.net/l/cp/JizkynhU throw new NotImplementedException(); } + // TODO: remove all methods below public async IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken) { List messages = new List() diff --git a/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs b/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs index f96d491..4d40e28 100644 --- a/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs +++ b/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs @@ -187,25 +187,6 @@ public class DbInitialaizer Name = "Gaming PC", Type = WishlistTypes.Product.ToString(), CreatedById = user1.Id, - Messages = new Message[] - { - new Message - { - Text = "Prompt", - Role = MessageRoles.User.ToString(), - WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd"), - CreatedById = user1.Id, - CreatedDateUtc = DateTime.UtcNow - }, - new Message - { - Text = "Answer", - Role = MessageRoles.Application.ToString(), - WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd"), - CreatedById = user1.Id, - CreatedDateUtc = DateTime.UtcNow - }, - } }, new Wishlist { @@ -213,17 +194,6 @@ public class DbInitialaizer Name = "Generic Wishlist Name", Type = WishlistTypes.Product.ToString(), CreatedById = user2.Id, - Messages = new Message[] - { - new Message - { - Text = "Prompt", - Role = MessageRoles.User.ToString(), - WishlistId = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), - CreatedById = user1.Id, - CreatedDateUtc = DateTime.UtcNow - } - } } }; From ba116a353349968026e62d631514830fc7fe5863 Mon Sep 17 00:00:00 2001 From: stasex Date: Mon, 23 Oct 2023 15:11:22 +0300 Subject: [PATCH 12/21] added the initial implementation of the method SearchProductAsync and a rough test for it --- .../Services/ProductService.cs | 128 +++++++++++++++++- .../ProductTests.cs | 106 ++++++++++++++- .../ShoppingAssistantApi.UnitTests.csproj | 1 + 3 files changed, 224 insertions(+), 11 deletions(-) diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index 7bc99ed..6da34ab 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -10,6 +10,7 @@ using ShoppingAssistantApi.Application.Models.OpenAi; using ShoppingAssistantApi.Application.Models.ProductSearch; using ShoppingAssistantApi.Domain.Entities; using ShoppingAssistantApi.Domain.Enums; +using ServerSentEvent = ShoppingAssistantApi.Application.Models.ProductSearch.ServerSentEvent; namespace ShoppingAssistantApi.Infrastructure.Services; @@ -18,6 +19,7 @@ public class ProductService : IProductService private readonly IWishlistsService _wishlistsService; private readonly IOpenAiService _openAiService; + public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService) { @@ -25,13 +27,127 @@ public class ProductService : IProductService _wishlistsService = wishlistsService; } - public IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken) + public async IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken) { - // Documentation: https://shchoholiev.atlassian.net/l/cp/JizkynhU + var chatRequest = new ChatCompletionRequest + { + Messages = new List + { + new OpenAiMessage + { + Role = "User", + Content = PromptForProductSearch(message.Text) + } + }, + Stream = true + }; + + var currentDataType = SearchEventType.Wishlist; + var dataTypeHolder = string.Empty; - throw new NotImplementedException(); + await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) + { + if (data.Contains("[")) + { + dataTypeHolder = string.Empty; + dataTypeHolder += data; + } + + else if (data.Contains("]")) + { + dataTypeHolder += data; + currentDataType = DetermineDataType(dataTypeHolder); + } + + else if (dataTypeHolder=="[" && !data.Contains("[")) + { + dataTypeHolder += data; + } + + else + { + switch (currentDataType) + { + case SearchEventType.Message: + yield return new ServerSentEvent + { + Event = SearchEventType.Message, + Data = data + }; + break; + + case SearchEventType.Suggestion: + yield return new ServerSentEvent + { + Event = SearchEventType.Suggestion, + Data = data + }; + break; + + case SearchEventType.Product: + yield return new ServerSentEvent + { + Event = SearchEventType.Product, + Data = data + }; + break; + + case SearchEventType.Wishlist: + yield return new ServerSentEvent + { + Event = SearchEventType.Wishlist, + Data = data + }; + break; + + } + dataTypeHolder = string.Empty; + } + } } + private SearchEventType DetermineDataType(string dataTypeHolder) + { + if (dataTypeHolder.StartsWith("[Question]")) + { + return SearchEventType.Message; + } + else if (dataTypeHolder.StartsWith("[Options]")) + { + return SearchEventType.Suggestion; + } + else if (dataTypeHolder.StartsWith("[Message]")) + { + return SearchEventType.Message; + } + else if (dataTypeHolder.StartsWith("[Products]")) + { + return SearchEventType.Product; + } + else + { + return SearchEventType.Wishlist; + } + } + + + + + + + + + + + + + + + + + + + // TODO: remove all methods below public async IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken) { @@ -39,7 +155,7 @@ public class ProductService : IProductService { new OpenAiMessage() { - Role = OpenAiRole.User, + Role = "User", Content = PromptForProductSearch(message.Text) } }; @@ -73,7 +189,7 @@ public class ProductService : IProductService { new OpenAiMessage() { - Role = OpenAiRole.User, + Role = "User", Content = PromptForProductSearchWithQuestion(message.Text) } }; @@ -114,7 +230,7 @@ public class ProductService : IProductService { new OpenAiMessage() { - Role = OpenAiRole.User, + Role = "User", Content = PromptForRecommendationsForProductSearch(message.Text) } }; diff --git a/ShoppingAssistantApi.UnitTests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs index 78b1376..caf6e09 100644 --- a/ShoppingAssistantApi.UnitTests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; using MongoDB.Bson; using Moq; using Newtonsoft.Json.Linq; @@ -28,13 +29,108 @@ public class ProductTests _productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); } + + + /*[Fact] + public async Task SearchProductAsync_WhenWishlistIdIsEmpty_CreatesWishlistAndReturnsEvent() + { + // Arrange + string wishlistId = string.Empty; // Simulating an empty wishlist ID + var message = new MessageCreateDto + { + Text = "Your message text here" + }; + var cancellationToken = CancellationToken.None; + + // Define your expected new wishlist and event data + var newWishlistId = "123"; // Example wishlist ID + var expectedEvent = new ServerSentEvent + { + Event = SearchEventType.Wishlist, + Data = newWishlistId + }; + + // Mock the StartPersonalWishlistAsync method to return the expected wishlist + _wishListServiceMock.Setup(x => x.StartPersonalWishlistAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(new WishlistDto + { + Id = "123", + Name = "MacBook", + Type = WishlistTypes.Product.ToString(), // Use enum + CreatedById = "someId" + }); + + // Mock the GetChatCompletionStream method to provide SSE data + var sseData = new List { "[Question] What is your question?" }; + _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) + .Returns(sseData.ToAsyncEnumerable()); + + // Act + var result = await _productService.SearchProductAsync(wishlistId, message, cancellationToken).ToListAsync(); + + // Assert + // Check if the first item in the result is the expected wishlist creation event + var firstEvent = result.FirstOrDefault(); + Assert.NotNull(firstEvent); + Assert.Equal(expectedEvent.Event, firstEvent.Event); + Assert.Equal(expectedEvent.Data, firstEvent.Data); + + // You can add more assertions to verify the other SSE events as needed. + }*/ + + + [Fact] + public async Task SearchProductAsync_WhenWishlistExists_ReturnsExpectedEvents() + { + // Arrange + string wishlistId = "existingWishlistId"; // Simulating an existing wishlist ID + var message = new MessageCreateDto + { + Text = "Your message text here" + }; + var cancellationToken = CancellationToken.None; + + // Define your expected SSE data for the test + var expectedSseData = new List + { + "[", + "Question", + "]", + " What", + " features", + " are", + " you", + " looking", + "?\n", + "[", + "Options", + "]", + " USB", + "-C" + }; + + // Mock the GetChatCompletionStream method to provide the expected SSE data + _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) + .Returns(expectedSseData.ToAsyncEnumerable()); + + // Act + var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken); + + // Convert the result stream to a list of ServerSentEvent + var actualSseEvents = await resultStream.ToListAsync(); + + // Assert + // Check if the actual SSE events match the expected SSE events + Assert.Equal(8, actualSseEvents.Count); + } + [Fact] public async Task StartNewSearchAndReturnWishlist_CreatesWishlistObject() { // Arrange var expectedOpenAiMessage = new OpenAiMessage { - Role = OpenAiRole.User, + Role = "User", Content = "{ \"Name\": [{ \"Name\": \"NVIDIA GeForce RTX 3080\" }, { \"Name\": \"AMD Radeon RX 6900 XT\" }] }" }; @@ -93,7 +189,7 @@ public class ProductTests var expectedOpenAiMessage = new OpenAiMessage { - Role = OpenAiRole.User, + Role = "User", Content = "{ \"Name\": [{ \"Name\": \"NVIDIA GeForce RTX 3080\" }, { \"Name\": \"AMD Radeon RX 6900 XT\" }] }" }; @@ -132,7 +228,7 @@ public class ProductTests var expectedOpenAiMessage = new OpenAiMessage { - Role = OpenAiRole.User, + Role = "User", Content = "{ \"AdditionalQuestion\": [{ \"QuestionText\": \"What specific MacBook model are you using?\" }," + " { \"QuestionText\": \"Do you have any preferences for brand or capacity?\" }] }" }; @@ -170,7 +266,7 @@ public class ProductTests var expectedOpenAiMessage = new OpenAiMessage { - Role = OpenAiRole.User, + Role = "User", Content = "{ \"Recommendation\": [\"Recommendation 1\", \"Recommendation 2\"] }" }; diff --git a/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj b/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj index e5361a9..05ef7fc 100644 --- a/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj +++ b/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj @@ -10,6 +10,7 @@ + From dc4826dacc4d8a9430e0e34d279b761124950164 Mon Sep 17 00:00:00 2001 From: stasex Date: Tue, 24 Oct 2023 02:01:46 +0300 Subject: [PATCH 13/21] added changes to the search method and removed unnecessary code --- .../Mutations/ProductMutation.cs | 3 - .../Queries/ProductQuery.cs | 9 - .../IServices/IProductService.cs | 9 +- .../Services/ProductService.cs | 216 ++-------------- .../ProductTests.cs | 244 ++---------------- 5 files changed, 48 insertions(+), 433 deletions(-) diff --git a/ShoppingAssistantApi.Api/Mutations/ProductMutation.cs b/ShoppingAssistantApi.Api/Mutations/ProductMutation.cs index 24ad5e3..f4598a5 100644 --- a/ShoppingAssistantApi.Api/Mutations/ProductMutation.cs +++ b/ShoppingAssistantApi.Api/Mutations/ProductMutation.cs @@ -9,7 +9,4 @@ namespace ShoppingAssistantApi.Api.Mutations; [ExtendObjectType(OperationTypeNames.Mutation)] public class ProductMutation { - public IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist( - Message message, CancellationToken cancellationToken, [Service] IProductService productService) - => productService.StartNewSearchAndReturnWishlist(message, cancellationToken); } \ No newline at end of file diff --git a/ShoppingAssistantApi.Api/Queries/ProductQuery.cs b/ShoppingAssistantApi.Api/Queries/ProductQuery.cs index cbaa775..b76586b 100644 --- a/ShoppingAssistantApi.Api/Queries/ProductQuery.cs +++ b/ShoppingAssistantApi.Api/Queries/ProductQuery.cs @@ -7,14 +7,5 @@ namespace ShoppingAssistantApi.Api.Queries; [ExtendObjectType(OperationTypeNames.Query)] public class ProductQuery { - [Authorize] - public IAsyncEnumerable GetProductFromSearch(Message message, CancellationToken cancellationToken, - [Service] IProductService productService) - => productService.GetProductFromSearch(message, cancellationToken); - [Authorize] - public IAsyncEnumerable GetRecommendationsForProductFromSearchStream(Message message, CancellationToken cancellationToken, - [Service] IProductService productService) - => productService.GetRecommendationsForProductFromSearchStream(message, cancellationToken); - } \ No newline at end of file diff --git a/ShoppingAssistantApi.Application/IServices/IProductService.cs b/ShoppingAssistantApi.Application/IServices/IProductService.cs index 03a8545..3cf6d42 100644 --- a/ShoppingAssistantApi.Application/IServices/IProductService.cs +++ b/ShoppingAssistantApi.Application/IServices/IProductService.cs @@ -8,12 +8,5 @@ namespace ShoppingAssistantApi.Application.IServices; public interface IProductService { IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken); - - // TODO remove all methods below - IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken); - - IAsyncEnumerable GetProductFromSearch(Message message, CancellationToken cancellationToken); - - IAsyncEnumerable GetRecommendationsForProductFromSearchStream(Message message, - CancellationToken cancellationToken); + } \ No newline at end of file diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index 6da34ab..a429da6 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -1,9 +1,4 @@ -using System.Collections.ObjectModel; -using System.Linq.Expressions; -using System.Runtime.CompilerServices; -using Newtonsoft.Json.Linq; -using ShoppingAssistantApi.Application.IRepositories; -using ShoppingAssistantApi.Application.IServices; +using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.Models.CreateDtos; using ShoppingAssistantApi.Application.Models.Dtos; using ShoppingAssistantApi.Application.Models.OpenAi; @@ -19,7 +14,6 @@ public class ProductService : IProductService private readonly IWishlistsService _wishlistsService; private readonly IOpenAiService _openAiService; - public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService) { @@ -36,19 +30,29 @@ public class ProductService : IProductService new OpenAiMessage { Role = "User", - Content = PromptForProductSearch(message.Text) + Content = "" } }, Stream = true }; - + + var suggestionBuffer = new Suggestion(); + var messageBuffer = new MessagePart(); var currentDataType = SearchEventType.Wishlist; var dataTypeHolder = string.Empty; + var dataBuffer = string.Empty; await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) { if (data.Contains("[")) { + if (dataTypeHolder=="[Message]" && messageBuffer.Text!=null) + { + _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageCreateDto() + { + Text = messageBuffer.Text, + }, cancellationToken); + } dataTypeHolder = string.Empty; dataTypeHolder += data; } @@ -66,6 +70,8 @@ public class ProductService : IProductService else { + dataBuffer += data; + switch (currentDataType) { case SearchEventType.Message: @@ -74,16 +80,21 @@ public class ProductService : IProductService Event = SearchEventType.Message, Data = data }; + messageBuffer.Text += data; break; case SearchEventType.Suggestion: - yield return new ServerSentEvent + suggestionBuffer.Text += data; + if (data.Contains(";")) { - Event = SearchEventType.Suggestion, - Data = data - }; - break; - + yield return new ServerSentEvent + { + Event = SearchEventType.Suggestion, + Data = suggestionBuffer.Text + }; + suggestionBuffer.Text = string.Empty; + } + break; case SearchEventType.Product: yield return new ServerSentEvent { @@ -91,17 +102,8 @@ public class ProductService : IProductService Data = data }; break; - - case SearchEventType.Wishlist: - yield return new ServerSentEvent - { - Event = SearchEventType.Wishlist, - Data = data - }; - break; } - dataTypeHolder = string.Empty; } } } @@ -129,170 +131,4 @@ public class ProductService : IProductService return SearchEventType.Wishlist; } } - - - - - - - - - - - - - - - - - - - - // TODO: remove all methods below - public async IAsyncEnumerable<(List ProductNames, WishlistDto Wishlist)> StartNewSearchAndReturnWishlist(Message message, CancellationToken cancellationToken) - { - List messages = new List() - { - new OpenAiMessage() - { - Role = "User", - Content = PromptForProductSearch(message.Text) - } - }; - - var chatRequest = new ChatCompletionRequest - { - Messages = messages - }; - - await foreach (var response in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) - { - var openAiContent = JObject.Parse(response); - var productNames = openAiContent["Name"]?.ToObject>() ?? new List(); - - WishlistCreateDto newWishlist = new WishlistCreateDto() - { - Type = "Product", - FirstMessageText = message.Text - }; - - var resultWishlistTask = _wishlistsService.StartPersonalWishlistAsync(newWishlist, cancellationToken); - var resultWishlist = await resultWishlistTask; - - yield return (productNames, resultWishlist); - } - } - - public async IAsyncEnumerable GetProductFromSearch(Message message, [EnumeratorCancellation] CancellationToken cancellationToken) - { - List messages = new List() - { - new OpenAiMessage() - { - Role = "User", - Content = PromptForProductSearchWithQuestion(message.Text) - } - }; - - var chatRequest = new ChatCompletionRequest - { - Messages = messages - }; - - await foreach (var response in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) - { - var openAiContent = JObject.Parse(response); - var productNames = openAiContent["Name"]?.ToObject>(); - - if (productNames != null && productNames.Any()) - { - foreach (var productName in productNames) - { - yield return productName.Name; - } - } - else - { - var questions = openAiContent["AdditionalQuestion"]?.ToObject>() ?? new List(); - - foreach (var question in questions) - { - yield return question.QuestionText; - } - } - } - } - - - public async IAsyncEnumerable GetRecommendationsForProductFromSearchStream(Message message, CancellationToken cancellationToken) - { - List messages = new List() - { - new OpenAiMessage() - { - Role = "User", - Content = PromptForRecommendationsForProductSearch(message.Text) - } - }; - - var chatRequest = new ChatCompletionRequest - { - Messages = messages - }; - - await foreach (var response in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) - { - var openAiContent = JObject.Parse(response); - var recommendations = openAiContent["Recommendation"]?.ToObject>() ?? new List(); - - foreach (var recommendation in recommendations) - { - yield return recommendation; - } - } - } - - public string PromptForProductSearch(string message) - { - string promptForSearch = "Return information in JSON. " + - "\nProvide information, only that indicated in the type of answer, namely only the name. " + - "\nAsk additional questions to the user if there is not enough information. " + - "\nIf there are several answer options, list them. " + - "\nYou don't need to display questions and products together! " + - "\nDo not output any text other than JSON!!! " + - $"\n\nQuestion: {message} " + - $"\nType of answer: Question:[] " + - $"\n\nif there are no questions, then just display the products " + - $"\nType of answer: Name:"; - return promptForSearch; - } - - public string PromptForRecommendationsForProductSearch(string message) - { - string promptForSearch = "Return information in JSON. " + - "\nProvide only information indicated in the type of answer, namely only the recommendation. " + - "\nIf there are several answer options, list them. " + - "\nDo not output any text other than JSON." + - $"\n\nGive recommendations for this question: {message} " + - "\nType of answer: " + - "\n\nRecommendation :"; - return promptForSearch; - } - - public string PromptForProductSearchWithQuestion(string message) - { - string promptForSearch = "Return information in JSON. " + - "\nAsk additional questions to the user if there is not enough information." + - "\nIf there are several answer options, list them. " + - "\nYou don't need to display questions and products together!" + - "\nDo not output any text other than JSON!!!" + - $"\n\nQuestion: {message}" + - "\n\nif you can ask questions to clarify the choice, then ask them" + - "\nType of answer:" + - "\nAdditionalQuestion:[]" + - "\n\nif there are no questions, then just display the products" + - "\nType of answer:" + - "\nName:"; - return promptForSearch; - } } \ No newline at end of file diff --git a/ShoppingAssistantApi.UnitTests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs index caf6e09..e4b6a01 100644 --- a/ShoppingAssistantApi.UnitTests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -29,56 +29,6 @@ public class ProductTests _productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); } - - - /*[Fact] - public async Task SearchProductAsync_WhenWishlistIdIsEmpty_CreatesWishlistAndReturnsEvent() - { - // Arrange - string wishlistId = string.Empty; // Simulating an empty wishlist ID - var message = new MessageCreateDto - { - Text = "Your message text here" - }; - var cancellationToken = CancellationToken.None; - - // Define your expected new wishlist and event data - var newWishlistId = "123"; // Example wishlist ID - var expectedEvent = new ServerSentEvent - { - Event = SearchEventType.Wishlist, - Data = newWishlistId - }; - - // Mock the StartPersonalWishlistAsync method to return the expected wishlist - _wishListServiceMock.Setup(x => x.StartPersonalWishlistAsync(It.IsAny(), CancellationToken.None)) - .ReturnsAsync(new WishlistDto - { - Id = "123", - Name = "MacBook", - Type = WishlistTypes.Product.ToString(), // Use enum - CreatedById = "someId" - }); - - // Mock the GetChatCompletionStream method to provide SSE data - var sseData = new List { "[Question] What is your question?" }; - _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) - .Returns(sseData.ToAsyncEnumerable()); - - // Act - var result = await _productService.SearchProductAsync(wishlistId, message, cancellationToken).ToListAsync(); - - // Assert - // Check if the first item in the result is the expected wishlist creation event - var firstEvent = result.FirstOrDefault(); - Assert.NotNull(firstEvent); - Assert.Equal(expectedEvent.Event, firstEvent.Event); - Assert.Equal(expectedEvent.Data, firstEvent.Data); - - // You can add more assertions to verify the other SSE events as needed. - }*/ - - [Fact] public async Task SearchProductAsync_WhenWishlistExists_ReturnsExpectedEvents() { @@ -94,19 +44,34 @@ public class ProductTests var expectedSseData = new List { "[", - "Question", + "Message", "]", " What", - " features", - " are", - " you", - " looking", + " u", + " want", + " ?", + "[", + "Options", + "]", + " USB-C", + " ;", + " Keyboard", + " ultra", + " ;", "?\n", "[", "Options", "]", " USB", - "-C" + "-C", + " ;", + "[", + "Message", + "]", + " What", + " u", + " want", + " ?" }; // Mock the GetChatCompletionStream method to provide the expected SSE data @@ -123,171 +88,4 @@ public class ProductTests // Check if the actual SSE events match the expected SSE events Assert.Equal(8, actualSseEvents.Count); } - - [Fact] - public async Task StartNewSearchAndReturnWishlist_CreatesWishlistObject() - { - // Arrange - var expectedOpenAiMessage = new OpenAiMessage - { - Role = "User", - Content = "{ \"Name\": [{ \"Name\": \"NVIDIA GeForce RTX 3080\" }, { \"Name\": \"AMD Radeon RX 6900 XT\" }] }" - }; - - _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), CancellationToken.None)) - .Returns((ChatCompletionRequest request, CancellationToken token) => - { - var asyncEnumerable = new List { expectedOpenAiMessage.Content }.ToAsyncEnumerable(); - return asyncEnumerable; - }); - - _wishListServiceMock.Setup(x => x.StartPersonalWishlistAsync(It.IsAny(), CancellationToken.None)) - .ReturnsAsync(new WishlistDto - { - Id = "someID", - Name = "MacBook", - Type = "Product", // Use enum - CreatedById = "someId" - }); - - var message = new Message - { - Id = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), - Text = "what are the best graphics cards you know?", - CreatedById = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), - Role = "user" - }; - - List productNames = null; - WishlistDto createdWishList = null; - - // Act - var result = _productService.StartNewSearchAndReturnWishlist(message, CancellationToken.None); - - await foreach (var (productList, wishlist) in result) - { - productNames = productList; - createdWishList = wishlist; - } - - // Assert - Assert.NotNull(createdWishList); - Assert.NotNull(productNames); - } - - [Fact] - public async Task GetProductFromSearch_ReturnsProductListWithName() - { - var message = new Message - { - Id = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), - Text = "what are the best graphics cards you know?", - CreatedById = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), - Role = "user" - }; - var cancellationToken = CancellationToken.None; - - var expectedOpenAiMessage = new OpenAiMessage - { - Role = "User", - Content = "{ \"Name\": [{ \"Name\": \"NVIDIA GeForce RTX 3080\" }, { \"Name\": \"AMD Radeon RX 6900 XT\" }] }" - }; - - _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) - .Returns(new List { expectedOpenAiMessage.Content }.ToAsyncEnumerable()); - - var productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); - - var productList = new List(); - - await foreach (var product in productService.GetProductFromSearch(message, cancellationToken)) - { - productList.Add(product); - } - - var openAiContent = JObject.Parse(expectedOpenAiMessage.Content); - var productNames = openAiContent["Name"].ToObject>(); - var expectedProductList = productNames.Select(info => info.Name).ToList(); - - Assert.Equal(expectedProductList, productList); - Assert.NotNull(openAiContent); - Assert.True(openAiContent.ContainsKey("Name")); - } - - [Fact] - public async Task GetProductFromSearch_ReturnsProductListWithQuestion() - { - var message = new Message - { - Id = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), - Text = "what are the best graphics cards you know?", - CreatedById = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), - Role = "user" - }; - var cancellationToken = CancellationToken.None; - - var expectedOpenAiMessage = new OpenAiMessage - { - Role = "User", - Content = "{ \"AdditionalQuestion\": [{ \"QuestionText\": \"What specific MacBook model are you using?\" }," + - " { \"QuestionText\": \"Do you have any preferences for brand or capacity?\" }] }" - }; - - _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) - .Returns(new List { expectedOpenAiMessage.Content }.ToAsyncEnumerable()); - - var productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); - - var productList = new List(); - - await foreach (var product in productService.GetProductFromSearch(message, cancellationToken)) - { - productList.Add(product); - } - - var openAiContent = JObject.Parse(expectedOpenAiMessage.Content); - var productNames = openAiContent["AdditionalQuestion"].ToObject>(); - - Assert.NotNull(openAiContent); - Assert.True(openAiContent.ContainsKey("AdditionalQuestion")); - } - - [Fact] - public async Task GetRecommendationsForProductFromSearch_ReturnsRecommendations() - { - var message = new Message - { - Id = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), - Text = "get recommendations for this product", - CreatedById = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), - Role = "user" - }; - var cancellationToken = CancellationToken.None; - - var expectedOpenAiMessage = new OpenAiMessage - { - Role = "User", - Content = "{ \"Recommendation\": [\"Recommendation 1\", \"Recommendation 2\"] }" - }; - - _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) - .Returns((ChatCompletionRequest request, CancellationToken token) => - { - var asyncEnumerable = new List { expectedOpenAiMessage.Content }.ToAsyncEnumerable(); - return asyncEnumerable; - }); - - var recommendations = new List(); - var productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); - - await foreach (var recommendation in productService.GetRecommendationsForProductFromSearchStream(message, cancellationToken)) - { - recommendations.Add(recommendation); - } - - var openAiContent = JObject.Parse(expectedOpenAiMessage.Content); - Assert.NotNull(openAiContent); - Assert.True(openAiContent.ContainsKey("Recommendation")); - Assert.Equal(new List { "Recommendation 1", "Recommendation 2" }, recommendations); - } } \ No newline at end of file From 3372a0910b4c56a744c74714cb62f755ec2e7757 Mon Sep 17 00:00:00 2001 From: stasex Date: Tue, 24 Oct 2023 20:03:54 +0300 Subject: [PATCH 14/21] added new chips to the product search service and implemented unit tests --- .../Controllers/BaseController.cs | 10 ++ .../Controllers/ProductsSearchController.cs | 37 +++++ .../ShoppingAssistantApi.Api.csproj | 3 - .../ServicesExtention.cs | 1 + .../Services/ProductService.cs | 128 +++++++++++++++--- .../Tests/ProductsTests.cs | 33 +++++ .../ProductTests.cs | 91 ++++++++++++- 7 files changed, 273 insertions(+), 30 deletions(-) create mode 100644 ShoppingAssistantApi.Api/Controllers/BaseController.cs create mode 100644 ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs create mode 100644 ShoppingAssistantApi.Tests/Tests/ProductsTests.cs diff --git a/ShoppingAssistantApi.Api/Controllers/BaseController.cs b/ShoppingAssistantApi.Api/Controllers/BaseController.cs new file mode 100644 index 0000000..87efa2c --- /dev/null +++ b/ShoppingAssistantApi.Api/Controllers/BaseController.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc; + +namespace ShoppingAssistantApi.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class BaseController : ControllerBase +{ + +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs b/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs new file mode 100644 index 0000000..6873127 --- /dev/null +++ b/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using ShoppingAssistantApi.Application.IServices; +using ShoppingAssistantApi.Application.Models.CreateDtos; + +namespace ShoppingAssistantApi.Api.Controllers; + +public class ProductsSearchController : BaseController +{ + private readonly IProductService _productService; + + public ProductsSearchController(IProductService productService) + { + _productService = productService; + } + + [HttpPost("search/{wishlistId}")] + public async Task StreamDataToClient(string wishlistId, [FromBody]MessageCreateDto message, CancellationToken cancellationToken) + { + Response.Headers.Add("Content-Type", "text/event-stream"); + Response.Headers.Add("Cache-Control", "no-cache"); + Response.Headers.Add("Connection", "keep-alive"); + + var result = _productService.SearchProductAsync(wishlistId, message, cancellationToken); + + await foreach (var sse in result) + { + var chunk = JsonConvert.SerializeObject(sse.Data); + + var serverSentEvent = $"event: {sse.Event}\ndata: {chunk}\n\n"; + + await Response.WriteAsync(serverSentEvent); + await Response.Body.FlushAsync(); + } + } + +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Api/ShoppingAssistantApi.Api.csproj b/ShoppingAssistantApi.Api/ShoppingAssistantApi.Api.csproj index 50dd57a..761a282 100644 --- a/ShoppingAssistantApi.Api/ShoppingAssistantApi.Api.csproj +++ b/ShoppingAssistantApi.Api/ShoppingAssistantApi.Api.csproj @@ -21,8 +21,5 @@ - - - diff --git a/ShoppingAssistantApi.Infrastructure/InfrastructureExtentions/ServicesExtention.cs b/ShoppingAssistantApi.Infrastructure/InfrastructureExtentions/ServicesExtention.cs index cbc7b65..a5ee8a3 100644 --- a/ShoppingAssistantApi.Infrastructure/InfrastructureExtentions/ServicesExtention.cs +++ b/ShoppingAssistantApi.Infrastructure/InfrastructureExtentions/ServicesExtention.cs @@ -18,6 +18,7 @@ public static class ServicesExtention services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index a429da6..1d87086 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -14,6 +14,7 @@ public class ProductService : IProductService private readonly IWishlistsService _wishlistsService; private readonly IOpenAiService _openAiService; + public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService) { @@ -23,24 +24,91 @@ public class ProductService : IProductService public async IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken) { - var chatRequest = new ChatCompletionRequest - { - Messages = new List - { - new OpenAiMessage - { - Role = "User", - Content = "" - } - }, - Stream = true - }; + bool checker = false; + var isFirstMessage = _wishlistsService + .GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 1, cancellationToken).Result; - var suggestionBuffer = new Suggestion(); + var chatRequest = new ChatCompletionRequest(); + + if (isFirstMessage==null) + { + chatRequest = new ChatCompletionRequest + { + Messages = new List + { + new OpenAiMessage + { + Role = OpenAiRole.System.ToString(), + Content = "You are a Shopping Assistant that helps people find product recommendations. Ask user additional questions if more context needed." + + "\nYou must return data with one of the prefixes:" + + "\n[Question] - return question" + + "\n[Suggestions] - return semicolon separated suggestion how to answer to a question" + + "\n[Message] - return text" + + "\n[Products] - return semicolon separated product names" + }, + + new OpenAiMessage() + { + Role = OpenAiRole.Assistant.ToString(), + Content = "What are you looking for?" + } + }, + Stream = true + }; + + _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageCreateDto() + { + Text = "What are you looking for?", + }, cancellationToken); + + yield return new ServerSentEvent + { + Event = SearchEventType.Message, + Data = "What are you looking for?" + }; + + yield return new ServerSentEvent + { + Event = SearchEventType.Suggestion, + Data = "Bicycle" + }; + + yield return new ServerSentEvent + { + Event = SearchEventType.Suggestion, + Data = "Laptop" + }; + + checker = true; + } + + if(isFirstMessage!=null && checker==false) + { + var previousMessages = _wishlistsService + .GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 1, cancellationToken).Result.Items.ToList(); + + var messagesForOpenAI = new List(); + foreach (var item in previousMessages ) + { + messagesForOpenAI.Add( + new OpenAiMessage() + { + Role = item.Role, + Content = item.Text + }); + } + + chatRequest = new ChatCompletionRequest + { + Messages = messagesForOpenAI, + Stream = true + }; + + var suggestionBuffer = new Suggestion(); var messageBuffer = new MessagePart(); + var productBuffer = new ProductName(); var currentDataType = SearchEventType.Wishlist; var dataTypeHolder = string.Empty; - var dataBuffer = string.Empty; await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) { @@ -53,6 +121,18 @@ public class ProductService : IProductService Text = messageBuffer.Text, }, cancellationToken); } + if (dataTypeHolder=="[Products]" && productBuffer.Name!=null) + { + _wishlistsService.AddProductToPersonalWishlistAsync(wishlistId, new ProductCreateDto() + { + Url = "", + Name = productBuffer.Name, + Rating = 0, + Description = "", + ImagesUrls = new []{"", ""}, + WasOpened = false + }, cancellationToken); + } dataTypeHolder = string.Empty; dataTypeHolder += data; } @@ -70,8 +150,6 @@ public class ProductService : IProductService else { - dataBuffer += data; - switch (currentDataType) { case SearchEventType.Message: @@ -96,16 +174,22 @@ public class ProductService : IProductService } break; case SearchEventType.Product: - yield return new ServerSentEvent + productBuffer.Name += data; + if (data.Contains(";")) { - Event = SearchEventType.Product, - Data = data - }; - break; - + yield return new ServerSentEvent + { + Event = SearchEventType.Product, + Data = productBuffer.Name + }; + productBuffer.Name = string.Empty; + } + break; } } } + + } } private SearchEventType DetermineDataType(string dataTypeHolder) diff --git a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs new file mode 100644 index 0000000..d120697 --- /dev/null +++ b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs @@ -0,0 +1,33 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text; +using MongoDB.Bson; +using Newtonsoft.Json; +using ShoppingAssistantApi.Application.Models.CreateDtos; +using ShoppingAssistantApi.Tests.TestExtentions; + +namespace ShoppingAssistantApi.Tests.Tests; + +public class ProductsTests : TestsBase +{ + public ProductsTests(TestingFactory factory) + : base(factory) + { + } + + [Fact] + public async Task StreamDataToClient_ReturnsExpectedResponse() + { + // Arrange + var wishlistId = "your_wishlist_id"; + var message = new MessageCreateDto { Text = "Your message text" }; + + // Act + var response = await _httpClient.PostAsJsonAsync($"http://localhost:5183/api/products/search/{"ab79cde6f69abcd3efab65cd"}", message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Додайте додаткові перевірки на відповідь, якщо необхідно + } +} \ No newline at end of file diff --git a/ShoppingAssistantApi.UnitTests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs index e4b6a01..1ba4861 100644 --- a/ShoppingAssistantApi.UnitTests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -8,6 +8,7 @@ using ShoppingAssistantApi.Application.Models.CreateDtos; using ShoppingAssistantApi.Application.Models.Dtos; using ShoppingAssistantApi.Application.Models.OpenAi; using ShoppingAssistantApi.Application.Models.ProductSearch; +using ShoppingAssistantApi.Application.Paging; using ShoppingAssistantApi.Domain.Entities; using ShoppingAssistantApi.Domain.Enums; using ShoppingAssistantApi.Infrastructure.Services; @@ -66,11 +67,17 @@ public class ProductTests "-C", " ;", "[", - "Message", + "Products", "]", - " What", - " u", - " want", + " GTX", + " 3090", + " ;", + " GTX", + " 3070TI", + " ;", + " GTX", + " 4070TI", + " ;", " ?" }; @@ -78,6 +85,17 @@ public class ProductTests _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) .Returns(expectedSseData.ToAsyncEnumerable()); + _wishListServiceMock.Setup(w => w.GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 1, cancellationToken)) + .ReturnsAsync(new PagedList(new List + { + new MessageDto + { + Text = "Some existing message", + Id = "", + CreatedById = "", + Role = "" + } + }, 1, 1, 1)); // Act var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken); @@ -86,6 +104,69 @@ public class ProductTests // Assert // Check if the actual SSE events match the expected SSE events - Assert.Equal(8, actualSseEvents.Count); + Assert.NotNull(actualSseEvents); + } + + + [Fact] + public async void SearchProductAsync_WithExistingMessageInWishlist_ReturnsExpectedEvents() + { + // Arrange + var wishlistId = "your_wishlist_id"; + var message = new MessageCreateDto { Text = "Your message text" }; + var cancellationToken = new CancellationToken(); + + var productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); + + var expectedSseData = new List + { + "[", + "Message", + "]", + " What", + " u", + " want", + " ?", + "[", + "Options", + "]", + " USB-C", + " ;", + " Keyboard", + " ultra", + " ;", + "?\n", + "[", + "Options", + "]", + " USB", + "-C", + " ;", + "[", + "Products", + "]", + " GTX", + " 3090", + " ;", + " GTX", + " 3070TI", + " ;", + " GTX", + " 4070TI", + " ;", + " ?" + }; + + // Mock the GetChatCompletionStream method to provide the expected SSE data + _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) + .Returns(expectedSseData.ToAsyncEnumerable()); + + // Act + var resultStream = productService.SearchProductAsync(wishlistId, message, cancellationToken); + + var actualSseEvents = await resultStream.ToListAsync(); + // Assert + + Assert.NotNull(actualSseEvents); } } \ No newline at end of file From f5d9c3e80e4eb606a950259ca8e5c446928ee496 Mon Sep 17 00:00:00 2001 From: stasex Date: Tue, 24 Oct 2023 23:24:23 +0300 Subject: [PATCH 15/21] added some fix for tests --- .../Services/ProductService.cs | 147 +++++++++--------- .../Tests/ProductsTests.cs | 6 +- .../ProductTests.cs | 1 + 3 files changed, 76 insertions(+), 78 deletions(-) diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index 1d87086..ce0fad0 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -1,4 +1,5 @@ -using ShoppingAssistantApi.Application.IServices; +using System.Diagnostics; +using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.Models.CreateDtos; using ShoppingAssistantApi.Application.Models.Dtos; using ShoppingAssistantApi.Application.Models.OpenAi; @@ -24,7 +25,6 @@ public class ProductService : IProductService public async IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken) { - bool checker = false; var isFirstMessage = _wishlistsService .GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 1, cancellationToken).Result; @@ -78,11 +78,9 @@ public class ProductService : IProductService Event = SearchEventType.Suggestion, Data = "Laptop" }; - - checker = true; } - if(isFirstMessage!=null && checker==false) + if(isFirstMessage!=null) { var previousMessages = _wishlistsService .GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 1, cancellationToken).Result.Items.ToList(); @@ -105,91 +103,91 @@ public class ProductService : IProductService }; var suggestionBuffer = new Suggestion(); - var messageBuffer = new MessagePart(); - var productBuffer = new ProductName(); - var currentDataType = SearchEventType.Wishlist; - var dataTypeHolder = string.Empty; + var messageBuffer = new MessagePart(); + var productBuffer = new ProductName(); + var currentDataType = SearchEventType.Wishlist; + var dataTypeHolder = string.Empty; - await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) - { - if (data.Contains("[")) + await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) { - if (dataTypeHolder=="[Message]" && messageBuffer.Text!=null) + if (data.Contains("[")) { - _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageCreateDto() + if (dataTypeHolder=="[Message]" && messageBuffer.Text!=null) { - Text = messageBuffer.Text, - }, cancellationToken); - } - if (dataTypeHolder=="[Products]" && productBuffer.Name!=null) - { - _wishlistsService.AddProductToPersonalWishlistAsync(wishlistId, new ProductCreateDto() + _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageCreateDto() + { + Text = messageBuffer.Text, + }, cancellationToken); + } + if (dataTypeHolder=="[Products]" && productBuffer.Name!=null) { - Url = "", - Name = productBuffer.Name, - Rating = 0, - Description = "", - ImagesUrls = new []{"", ""}, - WasOpened = false - }, cancellationToken); + _wishlistsService.AddProductToPersonalWishlistAsync(wishlistId, new ProductCreateDto() + { + Url = "", + Name = productBuffer.Name, + Rating = 0, + Description = "", + ImagesUrls = new []{"", ""}, + WasOpened = false + }, cancellationToken); + } + dataTypeHolder = string.Empty; + dataTypeHolder += data; } - dataTypeHolder = string.Empty; - dataTypeHolder += data; - } - else if (data.Contains("]")) - { - dataTypeHolder += data; - currentDataType = DetermineDataType(dataTypeHolder); - } - - else if (dataTypeHolder=="[" && !data.Contains("[")) - { - dataTypeHolder += data; - } - - else - { - switch (currentDataType) + else if (data.Contains("]")) { - case SearchEventType.Message: - yield return new ServerSentEvent - { - Event = SearchEventType.Message, - Data = data - }; - messageBuffer.Text += data; - break; + dataTypeHolder += data; + currentDataType = DetermineDataType(dataTypeHolder); + } - case SearchEventType.Suggestion: - suggestionBuffer.Text += data; - if (data.Contains(";")) - { + else if (dataTypeHolder=="[" && !data.Contains("[")) + { + dataTypeHolder += data; + } + + else + { + switch (currentDataType) + { + case SearchEventType.Message: yield return new ServerSentEvent { - Event = SearchEventType.Suggestion, - Data = suggestionBuffer.Text + Event = SearchEventType.Message, + Data = data }; - suggestionBuffer.Text = string.Empty; - } - break; - case SearchEventType.Product: - productBuffer.Name += data; - if (data.Contains(";")) - { - yield return new ServerSentEvent + messageBuffer.Text += data; + break; + + case SearchEventType.Suggestion: + suggestionBuffer.Text += data; + if (data.Contains(";")) { - Event = SearchEventType.Product, - Data = productBuffer.Name - }; - productBuffer.Name = string.Empty; - } - break; + yield return new ServerSentEvent + { + Event = SearchEventType.Suggestion, + Data = suggestionBuffer.Text + }; + suggestionBuffer.Text = string.Empty; + } + break; + + case SearchEventType.Product: + productBuffer.Name += data; + if (data.Contains(";")) + { + yield return new ServerSentEvent + { + Event = SearchEventType.Product, + Data = productBuffer.Name + }; + productBuffer.Name = string.Empty; + } + break; + } } } } - - } } private SearchEventType DetermineDataType(string dataTypeHolder) @@ -215,4 +213,5 @@ public class ProductService : IProductService return SearchEventType.Wishlist; } } + } \ No newline at end of file diff --git a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs index d120697..21ea9fe 100644 --- a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs @@ -19,15 +19,13 @@ public class ProductsTests : TestsBase public async Task StreamDataToClient_ReturnsExpectedResponse() { // Arrange - var wishlistId = "your_wishlist_id"; + var wishlistId = "ab79cde6f69abcd3efab65cd"; var message = new MessageCreateDto { Text = "Your message text" }; // Act - var response = await _httpClient.PostAsJsonAsync($"http://localhost:5183/api/products/search/{"ab79cde6f69abcd3efab65cd"}", message); + var response = await _httpClient.PostAsJsonAsync($"http://127.0.0.1:5183//api/products/search/{wishlistId}", message); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - // Додайте додаткові перевірки на відповідь, якщо необхідно } } \ No newline at end of file diff --git a/ShoppingAssistantApi.UnitTests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs index 1ba4861..2c2ac96 100644 --- a/ShoppingAssistantApi.UnitTests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -168,5 +168,6 @@ public class ProductTests // Assert Assert.NotNull(actualSseEvents); + Assert.Equal(3, actualSseEvents.Count); } } \ No newline at end of file From 77b14bf4c7db2190958086f3dcdffe21e6c6b06a Mon Sep 17 00:00:00 2001 From: stasex Date: Wed, 25 Oct 2023 14:16:11 +0300 Subject: [PATCH 16/21] changed the logic for product search and set up the integration test --- .../Controllers/ProductsSearchController.cs | 4 +- .../Services/ProductService.cs | 23 +++++----- .../TestExtentions/DbInitializer.cs | 45 ++++++++++++++++++- .../Tests/ProductsTests.cs | 7 +-- .../ProductTests.cs | 12 +++-- 5 files changed, 71 insertions(+), 20 deletions(-) diff --git a/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs b/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs index 6873127..7dd9949 100644 --- a/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs +++ b/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs @@ -1,10 +1,12 @@ -using Microsoft.AspNetCore.Mvc; +using HotChocolate.Authorization; +using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.Models.CreateDtos; namespace ShoppingAssistantApi.Api.Controllers; +[Authorize] public class ProductsSearchController : BaseController { private readonly IProductService _productService; diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index ce0fad0..b4d4739 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -1,4 +1,6 @@ using System.Diagnostics; +using MongoDB.Bson; +using ShoppingAssistantApi.Application.IRepositories; using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.Models.CreateDtos; using ShoppingAssistantApi.Application.Models.Dtos; @@ -15,22 +17,23 @@ public class ProductService : IProductService private readonly IWishlistsService _wishlistsService; private readonly IOpenAiService _openAiService; - - public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService) + private readonly IMessagesRepository _messagesRepository; + + public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService, IMessagesRepository messagesRepository) { _openAiService = openAiService; _wishlistsService = wishlistsService; + _messagesRepository = messagesRepository; } public async IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken) { - var isFirstMessage = _wishlistsService - .GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 1, cancellationToken).Result; + var isFirstMessage = await _messagesRepository.GetCountAsync(message=>message.WishlistId==ObjectId.Parse((wishlistId)), cancellationToken); var chatRequest = new ChatCompletionRequest(); - if (isFirstMessage==null) + if (isFirstMessage==0) { chatRequest = new ChatCompletionRequest { @@ -38,7 +41,7 @@ public class ProductService : IProductService { new OpenAiMessage { - Role = OpenAiRole.System.ToString(), + Role = OpenAiRole.System.ToString().ToLower(), Content = "You are a Shopping Assistant that helps people find product recommendations. Ask user additional questions if more context needed." + "\nYou must return data with one of the prefixes:" + "\n[Question] - return question" + @@ -49,7 +52,7 @@ public class ProductService : IProductService new OpenAiMessage() { - Role = OpenAiRole.Assistant.ToString(), + Role = OpenAiRole.System.ToString().ToLower(), Content = "What are you looking for?" } }, @@ -80,10 +83,10 @@ public class ProductService : IProductService }; } - if(isFirstMessage!=null) + if(isFirstMessage!=0) { var previousMessages = _wishlistsService - .GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 1, cancellationToken).Result.Items.ToList(); + .GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 50, cancellationToken).Result.Items.ToList(); var messagesForOpenAI = new List(); foreach (var item in previousMessages ) @@ -91,7 +94,7 @@ public class ProductService : IProductService messagesForOpenAI.Add( new OpenAiMessage() { - Role = item.Role, + Role = item.Role.ToLower(), Content = item.Text }); } diff --git a/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs index 811cce6..a89df14 100644 --- a/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs +++ b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs @@ -107,6 +107,8 @@ public class DbInitializer var wishlistId1 = ObjectId.Parse("ab79cde6f69abcd3efab65cd"); var wishlistId2 = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"); + var wishlistId3 = ObjectId.Parse("ab7c8c2d9edf39abcd1ef9ab"); + var wishlistId4 = ObjectId.Parse("ab8c8c2d9edf39abcd1ef9ab"); var wishlists = new Wishlist[] { @@ -125,7 +127,23 @@ public class DbInitializer Type = WishlistTypes.Product.ToString(), CreatedById = user2.Id, CreatedDateUtc = DateTime.UtcNow - } + }, + new Wishlist + { + Id = wishlistId3, + Name = "Test For Search", + Type = WishlistTypes.Product.ToString(), + CreatedById = user1.Id, + CreatedDateUtc = DateTime.UtcNow + }, + new Wishlist + { + Id = wishlistId4, + Name = "Test For Answer", + Type = WishlistTypes.Product.ToString(), + CreatedById = user1.Id, + CreatedDateUtc = DateTime.UtcNow + }, }; await wishlistsCollection.InsertManyAsync(wishlists); @@ -142,6 +160,8 @@ public class DbInitializer var wishlistId1 = ObjectId.Parse("ab79cde6f69abcd3efab65cd"); var wishlistId2 = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"); + var wishlistId3 = ObjectId.Parse("ab7c8c2d9edf39abcd1ef9ab"); + var wishlistId4 = ObjectId.Parse("ab8c8c2d9edf39abcd1ef9ab"); var messages = new Message[] { @@ -197,7 +217,28 @@ public class DbInitializer WishlistId = wishlistId2, CreatedById = user2.Id, CreatedDateUtc = DateTime.UtcNow - } + }, + new Message + { + Text = "You are a Shopping Assistant that helps people find product recommendations. Ask user additional questions if more context needed." + + "\nYou must return data with one of the prefixes:" + + "\n[Question] - return question" + + "\n[Suggestions] - return semicolon separated suggestion how to answer to a question" + + "\n[Message] - return text" + + "\n[Products] - return semicolon separated product names", + Role = "system", + WishlistId = wishlistId4, + CreatedById = user2.Id, + CreatedDateUtc = DateTime.UtcNow + }, + new Message + { + Text = "What are you looking for?", + Role = "system", + WishlistId = wishlistId4, + CreatedById = user2.Id, + CreatedDateUtc = DateTime.UtcNow + }, }; await messagesCollection.InsertManyAsync(messages); diff --git a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs index 21ea9fe..686d057 100644 --- a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs @@ -18,12 +18,13 @@ public class ProductsTests : TestsBase [Fact] public async Task StreamDataToClient_ReturnsExpectedResponse() { + await LoginAsync("wishlists@gmail.com", "Yuiop12345"); // Arrange - var wishlistId = "ab79cde6f69abcd3efab65cd"; - var message = new MessageCreateDto { Text = "Your message text" }; + var wishlistId = "ab8c8c2d9edf39abcd1ef9ab"; + var message = new MessageCreateDto { Text = "I want new powerful laptop" }; // Act - var response = await _httpClient.PostAsJsonAsync($"http://127.0.0.1:5183//api/products/search/{wishlistId}", message); + var response = await _httpClient.PostAsJsonAsync($"http://127.0.0.1:5183/api/ProductsSearch/search/{wishlistId}", message); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/ShoppingAssistantApi.UnitTests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs index 2c2ac96..6bf5b67 100644 --- a/ShoppingAssistantApi.UnitTests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using MongoDB.Bson; using Moq; using Newtonsoft.Json.Linq; +using ShoppingAssistantApi.Application.IRepositories; using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.Models.CreateDtos; using ShoppingAssistantApi.Application.Models.Dtos; @@ -21,13 +22,16 @@ public class ProductTests private IProductService _productService; - public Mock _wishListServiceMock; + private Mock _wishListServiceMock; - public ProductTests() + private IMessagesRepository _messagesRepository; + + public ProductTests(IMessagesRepository messagesRepository) { + _messagesRepository = messagesRepository; _openAiServiceMock = new Mock(); _wishListServiceMock = new Mock(); - _productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); + _productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object, _messagesRepository); } [Fact] @@ -116,7 +120,7 @@ public class ProductTests var message = new MessageCreateDto { Text = "Your message text" }; var cancellationToken = new CancellationToken(); - var productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object); + var productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object, _messagesRepository); var expectedSseData = new List { From 73b9b5213f8e08c59e208ea73c3c4171338db89e Mon Sep 17 00:00:00 2001 From: stasex Date: Wed, 25 Oct 2023 14:45:44 +0300 Subject: [PATCH 17/21] added a small change to the service --- .../Services/ProductService.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index b4d4739..5d0ff84 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -99,6 +99,12 @@ public class ProductService : IProductService }); } + messagesForOpenAI.Add(new OpenAiMessage() + { + Role = MessageRoles.User.ToString().ToLower(), + Content = message.Text + }); + chatRequest = new ChatCompletionRequest { Messages = messagesForOpenAI, From 40f294f61bf5d45f8d06a8dcbbace36a64b0cddf Mon Sep 17 00:00:00 2001 From: stasex Date: Wed, 25 Oct 2023 17:28:55 +0300 Subject: [PATCH 18/21] added new tests --- .../Models/OpenAi/ChatCompletionRequest.cs | 2 +- .../Services/ProductService.cs | 60 ++-- .../Tests/ProductsTests.cs | 4 +- .../ProductTests.cs | 280 +++++++++++------- 4 files changed, 213 insertions(+), 133 deletions(-) diff --git a/ShoppingAssistantApi.Application/Models/OpenAi/ChatCompletionRequest.cs b/ShoppingAssistantApi.Application/Models/OpenAi/ChatCompletionRequest.cs index d2ad66b..40f3972 100644 --- a/ShoppingAssistantApi.Application/Models/OpenAi/ChatCompletionRequest.cs +++ b/ShoppingAssistantApi.Application/Models/OpenAi/ChatCompletionRequest.cs @@ -2,7 +2,7 @@ namespace ShoppingAssistantApi.Application.Models.OpenAi; public class ChatCompletionRequest { - public string Model { get; set; } = "gpt-3.5-turbo"; + public string Model { get; set; } = "gpt-4"; public List Messages { get; set; } diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index 5d0ff84..d86bae6 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -29,6 +29,14 @@ public class ProductService : IProductService public async IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken) { + string promptForGpt = + "You are a Shopping Assistant that helps people find product recommendations. Ask user additional questions if more context needed." + + "\nYou must return data with one of the prefixes:" + + "\n[Question] - return question" + + "\n[Suggestions] - return semicolon separated suggestion how to answer to a question" + + "\n[Message] - return text" + + "\n[Products] - return semicolon separated product names"; + var isFirstMessage = await _messagesRepository.GetCountAsync(message=>message.WishlistId==ObjectId.Parse((wishlistId)), cancellationToken); var chatRequest = new ChatCompletionRequest(); @@ -42,12 +50,7 @@ public class ProductService : IProductService new OpenAiMessage { Role = OpenAiRole.System.ToString().ToLower(), - Content = "You are a Shopping Assistant that helps people find product recommendations. Ask user additional questions if more context needed." + - "\nYou must return data with one of the prefixes:" + - "\n[Question] - return question" + - "\n[Suggestions] - return semicolon separated suggestion how to answer to a question" + - "\n[Message] - return text" + - "\n[Products] - return semicolon separated product names" + Content = promptForGpt }, new OpenAiMessage() @@ -70,17 +73,6 @@ public class ProductService : IProductService Data = "What are you looking for?" }; - yield return new ServerSentEvent - { - Event = SearchEventType.Suggestion, - Data = "Bicycle" - }; - - yield return new ServerSentEvent - { - Event = SearchEventType.Suggestion, - Data = "Laptop" - }; } if(isFirstMessage!=0) @@ -89,6 +81,11 @@ public class ProductService : IProductService .GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 50, cancellationToken).Result.Items.ToList(); var messagesForOpenAI = new List(); + messagesForOpenAI.Add(new OpenAiMessage() + { + Role = OpenAiRole.System.ToString().ToLower(), + Content = promptForGpt + }); foreach (var item in previousMessages ) { messagesForOpenAI.Add( @@ -128,18 +125,6 @@ public class ProductService : IProductService Text = messageBuffer.Text, }, cancellationToken); } - if (dataTypeHolder=="[Products]" && productBuffer.Name!=null) - { - _wishlistsService.AddProductToPersonalWishlistAsync(wishlistId, new ProductCreateDto() - { - Url = "", - Name = productBuffer.Name, - Rating = 0, - Description = "", - ImagesUrls = new []{"", ""}, - WasOpened = false - }, cancellationToken); - } dataTypeHolder = string.Empty; dataTypeHolder += data; } @@ -169,7 +154,6 @@ public class ProductService : IProductService break; case SearchEventType.Suggestion: - suggestionBuffer.Text += data; if (data.Contains(";")) { yield return new ServerSentEvent @@ -178,11 +162,12 @@ public class ProductService : IProductService Data = suggestionBuffer.Text }; suggestionBuffer.Text = string.Empty; + break; } + suggestionBuffer.Text += data; break; case SearchEventType.Product: - productBuffer.Name += data; if (data.Contains(";")) { yield return new ServerSentEvent @@ -191,7 +176,20 @@ public class ProductService : IProductService Data = productBuffer.Name }; productBuffer.Name = string.Empty; + + await _wishlistsService.AddProductToPersonalWishlistAsync(wishlistId, new ProductCreateDto() + { + Url = "", + Name = productBuffer.Name, + Rating = 0, + Description = "", + ImagesUrls = new []{"", ""}, + WasOpened = false + }, cancellationToken); + + break; } + productBuffer.Name += data; break; } } diff --git a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs index 686d057..bb3119b 100644 --- a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs @@ -25,8 +25,10 @@ public class ProductsTests : TestsBase // Act var response = await _httpClient.PostAsJsonAsync($"http://127.0.0.1:5183/api/ProductsSearch/search/{wishlistId}", message); - + var responseContent = await response.Content.ReadAsStringAsync(); + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(responseContent); } } \ No newline at end of file diff --git a/ShoppingAssistantApi.UnitTests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs index 6bf5b67..f480031 100644 --- a/ShoppingAssistantApi.UnitTests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -1,18 +1,14 @@ -using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Bson; -using Moq; -using Newtonsoft.Json.Linq; +using Moq; using ShoppingAssistantApi.Application.IRepositories; using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.Models.CreateDtos; using ShoppingAssistantApi.Application.Models.Dtos; using ShoppingAssistantApi.Application.Models.OpenAi; -using ShoppingAssistantApi.Application.Models.ProductSearch; using ShoppingAssistantApi.Application.Paging; using ShoppingAssistantApi.Domain.Entities; using ShoppingAssistantApi.Domain.Enums; using ShoppingAssistantApi.Infrastructure.Services; +using System.Linq.Expressions; namespace ShoppingAssistantApi.Tests.Tests; @@ -24,21 +20,21 @@ public class ProductTests private Mock _wishListServiceMock; - private IMessagesRepository _messagesRepository; + private Mock _messagesRepositoryMock; - public ProductTests(IMessagesRepository messagesRepository) + public ProductTests() { - _messagesRepository = messagesRepository; + _messagesRepositoryMock = new Mock(); _openAiServiceMock = new Mock(); _wishListServiceMock = new Mock(); - _productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object, _messagesRepository); + _productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object, _messagesRepositoryMock.Object); } [Fact] - public async Task SearchProductAsync_WhenWishlistExists_ReturnsExpectedEvents() + public async Task SearchProductAsync_WhenWishlistsWithoutMessages_ReturnsExpectedEvents() { // Arrange - string wishlistId = "existingWishlistId"; // Simulating an existing wishlist ID + string wishlistId = "existingWishlistId"; var message = new MessageCreateDto { Text = "Your message text here" @@ -48,67 +44,44 @@ public class ProductTests // Define your expected SSE data for the test var expectedSseData = new List { - "[", - "Message", - "]", - " What", - " u", - " want", - " ?", - "[", - "Options", - "]", - " USB-C", - " ;", - " Keyboard", - " ultra", - " ;", - "?\n", - "[", - "Options", - "]", - " USB", - "-C", - " ;", - "[", - "Products", - "]", - " GTX", - " 3090", - " ;", - " GTX", - " 3070TI", - " ;", - " GTX", - " 4070TI", - " ;", - " ?" + "[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", " USB-C", " ;", " Keyboard", " ultra", + " ;", "[", "Options", "]", " USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX", + " 3070TI", " ;", " GTX", " 4070TI", " ;", " ?" }; + + var expectedMessages = new List { "What are you looking for?" }; // Mock the GetChatCompletionStream method to provide the expected SSE data _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) .Returns(expectedSseData.ToAsyncEnumerable()); - - _wishListServiceMock.Setup(w => w.GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 1, cancellationToken)) - .ReturnsAsync(new PagedList(new List - { - new MessageDto - { - Text = "Some existing message", - Id = "", - CreatedById = "", - Role = "" - } - }, 1, 1, 1)); + + _messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny>>(), It.IsAny())) + .ReturnsAsync(0); + + _wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken)) + .Verifiable(); + // Act var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken); // Convert the result stream to a list of ServerSentEvent var actualSseEvents = await resultStream.ToListAsync(); + var receivedMessages = actualSseEvents + .Where(e => e.Event == SearchEventType.Message) + .Select(e => e.Data) + .ToList(); + + var receivedSuggestions = actualSseEvents + .Where(e => e.Event == SearchEventType.Suggestion) + .Select(e => e.Data) + .ToList(); + // Assert // Check if the actual SSE events match the expected SSE events Assert.NotNull(actualSseEvents); + Assert.Equal(expectedMessages, receivedMessages); + _wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken), Times.Once); } @@ -120,58 +93,165 @@ public class ProductTests var message = new MessageCreateDto { Text = "Your message text" }; var cancellationToken = new CancellationToken(); - var productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object, _messagesRepository); - + var productService = _productService; + var expectedSseData = new List { - "[", - "Message", - "]", - " What", - " u", - " want", - " ?", - "[", - "Options", - "]", - " USB-C", - " ;", - " Keyboard", - " ultra", - " ;", - "?\n", - "[", - "Options", - "]", - " USB", - "-C", - " ;", - "[", - "Products", - "]", - " GTX", - " 3090", - " ;", - " GTX", - " 3070TI", - " ;", - " GTX", - " 4070TI", - " ;", - " ?" + "[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", "USB-C", " ;", "Keyboard", " ultra", + " ;", "[", "Options", "]", "USB", "-C", " ;" }; + + var expectedMessages = new List { " What", " u", " want", " ?" }; + var expectedSuggestions = new List { "USB-C", "Keyboard ultra", "USB-C" }; // Mock the GetChatCompletionStream method to provide the expected SSE data _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) .Returns(expectedSseData.ToAsyncEnumerable()); - - // Act - var resultStream = productService.SearchProductAsync(wishlistId, message, cancellationToken); + _messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny>>(), It.IsAny())) + .ReturnsAsync(3); + + _wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken)) + .Verifiable(); + + _wishListServiceMock + .Setup(w => w.GetMessagesPageFromPersonalWishlistAsync( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList(new List + { + new MessageDto + { + Text = "Message 1", + Id = "1", + CreatedById = "User2", + Role = "User" + }, + new MessageDto + { + Text = "Message 2", + Id = "2", + CreatedById = "User2", + Role = "User" + }, + new MessageDto + { + Text = "Message 3", + Id = "3", + CreatedById = "User2", + Role = "User" + }, + }, 1, 3, 3)); + + // Act + var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken); + + // Convert the result stream to a list of ServerSentEvent var actualSseEvents = await resultStream.ToListAsync(); + + var receivedMessages = actualSseEvents + .Where(e => e.Event == SearchEventType.Message) + .Select(e => e.Data) + .ToList(); + + var receivedSuggestions = actualSseEvents + .Where(e => e.Event == SearchEventType.Suggestion) + .Select(e => e.Data) + .ToList(); // Assert Assert.NotNull(actualSseEvents); - Assert.Equal(3, actualSseEvents.Count); + Assert.Equal(expectedMessages, receivedMessages); + Assert.Equal(expectedSuggestions, receivedSuggestions); + _wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken), Times.Once); + } + + + [Fact] + public async void SearchProductAsync_WithExistingMessageInWishlistAndAddProduct_ReturnsExpectedEvents() + { + // Arrange + var wishlistId = "your_wishlist_id"; + var message = new MessageCreateDto { Text = "Your message text" }; + var cancellationToken = new CancellationToken(); + + var productService = _productService; + + var expectedSseData = new List + { + "[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", "USB-C", " ;", "Keyboard", " ultra", + " ;", "[", "Options", "]", "USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX", + " 3070TI", " ;", " GTX", " 4070TI", " ;", " ?" + }; + + var expectedMessages = new List { " What", " u", " want", " ?" }; + var expectedSuggestions = new List { "USB-C", "Keyboard ultra", "USB-C" }; + + // Mock the GetChatCompletionStream method to provide the expected SSE data + _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) + .Returns(expectedSseData.ToAsyncEnumerable()); + + _messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny>>(), It.IsAny())) + .ReturnsAsync(3); + + _wishListServiceMock + .Setup(w => w.AddProductToPersonalWishlistAsync( + It.IsAny(), It.IsAny(), It.IsAny())) + .Verifiable(); + + _wishListServiceMock.Setup(w => w.AddProductToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken)) + .Verifiable(); + + _wishListServiceMock + .Setup(w => w.GetMessagesPageFromPersonalWishlistAsync( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList(new List + { + new MessageDto + { + Text = "Message 1", + Id = "1", + CreatedById = "User2", + Role = "User" + }, + new MessageDto + { + Text = "Message 2", + Id = "2", + CreatedById = "User2", + Role = "User" + }, + new MessageDto + { + Text = "Message 3", + Id = "3", + CreatedById = "User2", + Role = "User" + }, + }, 1, 3, 3)); + + // Act + var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken); + + // Convert the result stream to a list of ServerSentEvent + var actualSseEvents = await resultStream.ToListAsync(); + + var receivedMessages = actualSseEvents + .Where(e => e.Event == SearchEventType.Message) + .Select(e => e.Data) + .ToList(); + + var receivedSuggestions = actualSseEvents + .Where(e => e.Event == SearchEventType.Suggestion) + .Select(e => e.Data) + .ToList(); + + // Assert + Assert.NotNull(actualSseEvents); + Assert.Equal(expectedMessages, receivedMessages); + Assert.Equal(expectedSuggestions, receivedSuggestions); + _wishListServiceMock.Verify(w => w.AddProductToPersonalWishlistAsync( + It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync( + wishlistId, It.IsAny(), cancellationToken), Times.Once); } } \ No newline at end of file From adf956b44c8fd571cd1a5a1581dc12dff8874218 Mon Sep 17 00:00:00 2001 From: stasex Date: Wed, 25 Oct 2023 21:36:30 +0300 Subject: [PATCH 19/21] added new test --- .../Services/ProductService.cs | 9 +++-- .../Tests/ProductsTests.cs | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index d86bae6..e2daa83 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -29,6 +29,8 @@ public class ProductService : IProductService public async IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken) { + string firstMessageForUser = "What are you looking for?"; + string promptForGpt = "You are a Shopping Assistant that helps people find product recommendations. Ask user additional questions if more context needed." + "\nYou must return data with one of the prefixes:" + @@ -56,7 +58,7 @@ public class ProductService : IProductService new OpenAiMessage() { Role = OpenAiRole.System.ToString().ToLower(), - Content = "What are you looking for?" + Content = firstMessageForUser } }, Stream = true @@ -64,13 +66,13 @@ public class ProductService : IProductService _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageCreateDto() { - Text = "What are you looking for?", + Text = firstMessageForUser, }, cancellationToken); yield return new ServerSentEvent { Event = SearchEventType.Message, - Data = "What are you looking for?" + Data = firstMessageForUser }; } @@ -177,6 +179,7 @@ public class ProductService : IProductService }; productBuffer.Name = string.Empty; + //a complete description of the entity when the Amazon API is connected await _wishlistsService.AddProductToPersonalWishlistAsync(wishlistId, new ProductCreateDto() { Url = "", diff --git a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs index bb3119b..a017f06 100644 --- a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs @@ -31,4 +31,40 @@ public class ProductsTests : TestsBase Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(responseContent); } + + [Fact] + public async Task StreamDataToClientFirstly_ReturnsExpectedResponse() + { + await LoginAsync("wishlists@gmail.com", "Yuiop12345"); + // Arrange + var wishlistId = "ab7c8c2d9edf39abcd1ef9ab"; + var message = new MessageCreateDto { Text = "I want new powerful laptop" }; + + // Act + var response = await _httpClient.PostAsJsonAsync($"http://127.0.0.1:5183/api/ProductsSearch/search/{wishlistId}", message); + var responseContent = await response.Content.ReadAsStringAsync(); + var sseEvents = responseContent.Split("\n\n", StringSplitOptions.RemoveEmptyEntries); + bool foundMessageEvent = false; + + // Assert + foreach (var sseEvent in sseEvents) + { + var sseParts = sseEvent.Split('\n'); + if (sseParts.Length >= 2) + { + var eventName = sseParts[0]; + var eventData = sseParts[1].Substring("data: ".Length); + if (eventName == "event: Message") + { + foundMessageEvent = true; + Assert.Equal("\"What are you looking for?\"", eventData); + break; + } + } + } + + Assert.True(foundMessageEvent, "Message event not found"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(responseContent); + } } \ No newline at end of file From 51aca2e2707261c3c5afaab875b960cddb7a7450 Mon Sep 17 00:00:00 2001 From: stasex Date: Thu, 26 Oct 2023 12:57:52 +0300 Subject: [PATCH 20/21] made a fix for the pr --- .../Mutations/ProductMutation.cs | 12 ---- .../Queries/ProductQuery.cs | 11 ---- .../Services/ProductService.cs | 65 +++++++------------ .../TestExtentions/DbInitializer.cs | 8 +++ .../ProductTests.cs | 27 +++++++- 5 files changed, 56 insertions(+), 67 deletions(-) delete mode 100644 ShoppingAssistantApi.Api/Mutations/ProductMutation.cs delete mode 100644 ShoppingAssistantApi.Api/Queries/ProductQuery.cs diff --git a/ShoppingAssistantApi.Api/Mutations/ProductMutation.cs b/ShoppingAssistantApi.Api/Mutations/ProductMutation.cs deleted file mode 100644 index f4598a5..0000000 --- a/ShoppingAssistantApi.Api/Mutations/ProductMutation.cs +++ /dev/null @@ -1,12 +0,0 @@ -using ShoppingAssistantApi.Application.IServices; -using ShoppingAssistantApi.Application.Models.Dtos; -using ShoppingAssistantApi.Application.Models.ProductSearch; -using ShoppingAssistantApi.Domain.Entities; -using ShoppingAssistantApi.Infrastructure.Services; - -namespace ShoppingAssistantApi.Api.Mutations; - -[ExtendObjectType(OperationTypeNames.Mutation)] -public class ProductMutation -{ -} \ No newline at end of file diff --git a/ShoppingAssistantApi.Api/Queries/ProductQuery.cs b/ShoppingAssistantApi.Api/Queries/ProductQuery.cs deleted file mode 100644 index b76586b..0000000 --- a/ShoppingAssistantApi.Api/Queries/ProductQuery.cs +++ /dev/null @@ -1,11 +0,0 @@ -using HotChocolate.Authorization; -using ShoppingAssistantApi.Application.IServices; -using ShoppingAssistantApi.Domain.Entities; - -namespace ShoppingAssistantApi.Api.Queries; - -[ExtendObjectType(OperationTypeNames.Query)] -public class ProductQuery -{ - -} \ No newline at end of file diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index e2daa83..32fb2fc 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -29,8 +29,6 @@ public class ProductService : IProductService public async IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken) { - string firstMessageForUser = "What are you looking for?"; - string promptForGpt = "You are a Shopping Assistant that helps people find product recommendations. Ask user additional questions if more context needed." + "\nYou must return data with one of the prefixes:" + @@ -39,56 +37,43 @@ public class ProductService : IProductService "\n[Message] - return text" + "\n[Products] - return semicolon separated product names"; - var isFirstMessage = await _messagesRepository.GetCountAsync(message=>message.WishlistId==ObjectId.Parse((wishlistId)), cancellationToken); + var countOfMessage = await _messagesRepository + .GetCountAsync(message=>message.WishlistId==ObjectId.Parse((wishlistId)), cancellationToken); - var chatRequest = new ChatCompletionRequest(); - - if (isFirstMessage==0) + var previousMessages = await _wishlistsService + .GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, countOfMessage, cancellationToken); + + var chatRequest = new ChatCompletionRequest { - chatRequest = new ChatCompletionRequest + Messages = new List { - Messages = new List + new OpenAiMessage { - new OpenAiMessage - { - Role = OpenAiRole.System.ToString().ToLower(), - Content = promptForGpt - }, - - new OpenAiMessage() - { - Role = OpenAiRole.System.ToString().ToLower(), - Content = firstMessageForUser - } - }, - Stream = true - }; - - _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageCreateDto() - { - Text = firstMessageForUser, - }, cancellationToken); - + Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.System), + Content = promptForGpt + } + } + }; + + if (countOfMessage==1) + { yield return new ServerSentEvent { Event = SearchEventType.Message, - Data = firstMessageForUser + Data = previousMessages.Items.FirstOrDefault()?.Text }; - } - if(isFirstMessage!=0) + if(countOfMessage>1) { - var previousMessages = _wishlistsService - .GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 50, cancellationToken).Result.Items.ToList(); - var messagesForOpenAI = new List(); messagesForOpenAI.Add(new OpenAiMessage() { - Role = OpenAiRole.System.ToString().ToLower(), + Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.System), Content = promptForGpt }); - foreach (var item in previousMessages ) + + foreach (var item in previousMessages.Items) { messagesForOpenAI.Add( new OpenAiMessage() @@ -100,15 +85,11 @@ public class ProductService : IProductService messagesForOpenAI.Add(new OpenAiMessage() { - Role = MessageRoles.User.ToString().ToLower(), + Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.User), Content = message.Text }); - chatRequest = new ChatCompletionRequest - { - Messages = messagesForOpenAI, - Stream = true - }; + chatRequest.Messages.AddRange(messagesForOpenAI); var suggestionBuffer = new Suggestion(); var messageBuffer = new MessagePart(); diff --git a/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs index a89df14..f66a9b1 100644 --- a/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs +++ b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs @@ -239,6 +239,14 @@ public class DbInitializer CreatedById = user2.Id, CreatedDateUtc = DateTime.UtcNow }, + new Message + { + Text = "What are you looking for?", + Role = "system", + WishlistId = wishlistId3, + CreatedById = user2.Id, + CreatedDateUtc = DateTime.UtcNow + }, }; await messagesCollection.InsertManyAsync(messages); diff --git a/ShoppingAssistantApi.UnitTests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs index f480031..8a6fb99 100644 --- a/ShoppingAssistantApi.UnitTests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -56,11 +56,35 @@ public class ProductTests .Returns(expectedSseData.ToAsyncEnumerable()); _messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny>>(), It.IsAny())) - .ReturnsAsync(0); + .ReturnsAsync(1); _wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken)) .Verifiable(); + _wishListServiceMock + .Setup(m => m.GetMessagesPageFromPersonalWishlistAsync( + It.IsAny(), // Очікуваний параметр wishlistId + It.IsAny(), // Очікуваний параметр pageNumber + It.IsAny(), // Очікуваний параметр pageSize + It.IsAny()) // Очікуваний параметр cancellationToken + ) + .ReturnsAsync(new PagedList( + new List + { + new MessageDto + { + Text = "What are you looking for?", + Id = "3", + CreatedById = "User2", + Role = "User" + }, + + }, + 1, + 1, + 1 + )); + // Act var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken); @@ -81,7 +105,6 @@ public class ProductTests // Check if the actual SSE events match the expected SSE events Assert.NotNull(actualSseEvents); Assert.Equal(expectedMessages, receivedMessages); - _wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken), Times.Once); } From 98377dbf9dd0a391da3c8069d2131c64f066048f Mon Sep 17 00:00:00 2001 From: stasex Date: Fri, 27 Oct 2023 01:16:02 +0300 Subject: [PATCH 21/21] changed the logic in the service and added some changes to the tests --- .../Services/ProductService.cs | 191 ++++++++---------- .../TestExtentions/DbInitializer.cs | 19 +- .../Tests/ProductsTests.cs | 2 +- .../ProductTests.cs | 13 +- 4 files changed, 98 insertions(+), 127 deletions(-) diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index 32fb2fc..8c4d567 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -55,127 +55,110 @@ public class ProductService : IProductService } }; - if (countOfMessage==1) + + var messagesForOpenAI = new List(); + + foreach (var item in previousMessages.Items) { - yield return new ServerSentEvent - { - Event = SearchEventType.Message, - Data = previousMessages.Items.FirstOrDefault()?.Text - }; - } - - if(countOfMessage>1) - { - var messagesForOpenAI = new List(); - messagesForOpenAI.Add(new OpenAiMessage() - { - Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.System), - Content = promptForGpt - }); - - foreach (var item in previousMessages.Items) - { - messagesForOpenAI.Add( - new OpenAiMessage() + messagesForOpenAI + .Add(new OpenAiMessage() { Role = item.Role.ToLower(), - Content = item.Text + Content = item.Text }); + } + + messagesForOpenAI.Add(new OpenAiMessage() + { + Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.User), + Content = message.Text + }); + + chatRequest.Messages.AddRange(messagesForOpenAI); + + var suggestionBuffer = new Suggestion(); + var messageBuffer = new MessagePart(); + var productBuffer = new ProductName(); + var currentDataType = SearchEventType.Wishlist; + var dataTypeHolder = string.Empty; + + await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) + { + if (data.Contains("[")) + { + if (dataTypeHolder=="[Message]" && messageBuffer.Text!=null) + { + _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageCreateDto() + { + Text = messageBuffer.Text, + }, cancellationToken); + } + dataTypeHolder = string.Empty; + dataTypeHolder += data; } - - messagesForOpenAI.Add(new OpenAiMessage() - { - Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.User), - Content = message.Text - }); - - chatRequest.Messages.AddRange(messagesForOpenAI); - - var suggestionBuffer = new Suggestion(); - var messageBuffer = new MessagePart(); - var productBuffer = new ProductName(); - var currentDataType = SearchEventType.Wishlist; - var dataTypeHolder = string.Empty; - await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) + else if (data.Contains("]")) { - if (data.Contains("[")) + dataTypeHolder += data; + currentDataType = DetermineDataType(dataTypeHolder); + } + + else if (dataTypeHolder=="[" && !data.Contains("[")) + { + dataTypeHolder += data; + } + + else + { + switch (currentDataType) { - if (dataTypeHolder=="[Message]" && messageBuffer.Text!=null) - { - _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageCreateDto() + case SearchEventType.Message: + yield return new ServerSentEvent { - Text = messageBuffer.Text, - }, cancellationToken); - } - dataTypeHolder = string.Empty; - dataTypeHolder += data; - } + Event = SearchEventType.Message, + Data = data + }; + messageBuffer.Text += data; + break; - else if (data.Contains("]")) - { - dataTypeHolder += data; - currentDataType = DetermineDataType(dataTypeHolder); - } - - else if (dataTypeHolder=="[" && !data.Contains("[")) - { - dataTypeHolder += data; - } - - else - { - switch (currentDataType) - { - case SearchEventType.Message: + case SearchEventType.Suggestion: + if (data.Contains(";")) + { yield return new ServerSentEvent { - Event = SearchEventType.Message, - Data = data + Event = SearchEventType.Suggestion, + Data = suggestionBuffer.Text }; - messageBuffer.Text += data; - break; - - case SearchEventType.Suggestion: - if (data.Contains(";")) - { - yield return new ServerSentEvent - { - Event = SearchEventType.Suggestion, - Data = suggestionBuffer.Text - }; - suggestionBuffer.Text = string.Empty; - break; - } - suggestionBuffer.Text += data; + suggestionBuffer.Text = string.Empty; break; + } + suggestionBuffer.Text += data; + break; - case SearchEventType.Product: - if (data.Contains(";")) + case SearchEventType.Product: + if (data.Contains(";")) + { + yield return new ServerSentEvent { - yield return new ServerSentEvent - { - Event = SearchEventType.Product, - Data = productBuffer.Name - }; - productBuffer.Name = string.Empty; + Event = SearchEventType.Product, + Data = productBuffer.Name + }; + productBuffer.Name = string.Empty; - //a complete description of the entity when the Amazon API is connected - await _wishlistsService.AddProductToPersonalWishlistAsync(wishlistId, new ProductCreateDto() - { - Url = "", - Name = productBuffer.Name, - Rating = 0, - Description = "", - ImagesUrls = new []{"", ""}, - WasOpened = false - }, cancellationToken); - - break; - } - productBuffer.Name += data; - break; - } + //a complete description of the entity when the Amazon API is connected + await _wishlistsService.AddProductToPersonalWishlistAsync(wishlistId, new ProductCreateDto() + { + Url = "", + Name = productBuffer.Name, + Rating = 0, + Description = "", + ImagesUrls = new []{"", ""}, + WasOpened = false + }, cancellationToken); + break; + } + productBuffer.Name += data; + break; } } } diff --git a/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs index f66a9b1..02929da 100644 --- a/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs +++ b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs @@ -220,13 +220,8 @@ public class DbInitializer }, new Message { - Text = "You are a Shopping Assistant that helps people find product recommendations. Ask user additional questions if more context needed." + - "\nYou must return data with one of the prefixes:" + - "\n[Question] - return question" + - "\n[Suggestions] - return semicolon separated suggestion how to answer to a question" + - "\n[Message] - return text" + - "\n[Products] - return semicolon separated product names", - Role = "system", + Text = "What are you looking for?", + Role = "assistant", WishlistId = wishlistId4, CreatedById = user2.Id, CreatedDateUtc = DateTime.UtcNow @@ -234,15 +229,7 @@ public class DbInitializer new Message { Text = "What are you looking for?", - Role = "system", - WishlistId = wishlistId4, - CreatedById = user2.Id, - CreatedDateUtc = DateTime.UtcNow - }, - new Message - { - Text = "What are you looking for?", - Role = "system", + Role = "assistant", WishlistId = wishlistId3, CreatedById = user2.Id, CreatedDateUtc = DateTime.UtcNow diff --git a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs index a017f06..f9756ec 100644 --- a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs @@ -57,7 +57,7 @@ public class ProductsTests : TestsBase if (eventName == "event: Message") { foundMessageEvent = true; - Assert.Equal("\"What are you looking for?\"", eventData); + Assert.NotNull(eventData); break; } } diff --git a/ShoppingAssistantApi.UnitTests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs index 8a6fb99..9887d90 100644 --- a/ShoppingAssistantApi.UnitTests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -49,7 +49,8 @@ public class ProductTests " 3070TI", " ;", " GTX", " 4070TI", " ;", " ?" }; - var expectedMessages = new List { "What are you looking for?" }; + var expectedMessages = new List { " What", " u", " want", " ?" }; + var expectedSuggestion = new List { " USB-C", " Keyboard ultra", " USB-C" }; // Mock the GetChatCompletionStream method to provide the expected SSE data _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) @@ -63,10 +64,10 @@ public class ProductTests _wishListServiceMock .Setup(m => m.GetMessagesPageFromPersonalWishlistAsync( - It.IsAny(), // Очікуваний параметр wishlistId - It.IsAny(), // Очікуваний параметр pageNumber - It.IsAny(), // Очікуваний параметр pageSize - It.IsAny()) // Очікуваний параметр cancellationToken + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()) ) .ReturnsAsync(new PagedList( new List @@ -78,7 +79,6 @@ public class ProductTests CreatedById = "User2", Role = "User" }, - }, 1, 1, @@ -105,6 +105,7 @@ public class ProductTests // Check if the actual SSE events match the expected SSE events Assert.NotNull(actualSseEvents); Assert.Equal(expectedMessages, receivedMessages); + Assert.Equal(expectedSuggestion, receivedSuggestions); }