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 @@ - - - + + +