added new chips to the product search service and implemented unit tests

This commit is contained in:
stasex 2023-10-24 20:03:54 +03:00
parent dc4826dacc
commit 3372a0910b
7 changed files with 273 additions and 30 deletions

View File

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Mvc;
namespace ShoppingAssistantApi.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class BaseController : ControllerBase
{
}

View File

@ -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();
}
}
}

View File

@ -21,8 +21,5 @@
<ProjectReference Include="..\ShoppingAssistantApi.Persistance\ShoppingAssistantApi.Persistance.csproj" /> <ProjectReference Include="..\ShoppingAssistantApi.Persistance\ShoppingAssistantApi.Persistance.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
</ItemGroup>
</Project> </Project>

View File

@ -18,6 +18,7 @@ public static class ServicesExtention
services.AddScoped<IUsersService, UsersService>(); services.AddScoped<IUsersService, UsersService>();
services.AddScoped<IWishlistsService, WishlistsService>(); services.AddScoped<IWishlistsService, WishlistsService>();
services.AddScoped<IOpenAiService, OpenAiService>(); services.AddScoped<IOpenAiService, OpenAiService>();
services.AddScoped<IProductService, ProductService>();
return services; return services;
} }

View File

@ -15,6 +15,7 @@ public class ProductService : IProductService
private readonly IOpenAiService _openAiService; private readonly IOpenAiService _openAiService;
public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService) public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService)
{ {
_openAiService = openAiService; _openAiService = openAiService;
@ -23,24 +24,91 @@ public class ProductService : IProductService
public async IAsyncEnumerable<ServerSentEvent> SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken) public async IAsyncEnumerable<ServerSentEvent> SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken)
{ {
var chatRequest = new ChatCompletionRequest bool checker = false;
var isFirstMessage = _wishlistsService
.GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 1, cancellationToken).Result;
var chatRequest = new ChatCompletionRequest();
if (isFirstMessage==null)
{
chatRequest = new ChatCompletionRequest
{ {
Messages = new List<OpenAiMessage> Messages = new List<OpenAiMessage>
{ {
new OpenAiMessage new OpenAiMessage
{ {
Role = "User", Role = OpenAiRole.System.ToString(),
Content = "" 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 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<OpenAiMessage>();
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 suggestionBuffer = new Suggestion();
var messageBuffer = new MessagePart(); var messageBuffer = new MessagePart();
var productBuffer = new ProductName();
var currentDataType = SearchEventType.Wishlist; var currentDataType = SearchEventType.Wishlist;
var dataTypeHolder = string.Empty; var dataTypeHolder = string.Empty;
var dataBuffer = string.Empty;
await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken))
{ {
@ -53,6 +121,18 @@ public class ProductService : IProductService
Text = messageBuffer.Text, Text = messageBuffer.Text,
}, cancellationToken); }, 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 = string.Empty;
dataTypeHolder += data; dataTypeHolder += data;
} }
@ -70,8 +150,6 @@ public class ProductService : IProductService
else else
{ {
dataBuffer += data;
switch (currentDataType) switch (currentDataType)
{ {
case SearchEventType.Message: case SearchEventType.Message:
@ -96,17 +174,23 @@ public class ProductService : IProductService
} }
break; break;
case SearchEventType.Product: case SearchEventType.Product:
productBuffer.Name += data;
if (data.Contains(";"))
{
yield return new ServerSentEvent yield return new ServerSentEvent
{ {
Event = SearchEventType.Product, Event = SearchEventType.Product,
Data = data Data = productBuffer.Name
}; };
productBuffer.Name = string.Empty;
}
break; break;
}
}
}
} }
} }
}
}
private SearchEventType DetermineDataType(string dataTypeHolder) private SearchEventType DetermineDataType(string dataTypeHolder)
{ {

View File

@ -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<Program> 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);
// Додайте додаткові перевірки на відповідь, якщо необхідно
}
}

View File

@ -8,6 +8,7 @@ using ShoppingAssistantApi.Application.Models.CreateDtos;
using ShoppingAssistantApi.Application.Models.Dtos; using ShoppingAssistantApi.Application.Models.Dtos;
using ShoppingAssistantApi.Application.Models.OpenAi; using ShoppingAssistantApi.Application.Models.OpenAi;
using ShoppingAssistantApi.Application.Models.ProductSearch; using ShoppingAssistantApi.Application.Models.ProductSearch;
using ShoppingAssistantApi.Application.Paging;
using ShoppingAssistantApi.Domain.Entities; using ShoppingAssistantApi.Domain.Entities;
using ShoppingAssistantApi.Domain.Enums; using ShoppingAssistantApi.Domain.Enums;
using ShoppingAssistantApi.Infrastructure.Services; using ShoppingAssistantApi.Infrastructure.Services;
@ -66,11 +67,17 @@ public class ProductTests
"-C", "-C",
" ;", " ;",
"[", "[",
"Message", "Products",
"]", "]",
" What", " GTX",
" u", " 3090",
" want", " ;",
" GTX",
" 3070TI",
" ;",
" GTX",
" 4070TI",
" ;",
" ?" " ?"
}; };
@ -78,6 +85,17 @@ public class ProductTests
_openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken)) _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken))
.Returns(expectedSseData.ToAsyncEnumerable()); .Returns(expectedSseData.ToAsyncEnumerable());
_wishListServiceMock.Setup(w => w.GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 1, cancellationToken))
.ReturnsAsync(new PagedList<MessageDto>(new List<MessageDto>
{
new MessageDto
{
Text = "Some existing message",
Id = "",
CreatedById = "",
Role = ""
}
}, 1, 1, 1));
// Act // Act
var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken); var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken);
@ -86,6 +104,69 @@ public class ProductTests
// Assert // Assert
// Check if the actual SSE events match the expected SSE events // 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<string>
{
"[",
"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<ChatCompletionRequest>(), cancellationToken))
.Returns(expectedSseData.ToAsyncEnumerable());
// Act
var resultStream = productService.SearchProductAsync(wishlistId, message, cancellationToken);
var actualSseEvents = await resultStream.ToListAsync();
// Assert
Assert.NotNull(actualSseEvents);
} }
} }