mirror of
https://github.com/Shchoholiev/shopping-assistant-api.git
synced 2025-04-09 08:28:46 +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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Controllers\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
|
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.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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user