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