mirror of
https://github.com/Shchoholiev/shopping-assistant-api.git
synced 2025-04-06 17:49:41 +00:00
commit
9a79645730
ShoppingAssistantApi.Api
ShoppingAssistantApi.Application
ShoppingAssistantApi.Domain/Enums
ShoppingAssistantApi.Infrastructure/Services
ShoppingAssistantApi.Persistance/Repositories
ShoppingAssistantApi.UnitTests
@ -8,7 +8,6 @@ using ShoppingAssistantApi.Domain.Enums;
|
||||
|
||||
namespace ShoppingAssistantApi.Api.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class ProductsSearchController : BaseController
|
||||
{
|
||||
private readonly IProductService _productService;
|
||||
@ -21,16 +20,10 @@ public class ProductsSearchController : BaseController
|
||||
_wishlistsService = wishlistsService;
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPost("search/{wishlistId}")]
|
||||
public async Task StreamDataToClient(string wishlistId, [FromBody]MessageCreateDto message, CancellationToken cancellationToken)
|
||||
public async Task StreamDataToClient(string wishlistId, [FromBody] MessageCreateDto message, CancellationToken cancellationToken)
|
||||
{
|
||||
var dto = new MessageDto()
|
||||
{
|
||||
Text = message.Text,
|
||||
Role = MessageRoles.User.ToString(),
|
||||
};
|
||||
await _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, dto, cancellationToken);
|
||||
|
||||
Response.Headers.Add("Content-Type", "text/event-stream");
|
||||
Response.Headers.Add("Cache-Control", "no-cache");
|
||||
Response.Headers.Add("Connection", "keep-alive");
|
||||
@ -43,8 +36,8 @@ public class ProductsSearchController : BaseController
|
||||
|
||||
var serverSentEvent = $"event: {sse.Event}\ndata: {chunk}\n\n";
|
||||
|
||||
await Response.WriteAsync(serverSentEvent);
|
||||
await Response.Body.FlushAsync();
|
||||
await Response.WriteAsync(serverSentEvent, cancellationToken: cancellationToken);
|
||||
await Response.Body.FlushAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
using ShoppingAssistantApi.Application.Models.Dtos;
|
||||
using ShoppingAssistantApi.Application.Models.Operations;
|
||||
using HotChocolate.Authorization;
|
||||
using ShoppingAssistantApi.Application.IServices;
|
||||
|
||||
namespace ShoppingAssistantApi.Api.Mutations;
|
||||
|
||||
@ -27,4 +28,12 @@ public class UsersMutation
|
||||
public Task<UserDto> RemoveFromRoleAsync(string roleName, string userId, CancellationToken cancellationToken,
|
||||
[Service] IUserManager userManager)
|
||||
=> userManager.RemoveFromRoleAsync(roleName, userId, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
public async Task<bool> DeletePersonalUserAsync(string guestId, CancellationToken cancellationToken,
|
||||
[Service] IUsersService usersService)
|
||||
{
|
||||
await usersService.DeletePersonalUserAsync(guestId, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Linq.Expressions;
|
||||
using MongoDB.Bson;
|
||||
using ShoppingAssistantApi.Domain.Entities;
|
||||
|
||||
namespace ShoppingAssistantApi.Application.IRepositories;
|
||||
@ -6,4 +7,6 @@ namespace ShoppingAssistantApi.Application.IRepositories;
|
||||
public interface IMessagesRepository : IBaseRepository<Message>
|
||||
{
|
||||
Task<List<Message>> GetPageStartingFromEndAsync(int pageNumber, int pageSize, Expression<Func<Message, bool>> predicate, CancellationToken cancellationToken);
|
||||
|
||||
Task<List<Message>> GetWishlistMessagesAsync(ObjectId wishlistId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
@ -12,4 +12,6 @@ public interface IUsersService
|
||||
Task<UserDto> GetUserAsync(string id, CancellationToken cancellationToken);
|
||||
|
||||
Task UpdateUserAsync(UserDto dto, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
Task DeletePersonalUserAsync(string guestId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
@ -9,18 +9,14 @@ public enum OpenAiRole
|
||||
|
||||
public static class OpenAiRoleExtensions
|
||||
{
|
||||
public static string RequestConvert(this OpenAiRole role)
|
||||
public static string ToRequestString(this OpenAiRole role)
|
||||
{
|
||||
switch (role)
|
||||
return role switch
|
||||
{
|
||||
case OpenAiRole.System:
|
||||
return "system";
|
||||
case OpenAiRole.Assistant:
|
||||
return "assistant";
|
||||
case OpenAiRole.User:
|
||||
return "user";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
OpenAiRole.System => "system",
|
||||
OpenAiRole.Assistant => "assistant",
|
||||
OpenAiRole.User => "user",
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using ShoppingAssistantApi.Application.IServices;
|
||||
using ShoppingAssistantApi.Application.Models.OpenAi;
|
||||
@ -23,9 +22,14 @@ public class OpenAiService : IOpenAiService
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public OpenAiService(IHttpClientFactory httpClientFactory)
|
||||
private readonly ILogger<OpenAiService> _logger;
|
||||
|
||||
public OpenAiService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<OpenAiService> logger)
|
||||
{
|
||||
_httpClient = httpClientFactory.CreateClient("OpenAiHttpClient");
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<OpenAiMessage> GetChatCompletion(ChatCompletionRequest chat, CancellationToken cancellationToken)
|
||||
@ -45,27 +49,41 @@ public class OpenAiService : IOpenAiService
|
||||
|
||||
public async IAsyncEnumerable<string> GetChatCompletionStream(ChatCompletionRequest chat, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation($"Sending completion stream request to OpenAI.");
|
||||
|
||||
chat.Stream = true;
|
||||
var jsonBody = JsonConvert.SerializeObject(chat, _jsonSettings);
|
||||
|
||||
var body = new StringContent(jsonBody, Encoding.UTF8, "application/json");
|
||||
|
||||
using var httpResponse = await _httpClient.PostAsync("", body, cancellationToken);
|
||||
|
||||
using var responseStream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using var reader = new StreamReader(responseStream, Encoding.UTF8);
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "")
|
||||
{
|
||||
var jsonChunk = await reader.ReadLineAsync();
|
||||
if (jsonChunk.StartsWith("data: "))
|
||||
Content = body
|
||||
};
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
|
||||
|
||||
using var httpResponse = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
|
||||
var allData = string.Empty;
|
||||
|
||||
using var streamReader = new StreamReader(await httpResponse.Content.ReadAsStreamAsync(cancellationToken));
|
||||
while (!streamReader.EndOfStream)
|
||||
{
|
||||
var line = await streamReader.ReadLineAsync(cancellationToken);
|
||||
allData += line + "\n\n";
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
|
||||
var json = line?.Substring(6, line.Length - 6);
|
||||
if (json == "[DONE]")
|
||||
{
|
||||
jsonChunk = jsonChunk.Substring("data: ".Length);
|
||||
if (jsonChunk == "[DONE]") break;
|
||||
var data = JsonConvert.DeserializeObject<OpenAiResponse>(jsonChunk);
|
||||
if (data.Choices[0].Delta.Content == "" || data.Choices[0].Delta.Content == null) continue;
|
||||
yield return data.Choices[0].Delta.Content;
|
||||
yield return json;
|
||||
yield break;
|
||||
}
|
||||
|
||||
var data = JsonConvert.DeserializeObject<OpenAiResponse>(json);
|
||||
|
||||
if (data.Choices[0].Delta.Content == "" || data.Choices[0].Delta.Content == null) continue;
|
||||
|
||||
yield return data.Choices[0].Delta.Content;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MongoDB.Bson;
|
||||
using ShoppingAssistantApi.Application.IRepositories;
|
||||
using ShoppingAssistantApi.Application.IServices;
|
||||
@ -6,7 +6,6 @@ using ShoppingAssistantApi.Application.Models.CreateDtos;
|
||||
using ShoppingAssistantApi.Application.Models.Dtos;
|
||||
using ShoppingAssistantApi.Application.Models.OpenAi;
|
||||
using ShoppingAssistantApi.Application.Models.ProductSearch;
|
||||
using ShoppingAssistantApi.Domain.Entities;
|
||||
using ShoppingAssistantApi.Domain.Enums;
|
||||
using ServerSentEvent = ShoppingAssistantApi.Application.Models.ProductSearch.ServerSentEvent;
|
||||
|
||||
@ -15,126 +14,128 @@ namespace ShoppingAssistantApi.Infrastructure.Services;
|
||||
public class ProductService : IProductService
|
||||
{
|
||||
private readonly IWishlistsService _wishlistsService;
|
||||
|
||||
|
||||
private readonly IOpenAiService _openAiService;
|
||||
|
||||
private readonly IMessagesRepository _messagesRepository;
|
||||
|
||||
private bool mqchecker = false;
|
||||
|
||||
private SearchEventType currentDataType = SearchEventType.Wishlist;
|
||||
|
||||
public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService, IMessagesRepository messagesRepository)
|
||||
private readonly ILogger<ProductService> _logger;
|
||||
|
||||
public ProductService(
|
||||
IOpenAiService openAiService,
|
||||
IWishlistsService wishlistsService,
|
||||
IMessagesRepository messagesRepository,
|
||||
ILogger<ProductService> logger)
|
||||
{
|
||||
_openAiService = openAiService;
|
||||
_wishlistsService = wishlistsService;
|
||||
_messagesRepository = messagesRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<ServerSentEvent> SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken)
|
||||
public async IAsyncEnumerable<ServerSentEvent> SearchProductAsync(string wishlistId, MessageCreateDto newMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
string promptForGpt =
|
||||
var systemPrompt =
|
||||
"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[Question] - return question. Must be followed by suggestions how to answer the question" +
|
||||
"\n[Suggestions] - return semicolon separated suggestion how to answer to a question" +
|
||||
"\n[Message] - return text" +
|
||||
"\n[Products] - return semicolon separated product names";
|
||||
|
||||
var countOfMessage = await _messagesRepository
|
||||
.GetCountAsync(message=>message.WishlistId == ObjectId.Parse((wishlistId)), cancellationToken);
|
||||
|
||||
var previousMessages = await _wishlistsService
|
||||
.GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, countOfMessage, cancellationToken);
|
||||
var wishlistObjectId = ObjectId.Parse(wishlistId);
|
||||
var messages = await _messagesRepository.GetWishlistMessagesAsync(wishlistObjectId, cancellationToken);
|
||||
|
||||
var chatRequest = new ChatCompletionRequest
|
||||
{
|
||||
Messages = new List<OpenAiMessage>
|
||||
{
|
||||
new OpenAiMessage
|
||||
{
|
||||
Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.System),
|
||||
Content = promptForGpt
|
||||
new() {
|
||||
Role = OpenAiRole.System.ToRequestString(),
|
||||
Content = systemPrompt
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var messagesForOpenAI = new List<OpenAiMessage>();
|
||||
|
||||
foreach (var item in previousMessages.Items)
|
||||
for (int i = 0; i < messages.Count; i++)
|
||||
{
|
||||
if (item.Role == "Application")
|
||||
var message = messages[i];
|
||||
if (i == 0)
|
||||
{
|
||||
messagesForOpenAI
|
||||
.Add(new OpenAiMessage()
|
||||
{
|
||||
Role = OpenAiRole.Assistant.RequestConvert(),
|
||||
Content = item.Text
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
messagesForOpenAI
|
||||
.Add(new OpenAiMessage()
|
||||
{
|
||||
Role = item.Role.ToLower(),
|
||||
Content = item.Text
|
||||
});
|
||||
message.Text = "[Question] " + message.Text + "\n [Suggestions] Bicycle, Laptop";
|
||||
}
|
||||
|
||||
chatRequest.Messages
|
||||
.Add(new OpenAiMessage()
|
||||
{
|
||||
Role = message.Role == "Application" ? "assistant" : "user",
|
||||
Content = message.Text
|
||||
});
|
||||
}
|
||||
|
||||
messagesForOpenAI.Add(new OpenAiMessage()
|
||||
chatRequest.Messages.Add(new ()
|
||||
{
|
||||
Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.User),
|
||||
Content = message.Text
|
||||
Role = OpenAiRole.User.ToRequestString(),
|
||||
Content = newMessage.Text
|
||||
});
|
||||
|
||||
chatRequest.Messages.AddRange(messagesForOpenAI);
|
||||
|
||||
|
||||
// Don't wait for the task to finish because we dont need the result of this task
|
||||
var dto = new MessageDto()
|
||||
{
|
||||
Text = newMessage.Text,
|
||||
Role = MessageRoles.User.ToString(),
|
||||
};
|
||||
var saveNewMessageTask = _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, dto, cancellationToken);
|
||||
|
||||
var currentDataType = SearchEventType.Wishlist;
|
||||
var suggestionBuffer = new Suggestion();
|
||||
var messageBuffer = new MessagePart();
|
||||
var productBuffer = new ProductName();
|
||||
var dataTypeHolder = string.Empty;
|
||||
var counter = 0;
|
||||
|
||||
await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken))
|
||||
{
|
||||
counter++;
|
||||
if (mqchecker && currentDataType == SearchEventType.Message && messageBuffer != null)
|
||||
if (data == "[DONE]")
|
||||
{
|
||||
if (data == "[")
|
||||
if (!string.IsNullOrEmpty(messageBuffer.Text))
|
||||
{
|
||||
_wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageDto()
|
||||
_ = await _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageDto()
|
||||
{
|
||||
Text = messageBuffer.Text,
|
||||
Role = MessageRoles.Application.ToString(),
|
||||
}, cancellationToken);
|
||||
mqchecker = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.Contains("["))
|
||||
{
|
||||
dataTypeHolder = string.Empty;
|
||||
dataTypeHolder += data;
|
||||
}
|
||||
|
||||
else if (data.Contains("]"))
|
||||
yield break;
|
||||
}
|
||||
else if (data.Contains('['))
|
||||
{
|
||||
dataTypeHolder = data;
|
||||
}
|
||||
else if (data.Contains(']'))
|
||||
{
|
||||
dataTypeHolder += data;
|
||||
currentDataType = DetermineDataType(dataTypeHolder);
|
||||
if (currentDataType == SearchEventType.Message)
|
||||
{
|
||||
mqchecker = true;
|
||||
}
|
||||
}
|
||||
_ = await saveNewMessageTask;
|
||||
// Don't wait for the task to finish because we dont need the result of this task
|
||||
_ = _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageDto()
|
||||
{
|
||||
Text = messageBuffer.Text,
|
||||
Role = MessageRoles.Application.ToString(),
|
||||
}, cancellationToken);
|
||||
|
||||
else if (dataTypeHolder=="[" && !data.Contains("["))
|
||||
messageBuffer.Text = string.Empty;
|
||||
}
|
||||
|
||||
dataTypeHolder += data;
|
||||
currentDataType = DetermineDataType(dataTypeHolder);
|
||||
|
||||
dataTypeHolder = string.Empty;
|
||||
}
|
||||
else if (dataTypeHolder.Contains('['))
|
||||
{
|
||||
dataTypeHolder += data;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
switch (currentDataType)
|
||||
@ -147,47 +148,44 @@ public class ProductService : IProductService
|
||||
};
|
||||
currentDataType = SearchEventType.Message;
|
||||
messageBuffer.Text += data;
|
||||
|
||||
break;
|
||||
|
||||
case SearchEventType.Suggestion:
|
||||
if (data.Contains(";"))
|
||||
if (data.Contains(';'))
|
||||
{
|
||||
yield return new ServerSentEvent
|
||||
{
|
||||
Event = SearchEventType.Suggestion,
|
||||
Data = suggestionBuffer.Text
|
||||
Data = suggestionBuffer.Text.Trim()
|
||||
};
|
||||
suggestionBuffer.Text = string.Empty;
|
||||
break;
|
||||
}
|
||||
|
||||
suggestionBuffer.Text += data;
|
||||
|
||||
break;
|
||||
|
||||
case SearchEventType.Product:
|
||||
if (data.Contains(";"))
|
||||
if (data.Contains(';'))
|
||||
{
|
||||
yield return new ServerSentEvent
|
||||
{
|
||||
Event = SearchEventType.Product,
|
||||
Data = productBuffer.Name
|
||||
Data = productBuffer.Name.Trim()
|
||||
};
|
||||
productBuffer.Name = string.Empty;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
productBuffer.Name += data;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentDataType == SearchEventType.Message)
|
||||
{
|
||||
_wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageDto()
|
||||
{
|
||||
Text = messageBuffer.Text,
|
||||
Role = MessageRoles.Application.ToString(),
|
||||
}, cancellationToken);
|
||||
mqchecker = false;
|
||||
}
|
||||
}
|
||||
|
||||
private SearchEventType DetermineDataType(string dataTypeHolder)
|
||||
@ -196,7 +194,7 @@ public class ProductService : IProductService
|
||||
{
|
||||
return SearchEventType.Message;
|
||||
}
|
||||
else if (dataTypeHolder.StartsWith("[Options]"))
|
||||
else if (dataTypeHolder.StartsWith("[Suggestions]"))
|
||||
{
|
||||
return SearchEventType.Suggestion;
|
||||
}
|
||||
@ -213,5 +211,4 @@ public class ProductService : IProductService
|
||||
return SearchEventType.Wishlist;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -59,4 +59,26 @@ public class UsersService : IUsersService
|
||||
entity.LastModifiedDateUtc = DateTime.UtcNow;
|
||||
await _repository.UpdateUserAsync(entity, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeletePersonalUserAsync(string guestId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Guid.TryParse(guestId, out var guid))
|
||||
{
|
||||
throw new InvalidDataException("Provided id is invalid.");
|
||||
}
|
||||
|
||||
var entity = await _repository.GetUserAsync(u => u.GuestId == guid, cancellationToken);
|
||||
|
||||
if (entity.Id != GlobalUser.Id)
|
||||
{
|
||||
throw new UnAuthorizedException<User>();
|
||||
}
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
throw new EntityNotFoundException<User>();
|
||||
}
|
||||
|
||||
await _repository.DeleteAsync(entity, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -82,12 +82,12 @@ public class WishlistsService : IWishlistsService
|
||||
{
|
||||
new OpenAiMessage
|
||||
{
|
||||
Role = OpenAiRole.System.RequestConvert(),
|
||||
Content = "You will be provided with a general information about some product and your task is to generate general (not specific to any company or brand) chat name where recommendations on which specific product to buy will be given. Only name he product without adverbs and adjectives\nExamples:\n - Prompt: Hub For Macbook. Answer: Macbook Hub\n - Prompt: What is the best power bank for MacBook with capacity 20000 mAh and power near 20V? Answer: Macbook Powerbank"
|
||||
Role = OpenAiRole.System.ToRequestString(),
|
||||
Content = "You will be provided with a general information about some product and your task is to generate general (not specific to any company or brand) chat name where recommendations on which specific product to buy will be given. Only name he product without adverbs and adjectives. Limit the name length to 5 words\nExamples:\n - Prompt: Hub For Macbook. Answer: Macbook Hub\n - Prompt: What is the best power bank for MacBook with capacity 20000 mAh and power near 20V? Answer: Macbook Powerbank\nIf the information tells nothing about some product answer with short generic name"
|
||||
},
|
||||
new OpenAiMessage
|
||||
{
|
||||
Role = OpenAiRole.User.RequestConvert(),
|
||||
Role = OpenAiRole.User.ToRequestString(),
|
||||
Content = firstUserMessage.Text
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Linq.Expressions;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using ShoppingAssistantApi.Application.IRepositories;
|
||||
using ShoppingAssistantApi.Domain.Entities;
|
||||
@ -18,4 +19,11 @@ public class MessagesRepository : BaseRepository<Message>, IMessagesRepository
|
||||
.Limit(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public Task<List<Message>> GetWishlistMessagesAsync(ObjectId wishlistId, CancellationToken cancellationToken)
|
||||
{
|
||||
return _collection
|
||||
.Find(x => !x.IsDeleted && x.WishlistId == wishlistId)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
using ShoppingAssistantApi.Application.IServices;
|
||||
@ -31,7 +32,7 @@ public class OpenAiServiceTests
|
||||
return client;
|
||||
});
|
||||
|
||||
_openAiService = new OpenAiService(_mockHttpClientFactory.Object);
|
||||
_openAiService = new OpenAiService(_mockHttpClientFactory.Object, new Mock<ILogger<OpenAiService>>().Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -78,7 +79,7 @@ public class OpenAiServiceTests
|
||||
{
|
||||
new OpenAiMessage
|
||||
{
|
||||
Role = OpenAiRole.User.RequestConvert(),
|
||||
Role = OpenAiRole.User.ToRequestString(),
|
||||
Content = "Return Hello World!"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Moq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MongoDB.Bson;
|
||||
using Moq;
|
||||
using ShoppingAssistantApi.Application.IRepositories;
|
||||
using ShoppingAssistantApi.Application.IServices;
|
||||
using ShoppingAssistantApi.Application.Models.CreateDtos;
|
||||
@ -27,14 +29,14 @@ public class ProductTests
|
||||
_messagesRepositoryMock = new Mock<IMessagesRepository>();
|
||||
_openAiServiceMock = new Mock<IOpenAiService>();
|
||||
_wishListServiceMock = new Mock<IWishlistsService>();
|
||||
_productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object, _messagesRepositoryMock.Object);
|
||||
_productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object, _messagesRepositoryMock.Object, new Mock<ILogger<ProductService>>().Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchProductAsync_WhenWishlistsWithoutMessages_ReturnsExpectedEvents()
|
||||
{
|
||||
// Arrange
|
||||
string wishlistId = "existingWishlistId";
|
||||
string wishlistId = "657657677c13ae4bc95e2f41";
|
||||
var message = new MessageCreateDto
|
||||
{
|
||||
Text = "Your message text here"
|
||||
@ -44,47 +46,28 @@ public class ProductTests
|
||||
// Define your expected SSE data for the test
|
||||
var expectedSseData = new List<string>
|
||||
{
|
||||
"[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", " USB-C", " ;", " Keyboard", " ultra",
|
||||
" ;", "[", "Options", "]", " USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX",
|
||||
"[", "Message", "]", " What", " u", " want", " ?", "[", "Suggestions", "]", " USB-C", " ;", " Keyboard", " ultra",
|
||||
" ;", "[", "Suggestions", "]", " USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX",
|
||||
" 3070TI", " ;", " GTX", " 4070TI", " ;", " ?", "[", "Message", "]", " What", " u", " want", " ?"
|
||||
};
|
||||
|
||||
var expectedMessages = new List<string> { " What", " u", " want", " ?", " What", " u", " want", " ?" };
|
||||
var expectedSuggestion = new List<string> { " USB-C", " Keyboard ultra", " USB-C" };
|
||||
var expectedSuggestion = new List<string> { "USB-C", "Keyboard ultra", "USB-C" };
|
||||
|
||||
// Mock the GetChatCompletionStream method to provide the expected SSE data
|
||||
_openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken))
|
||||
.Returns(expectedSseData.ToAsyncEnumerable());
|
||||
|
||||
_messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny<Expression<Func<Message, bool>>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(1);
|
||||
|
||||
_wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny<MessageDto>(), cancellationToken))
|
||||
.Verifiable();
|
||||
|
||||
_wishListServiceMock
|
||||
.Setup(m => m.GetMessagesPageFromPersonalWishlistAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<CancellationToken>())
|
||||
)
|
||||
.ReturnsAsync(new PagedList<MessageDto>(
|
||||
new List<MessageDto>
|
||||
_messagesRepositoryMock.Setup(m => m.GetWishlistMessagesAsync(It.IsAny<ObjectId>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Message>
|
||||
{
|
||||
new MessageDto
|
||||
new()
|
||||
{
|
||||
Text = "What are you looking for?",
|
||||
Id = "3",
|
||||
CreatedById = "User2",
|
||||
Role = "User"
|
||||
},
|
||||
},
|
||||
1,
|
||||
1,
|
||||
1
|
||||
));
|
||||
|
||||
});
|
||||
|
||||
// Act
|
||||
var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken);
|
||||
|
||||
@ -113,7 +96,7 @@ public class ProductTests
|
||||
public async void SearchProductAsync_WithExistingMessageInWishlist_ReturnsExpectedEvents()
|
||||
{
|
||||
// Arrange
|
||||
var wishlistId = "your_wishlist_id";
|
||||
var wishlistId = "657657677c13ae4bc95e2f41";
|
||||
var message = new MessageCreateDto { Text = "Your message text" };
|
||||
var cancellationToken = new CancellationToken();
|
||||
|
||||
@ -121,8 +104,8 @@ public class ProductTests
|
||||
|
||||
var expectedSseData = new List<string>
|
||||
{
|
||||
"[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", "USB-C", " ;", "Keyboard", " ultra",
|
||||
" ;", "[", "Options", "]", "USB", "-C", " ;"
|
||||
"[", "Message", "]", " What", " u", " want", " ?", "[", "Suggestions", "]", "USB-C", " ;", "Keyboard", " ultra",
|
||||
" ;", "[", "Suggestions", "]", "USB", "-C", " ;"
|
||||
};
|
||||
|
||||
var expectedMessages = new List<string> { " What", " u", " want", " ?" };
|
||||
@ -131,40 +114,25 @@ public class ProductTests
|
||||
// Mock the GetChatCompletionStream method to provide the expected SSE data
|
||||
_openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken))
|
||||
.Returns(expectedSseData.ToAsyncEnumerable());
|
||||
|
||||
_messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny<Expression<Func<Message, bool>>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(3);
|
||||
|
||||
_wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny<MessageDto>(), cancellationToken))
|
||||
.Verifiable();
|
||||
|
||||
_wishListServiceMock
|
||||
.Setup(w => w.GetMessagesPageFromPersonalWishlistAsync(
|
||||
It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new PagedList<MessageDto>(new List<MessageDto>
|
||||
{
|
||||
new MessageDto
|
||||
_messagesRepositoryMock.Setup(m => m.GetWishlistMessagesAsync(It.IsAny<ObjectId>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Message>
|
||||
{
|
||||
Text = "Message 1",
|
||||
Id = "1",
|
||||
CreatedById = "User2",
|
||||
Role = "User"
|
||||
},
|
||||
new MessageDto
|
||||
{
|
||||
Text = "Message 2",
|
||||
Id = "2",
|
||||
CreatedById = "User2",
|
||||
Role = "User"
|
||||
},
|
||||
new MessageDto
|
||||
{
|
||||
Text = "Message 3",
|
||||
Id = "3",
|
||||
CreatedById = "User2",
|
||||
Role = "User"
|
||||
},
|
||||
}, 1, 3, 3));
|
||||
new() {
|
||||
Text = "Message 1",
|
||||
Role = "User"
|
||||
},
|
||||
new Message
|
||||
{
|
||||
Text = "Message 2",
|
||||
Role = "User"
|
||||
},
|
||||
new Message
|
||||
{
|
||||
Text = "Message 3",
|
||||
Role = "User"
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken);
|
||||
@ -186,7 +154,6 @@ public class ProductTests
|
||||
Assert.NotNull(actualSseEvents);
|
||||
Assert.Equal(expectedMessages, receivedMessages);
|
||||
Assert.Equal(expectedSuggestions, receivedSuggestions);
|
||||
_wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny<MessageDto>(), cancellationToken), Times.Once);
|
||||
}
|
||||
|
||||
|
||||
@ -194,7 +161,7 @@ public class ProductTests
|
||||
public async void SearchProductAsync_WithExistingMessageInWishlistAndAddProduct_ReturnsExpectedEvents()
|
||||
{
|
||||
// Arrange
|
||||
var wishlistId = "your_wishlist_id";
|
||||
var wishlistId = "657657677c13ae4bc95e2f41";
|
||||
var message = new MessageCreateDto { Text = "Your message text" };
|
||||
var cancellationToken = new CancellationToken();
|
||||
|
||||
@ -202,8 +169,8 @@ public class ProductTests
|
||||
|
||||
var expectedSseData = new List<string>
|
||||
{
|
||||
"[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", "USB-C", " ;", "Keyboard", " ultra",
|
||||
" ;", "[", "Options", "]", "USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX",
|
||||
"[", "Message", "]", " What", " u", " want", " ?", "[", "Suggestions", "]", "USB-C", " ;", "Keyboard", " ultra",
|
||||
" ;", "[", "Suggestions", "]", "USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX",
|
||||
" 3070TI", " ;", " GTX", " 4070TI", " ;", " ?"
|
||||
};
|
||||
|
||||
@ -214,36 +181,25 @@ public class ProductTests
|
||||
_openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken))
|
||||
.Returns(expectedSseData.ToAsyncEnumerable());
|
||||
|
||||
_messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny<Expression<Func<Message, bool>>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(3);
|
||||
|
||||
_wishListServiceMock
|
||||
.Setup(w => w.GetMessagesPageFromPersonalWishlistAsync(
|
||||
It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new PagedList<MessageDto>(new List<MessageDto>
|
||||
{
|
||||
new MessageDto
|
||||
_messagesRepositoryMock.Setup(m => m.GetWishlistMessagesAsync(It.IsAny<ObjectId>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Message>
|
||||
{
|
||||
Text = "Message 1",
|
||||
Id = "1",
|
||||
CreatedById = "User2",
|
||||
Role = "User"
|
||||
},
|
||||
new MessageDto
|
||||
{
|
||||
Text = "Message 2",
|
||||
Id = "2",
|
||||
CreatedById = "User2",
|
||||
Role = "User"
|
||||
},
|
||||
new MessageDto
|
||||
{
|
||||
Text = "Message 3",
|
||||
Id = "3",
|
||||
CreatedById = "User2",
|
||||
Role = "User"
|
||||
},
|
||||
}, 1, 3, 3));
|
||||
new()
|
||||
{
|
||||
Text = "Message 1",
|
||||
Role = "User"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Text = "Message 2",
|
||||
Role = "User"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Text = "Message 3",
|
||||
Role = "User"
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken);
|
||||
@ -265,7 +221,5 @@ public class ProductTests
|
||||
Assert.NotNull(actualSseEvents);
|
||||
Assert.Equal(expectedMessages, receivedMessages);
|
||||
Assert.Equal(expectedSuggestions, receivedSuggestions);
|
||||
_wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync(
|
||||
wishlistId, It.IsAny<MessageDto>(), cancellationToken), Times.Once);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user