diff --git a/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs b/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs index 7dd9949..f00b26a 100644 --- a/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs +++ b/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.Models.CreateDtos; +using ShoppingAssistantApi.Application.Models.Dtos; +using ShoppingAssistantApi.Domain.Enums; namespace ShoppingAssistantApi.Api.Controllers; @@ -11,14 +13,24 @@ public class ProductsSearchController : BaseController { private readonly IProductService _productService; - public ProductsSearchController(IProductService productService) + private readonly IWishlistsService _wishlistsService; + + public ProductsSearchController(IProductService productService, IWishlistsService wishlistsService) { _productService = productService; + _wishlistsService = wishlistsService; } [HttpPost("search/{wishlistId}")] public async Task StreamDataToClient(string wishlistId, [FromBody]MessageCreateDto message, CancellationToken cancellationToken) { + var dto = new MessageDto() + { + Text = message.Text, + Role = MessageRoles.User.ToString(), + }; + await _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, dto, cancellationToken); + Response.Headers.Add("Content-Type", "text/event-stream"); Response.Headers.Add("Cache-Control", "no-cache"); Response.Headers.Add("Connection", "keep-alive"); diff --git a/ShoppingAssistantApi.Api/Mutations/WishlistsMutation.cs b/ShoppingAssistantApi.Api/Mutations/WishlistsMutation.cs index d33cf81..a8f1d8f 100644 --- a/ShoppingAssistantApi.Api/Mutations/WishlistsMutation.cs +++ b/ShoppingAssistantApi.Api/Mutations/WishlistsMutation.cs @@ -15,7 +15,7 @@ public class WishlistsMutation [Service] IWishlistsService wishlistsService) => wishlistsService.GenerateNameForPersonalWishlistAsync(wishlistId, cancellationToken); - public Task AddMessageToPersonalWishlistAsync(string wishlistId, MessageCreateDto dto, CancellationToken cancellationToken, + public Task AddMessageToPersonalWishlistAsync(string wishlistId, MessageDto dto, CancellationToken cancellationToken, [Service] IWishlistsService wishlistsService) => wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, dto, cancellationToken); diff --git a/ShoppingAssistantApi.Application/IServices/IWishlistService.cs b/ShoppingAssistantApi.Application/IServices/IWishlistService.cs index 4b26cf1..c2d5899 100644 --- a/ShoppingAssistantApi.Application/IServices/IWishlistService.cs +++ b/ShoppingAssistantApi.Application/IServices/IWishlistService.cs @@ -10,7 +10,7 @@ public interface IWishlistsService Task GenerateNameForPersonalWishlistAsync(string wishlistId, CancellationToken cancellationToken); - Task AddMessageToPersonalWishlistAsync(string wishlistId, MessageCreateDto dto, CancellationToken cancellationToken); + Task AddMessageToPersonalWishlistAsync(string wishlistId, MessageDto dto, CancellationToken cancellationToken); Task> GetPersonalWishlistsPageAsync(int pageNumber, int pageSize, CancellationToken cancellationToken); diff --git a/ShoppingAssistantApi.Application/Models/CreateDtos/ProductCreateDto.cs b/ShoppingAssistantApi.Application/Models/CreateDtos/ProductCreateDto.cs index 015706c..01d900c 100644 --- a/ShoppingAssistantApi.Application/Models/CreateDtos/ProductCreateDto.cs +++ b/ShoppingAssistantApi.Application/Models/CreateDtos/ProductCreateDto.cs @@ -9,6 +9,8 @@ public class ProductCreateDto public required string Description { get; set; } public required double Rating { get; set; } + + public required double Price { get; set; } public required string[] ImagesUrls { get; set; } diff --git a/ShoppingAssistantApi.Application/Models/Dtos/MessageDto.cs b/ShoppingAssistantApi.Application/Models/Dtos/MessageDto.cs index 9225d00..7c2a7cd 100644 --- a/ShoppingAssistantApi.Application/Models/Dtos/MessageDto.cs +++ b/ShoppingAssistantApi.Application/Models/Dtos/MessageDto.cs @@ -2,11 +2,11 @@ namespace ShoppingAssistantApi.Application.Models.Dtos; public class MessageDto { - public required string Id { get; set; } + public string Id { get; set; } - public required string Text { get; set; } + public string Text { get; set; } - public required string Role { get; set; } + public string Role { get; set; } - public required string CreatedById { get; set; } + public string CreatedById { get; set; } } diff --git a/ShoppingAssistantApi.Application/Models/Dtos/ProductDto.cs b/ShoppingAssistantApi.Application/Models/Dtos/ProductDto.cs index 1697cd6..81cc6f0 100644 --- a/ShoppingAssistantApi.Application/Models/Dtos/ProductDto.cs +++ b/ShoppingAssistantApi.Application/Models/Dtos/ProductDto.cs @@ -12,6 +12,8 @@ public class ProductDto public required double Rating { get; set; } + public required double Price { get; set; } + public required string[] ImagesUrls { get; set; } public required bool WasOpened { get; set; } diff --git a/ShoppingAssistantApi.Application/Models/OpenAi/ChatCompletionRequest.cs b/ShoppingAssistantApi.Application/Models/OpenAi/ChatCompletionRequest.cs index 40f3972..6606d16 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-4"; + public string Model { get; set; } = "gpt-4-1106-preview"; public List Messages { get; set; } diff --git a/ShoppingAssistantApi.Domain/Entities/Product.cs b/ShoppingAssistantApi.Domain/Entities/Product.cs index 2085293..91e1cb1 100644 --- a/ShoppingAssistantApi.Domain/Entities/Product.cs +++ b/ShoppingAssistantApi.Domain/Entities/Product.cs @@ -17,6 +17,8 @@ public class Product : EntityBase public string[] ImagesUrls { get; set; } public bool WasOpened { get; set; } + + public double Price { get; set; } public ObjectId WishlistId { get; set; } } diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index 82d6476..c6ca80d 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -19,6 +19,10 @@ public class ProductService : IProductService private readonly IOpenAiService _openAiService; private readonly IMessagesRepository _messagesRepository; + + private bool mqchecker = false; + + private SearchEventType currentDataType = SearchEventType.Wishlist; public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService, IMessagesRepository messagesRepository) { @@ -60,12 +64,24 @@ public class ProductService : IProductService foreach (var item in previousMessages.Items) { - messagesForOpenAI - .Add(new OpenAiMessage() - { - Role = item.Role.ToLower(), - Content = item.Text - }); + if (item.Role == "Application") + { + messagesForOpenAI + .Add(new OpenAiMessage() + { + Role = OpenAiRole.Assistant.RequestConvert(), + Content = item.Text + }); + } + else + { + messagesForOpenAI + .Add(new OpenAiMessage() + { + Role = item.Role.ToLower(), + Content = item.Text + }); + } } messagesForOpenAI.Add(new OpenAiMessage() @@ -79,20 +95,27 @@ 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 counter = 0; await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) { - if (data.Contains("[")) + counter++; + if (mqchecker && currentDataType == SearchEventType.Message && messageBuffer != null) { - if (dataTypeHolder=="[Message]" && messageBuffer.Text!=null) + if (data == "[") { - _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageCreateDto() + _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageDto() { Text = messageBuffer.Text, + Role = MessageRoles.Application.ToString(), }, cancellationToken); + mqchecker = false; } + } + + if (data.Contains("[")) + { dataTypeHolder = string.Empty; dataTypeHolder += data; } @@ -101,13 +124,17 @@ public class ProductService : IProductService { dataTypeHolder += data; currentDataType = DetermineDataType(dataTypeHolder); + if (currentDataType == SearchEventType.Message) + { + mqchecker = true; + } } else if (dataTypeHolder=="[" && !data.Contains("[")) { dataTypeHolder += data; } - + else { switch (currentDataType) @@ -118,6 +145,7 @@ public class ProductService : IProductService Event = SearchEventType.Message, Data = data }; + currentDataType = SearchEventType.Message; messageBuffer.Text += data; break; @@ -152,6 +180,7 @@ public class ProductService : IProductService Name = productBuffer.Name, Rating = 0, Description = "", + Price = 0, ImagesUrls = new []{"", ""}, WasOpened = false }, cancellationToken); @@ -162,6 +191,15 @@ public class ProductService : IProductService } } } + if (currentDataType == SearchEventType.Message) + { + _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageDto() + { + Text = messageBuffer.Text, + Role = MessageRoles.Application.ToString(), + }, cancellationToken); + mqchecker = false; + } } private SearchEventType DetermineDataType(string dataTypeHolder) diff --git a/ShoppingAssistantApi.Infrastructure/Services/WishlistsService.cs b/ShoppingAssistantApi.Infrastructure/Services/WishlistsService.cs index 837d991..ddc2ec5 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/WishlistsService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/WishlistsService.cs @@ -45,7 +45,7 @@ public class WishlistsService : IWishlistsService throw new InvalidDataException("Provided type is invalid."); } - newWishlist.CreatedById = (ObjectId) GlobalUser.Id; + newWishlist.CreatedById = GlobalUser.Id.Value; newWishlist.CreatedDateUtc = DateTime.UtcNow; newWishlist.Name = $"{newWishlist.Type} Search"; @@ -54,8 +54,8 @@ public class WishlistsService : IWishlistsService var newMessage = new Message { Text = dto.FirstMessageText, - Role = MessageRoles.User.ToString(), - CreatedById = (ObjectId) GlobalUser.Id, + Role = MessageRoles.Application.ToString(), + CreatedById = GlobalUser.Id.Value, CreatedDateUtc = DateTime.UtcNow, WishlistId = createdWishlist.Id }; @@ -96,12 +96,12 @@ public class WishlistsService : IWishlistsService var openAiMessage = await _openAiService.GetChatCompletion(chatCompletionRequest, cancellationToken); wishlist = await _wishlistsRepository.UpdateWishlistNameAsync(wishlist.Id, - openAiMessage.Content, (ObjectId) GlobalUser.Id, cancellationToken); + openAiMessage.Content, GlobalUser.Id.Value, cancellationToken); return _mapper.Map(wishlist); } - public async Task AddMessageToPersonalWishlistAsync(string wishlistId, MessageCreateDto dto, CancellationToken cancellationToken) + public async Task AddMessageToPersonalWishlistAsync(string wishlistId, MessageDto dto, CancellationToken cancellationToken) { var newMessage = _mapper.Map(dto); @@ -110,8 +110,7 @@ public class WishlistsService : IWishlistsService throw new InvalidDataException("Provided id is invalid."); } - newMessage.Role = MessageRoles.User.ToString(); - newMessage.CreatedById = (ObjectId) GlobalUser.Id; + newMessage.CreatedById = GlobalUser.Id.Value; newMessage.CreatedDateUtc = DateTime.UtcNow; newMessage.WishlistId = wishlistObjectId; @@ -169,7 +168,7 @@ public class WishlistsService : IWishlistsService await TryGetPersonalWishlist(wishlistObjectId, cancellationToken); - newProduct.CreatedById = (ObjectId) GlobalUser.Id; + newProduct.CreatedById = GlobalUser.Id.Value; newProduct.CreatedDateUtc = DateTime.UtcNow; newProduct.WishlistId = wishlistObjectId; diff --git a/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs b/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs index 98425a6..37ab4e5 100644 --- a/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs +++ b/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs @@ -212,6 +212,7 @@ public class DbInitialaizer Name = "Thermaltake Glacier 360 Liquid-Cooled PC", Description = "Cool PC for any task!", Rating = 4.3, + Price = 855, Url = "https://www.amazon.com/Thermaltake-Liquid-Cooled-ToughRAM-Computer-S3WT-B550-G36-LCS/dp" + "/B09FYNM2GW/ref=sr_1_1?crid=391KAS4JFJSFF&keywords=gaming%2Bpc&qid=1697132083&sprefix=gaming%2Bpc%2Caps%2C209&sr=8-1&th=1", ImagesUrls = new string[] @@ -229,6 +230,7 @@ public class DbInitialaizer Name = "Apple MagSafe Battery Pack", Description = "Portable Charger with Fast Charging Capability, Power Bank Compatible with iPhone", Rating = 4.3, + Price = 35.99, Url = "https://www.amazon.com/Apple-MJWY3AM-A-MagSafe-Battery/dp/" + "B099BWY7WT/ref=sr_1_1?keywords=apple+power+bank&qid=1697375350&sr=8-1", ImagesUrls = new string[] @@ -247,6 +249,7 @@ public class DbInitialaizer Name = "Logitech K400 Plus Wireless Touch With Easy Media Control and Built-in Touchpad", Description = "Reliable membrane keyboard with touchpad!", Rating = 4.5, + Price = 99, Url = "https://www.amazon.com/Logitech-Wireless-Keyboard-Touchpad-PC-connected/dp/B014EUQOGK/" + "ref=sr_1_11?crid=BU2PHZKHKD65&keywords=keyboard+wireless&qid=1697375559&sprefix=keyboard+wir%2Caps%2C195&sr=8-11", ImagesUrls = new string[] @@ -266,6 +269,7 @@ public class DbInitialaizer Description = "Cross computer control: Game changing capacity to navigate seamlessly on three computers," + " and copy paste text, images, and files from one to the other using Logitech Flow", Rating = 4.6, + Price = 50.99, Url = "https://www.amazon.com/Logitech-Hyper-Fast-Scrolling-Rechargeable-Computers/dp/B08P2JFPQC/ref=sr_1_8?" + "crid=2BL6Z14W2TPP3&keywords=mouse%2Bwireless&qid=1697375784&sprefix=mousewireless%2Caps%2C197&sr=8-8&th=1", ImagesUrls = new string[] diff --git a/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs index 02929da..f4346d7 100644 --- a/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs +++ b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs @@ -257,6 +257,7 @@ public class DbInitializer Name = "AMD Ryzen 5 5600G 6-Core 12-Thread Unlocked Desktop Processor with Radeon Graphics", Description = "Features best-in-class graphics performance in a desktop processor for smooth 1080p gaming, no graphics card required", Rating = 4.8, + Price = 120, Url = "https://a.co/d/5ceuIrq", ImagesUrls = new string[] { @@ -273,6 +274,7 @@ public class DbInitializer Name = "Samsung 970 EVO Plus SSD 2TB NVMe M.2 Internal Solid State Hard Drive, V-NAND Technology, Storage and Memory Expansion for Gaming, Graphics w/ Heat Control, Max Speed, MZ-V7S2T0B/AM ", Description = "7 Year Limited Warranty: The 970 EVO Plus provides up to 1200 TBW (Terabytes Written) with 5-years of protection for exceptional endurance powered by the latest V-NAND technology and Samsung's reputation for quality ", Rating = 4.8, + Price = 153, Url = "https://a.co/d/gxnuqs1", ImagesUrls = new string[] { diff --git a/ShoppingAssistantApi.Tests/Tests/WishlistsTests.cs b/ShoppingAssistantApi.Tests/Tests/WishlistsTests.cs index 6a3c3a0..4d1e5d8 100644 --- a/ShoppingAssistantApi.Tests/Tests/WishlistsTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/WishlistsTests.cs @@ -3,6 +3,7 @@ using ShoppingAssistantApi.Domain.Enums; using ShoppingAssistantApi.Application.Models.Dtos; using ShoppingAssistantApi.Application.Paging; using Newtonsoft.Json.Linq; +using MongoDB.Bson; namespace ShoppingAssistantApi.Tests.Tests; @@ -88,6 +89,30 @@ public class WishlistsTests : TestsBase Assert.NotNull(startWishlistResponse); + const string MessageText = "I want laptop"; + var mutation = new + { + query = @" + mutation addMessageToPersonalWishlist($wishlistId: String!, $dto: MessageDtoInput!) { + addMessageToPersonalWishlist(wishlistId: $wishlistId, dto: $dto) { + role, text, createdById + } + }", + variables = new + { + wishlistId = startWishlistResponse.Id, + dto = new + { + id = ObjectId.Empty, + text = MessageText, + role = MessageRoles.User.ToString(), + createdById = ObjectId.Empty, + } + } + }; + + await SendGraphQlRequestAsync(mutation); + var generateWishlistNameMutation = new { query = @" @@ -174,7 +199,7 @@ public class WishlistsTests : TestsBase var mutation = new { query = @" - mutation addMessageToPersonalWishlist($wishlistId: String!, $dto: MessageCreateDtoInput!) { + mutation addMessageToPersonalWishlist($wishlistId: String!, $dto: MessageDtoInput!) { addMessageToPersonalWishlist(wishlistId: $wishlistId, dto: $dto) { role, text, createdById } @@ -184,7 +209,10 @@ public class WishlistsTests : TestsBase wishlistId = TestingValidWishlistId, dto = new { - text = MessageText + id = ObjectId.Empty, + text = MessageText, + role = MessageRoles.User.ToString(), + createdById = ObjectId.Empty, } } }; @@ -242,7 +270,7 @@ public class WishlistsTests : TestsBase query = @" mutation addProductToPersonalWishlist($wishlistId: String!, $dto: ProductCreateDtoInput!) { addProductToPersonalWishlist (wishlistId: $wishlistId, dto: $dto) { - url, name, description, rating, imagesUrls, wasOpened + url, name, price, description, rating, imagesUrls, wasOpened } }", variables = new @@ -252,6 +280,7 @@ public class WishlistsTests : TestsBase { url = "https://www.amazon.com/url", name = "Generic name", + price = 1, description = "Generic description", rating = 4.8, imagesUrls = new string[] @@ -272,6 +301,7 @@ public class WishlistsTests : TestsBase Assert.Equal("Generic name", product.Name); Assert.Equal("Generic description", product.Description); Assert.Equal(4.8, product.Rating); + Assert.Equal(1, product.Price); Assert.Equal("https://www.amazon.com/image-url-1", product.ImagesUrls[0]); } diff --git a/ShoppingAssistantApi.UnitTests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs index 9887d90..e5de43e 100644 --- a/ShoppingAssistantApi.UnitTests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -46,10 +46,10 @@ public class ProductTests { "[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", " USB-C", " ;", " Keyboard", " ultra", " ;", "[", "Options", "]", " USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX", - " 3070TI", " ;", " GTX", " 4070TI", " ;", " ?" + " 3070TI", " ;", " GTX", " 4070TI", " ;", " ?", "[", "Message", "]", " What", " u", " want", " ?" }; - var expectedMessages = new List { " What", " u", " want", " ?" }; + var expectedMessages = new List { " What", " u", " want", " ?", " What", " u", " want", " ?" }; var expectedSuggestion = new List { " USB-C", " Keyboard ultra", " USB-C" }; // Mock the GetChatCompletionStream method to provide the expected SSE data @@ -59,7 +59,7 @@ public class ProductTests _messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny>>(), It.IsAny())) .ReturnsAsync(1); - _wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken)) + _wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken)) .Verifiable(); _wishListServiceMock @@ -135,7 +135,7 @@ public class ProductTests _messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny>>(), It.IsAny())) .ReturnsAsync(3); - _wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken)) + _wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken)) .Verifiable(); _wishListServiceMock @@ -186,7 +186,7 @@ public class ProductTests Assert.NotNull(actualSseEvents); Assert.Equal(expectedMessages, receivedMessages); Assert.Equal(expectedSuggestions, receivedSuggestions); - _wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken), Times.Once); + _wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken), Times.Once); } @@ -276,6 +276,6 @@ public class ProductTests _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); + wishlistId, It.IsAny(), cancellationToken), Times.Once); } } \ No newline at end of file