mirror of
https://github.com/Shchoholiev/shopping-assistant-api.git
synced 2025-04-26 00:07:19 +00:00
added new chips to the product search service and implemented unit tests
This commit is contained in:
parent
dc4826dacc
commit
3372a0910b
10
ShoppingAssistantApi.Api/Controllers/BaseController.cs
Normal file
10
ShoppingAssistantApi.Api/Controllers/BaseController.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ShoppingAssistantApi.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class BaseController : ControllerBase
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
Messages = new List<OpenAiMessage>
|
.GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 1, cancellationToken).Result;
|
||||||
{
|
|
||||||
new OpenAiMessage
|
|
||||||
{
|
|
||||||
Role = "User",
|
|
||||||
Content = ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Stream = true
|
|
||||||
};
|
|
||||||
|
|
||||||
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 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,16 +174,22 @@ public class ProductService : IProductService
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case SearchEventType.Product:
|
case SearchEventType.Product:
|
||||||
yield return new ServerSentEvent
|
productBuffer.Name += data;
|
||||||
|
if (data.Contains(";"))
|
||||||
{
|
{
|
||||||
Event = SearchEventType.Product,
|
yield return new ServerSentEvent
|
||||||
Data = data
|
{
|
||||||
};
|
Event = SearchEventType.Product,
|
||||||
|
Data = productBuffer.Name
|
||||||
|
};
|
||||||
|
productBuffer.Name = string.Empty;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SearchEventType DetermineDataType(string dataTypeHolder)
|
private SearchEventType DetermineDataType(string dataTypeHolder)
|
||||||
|
33
ShoppingAssistantApi.Tests/Tests/ProductsTests.cs
Normal file
33
ShoppingAssistantApi.Tests/Tests/ProductsTests.cs
Normal 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);
|
||||||
|
|
||||||
|
// Додайте додаткові перевірки на відповідь, якщо необхідно
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user