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() {