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" />
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
</ItemGroup>
</Project>

View File

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

View File

@ -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<ServerSentEvent> SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken)
{
var chatRequest = new ChatCompletionRequest
{
Messages = new List<OpenAiMessage>
{
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<OpenAiMessage>
{
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<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 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)

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.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<ChatCompletionRequest>(), cancellationToken))
.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
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<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);
}
}