From 2dfadc4b79867c5bbb7f06d966faf7d00388a45e Mon Sep 17 00:00:00 2001 From: Kristian Ivanov Date: Tue, 11 Jun 2024 03:21:12 +0300 Subject: [PATCH] Created the ChatController and updated the ChatService. --- .../Common/CompletionConstants/Completions.cs | 4 +- .../CookingApp/Common/ExceptionMessages.cs | 7 +- .../CookingApp/Common/SuccessMessages.cs | 10 + .../Common/TaskInformationMessages.cs | 10 + .../CookingApp/Controllers/ChatController.cs | 72 +++++++ .../Controllers/ChatGPTController.cs | 54 ----- .../Extensions/ServiceCollectionExtensions.cs | 4 +- .../CookingApp/Models/DTOs/CreateChatDTO.cs | 18 ++ .../Services/ChatService/ChatService.cs | 184 +++++++++++++++++- .../Services/ChatService/IChatService.cs | 9 +- .../OpenAI/Completions/CompletionService.cs | 154 --------------- .../OpenAI/Completions/ICompletionService.cs | 11 -- 12 files changed, 311 insertions(+), 226 deletions(-) create mode 100644 src/server/CookingApp/Common/SuccessMessages.cs create mode 100644 src/server/CookingApp/Common/TaskInformationMessages.cs create mode 100644 src/server/CookingApp/Controllers/ChatController.cs delete mode 100644 src/server/CookingApp/Controllers/ChatGPTController.cs create mode 100644 src/server/CookingApp/Models/DTOs/CreateChatDTO.cs delete mode 100644 src/server/CookingApp/Services/OpenAI/Completions/CompletionService.cs delete mode 100644 src/server/CookingApp/Services/OpenAI/Completions/ICompletionService.cs diff --git a/src/server/CookingApp/Common/CompletionConstants/Completions.cs b/src/server/CookingApp/Common/CompletionConstants/Completions.cs index e21abf24..5824015e 100644 --- a/src/server/CookingApp/Common/CompletionConstants/Completions.cs +++ b/src/server/CookingApp/Common/CompletionConstants/Completions.cs @@ -2,7 +2,7 @@ { public class Completions { - public const string Instructions = "You are a helpful assistant that answers questions related to cooking tips, recipes, kitchen tips. \" +" + + public const string AssistantInstructions = "You are a helpful assistant that answers questions related to cooking tips, recipes, kitchen tips. \" +" + "\r\nYou will receive queries containing different questions on cooking thematic or a list of products that you have to make use of and come up with a recipe for the user.\" +" + "\r\nYou need to take into account the user's dietary needs and their allergies so that you do not suggest a recipe that includes unhealthy or inappropriate contents. \" +" + "\r\nHere is a list of the user's allergies:"; @@ -54,5 +54,7 @@ public class Completions "\r\nAdjust the seasoning according to your taste preference." + "\r\nFeel free to add other herbs or spices that you like." + "\r\nEnjoy your meal!"; + + public const string TitleGenerationPrompt = "Generate a title from this sentence:"; } } diff --git a/src/server/CookingApp/Common/ExceptionMessages.cs b/src/server/CookingApp/Common/ExceptionMessages.cs index 438836d0..2a660845 100644 --- a/src/server/CookingApp/Common/ExceptionMessages.cs +++ b/src/server/CookingApp/Common/ExceptionMessages.cs @@ -4,6 +4,11 @@ public static class ExceptionMessages { public const string NullOrEmptyInputValues = "The provided input contains either null or an empty value"; public const string SubscriptionCreationFail = "Failed to create a subscription. {0}"; - public const string ResponseRequestFailed = "The ChatGPT API failed to respond. Please try again."; + + public class ChatGPT + { + public const string ResponseError = "The ChatGPT Service failed to respond. Please try again."; + public const string ConnectionError = "Something went wrong. Follow the log for more information."; + } } } diff --git a/src/server/CookingApp/Common/SuccessMessages.cs b/src/server/CookingApp/Common/SuccessMessages.cs new file mode 100644 index 00000000..8472770e --- /dev/null +++ b/src/server/CookingApp/Common/SuccessMessages.cs @@ -0,0 +1,10 @@ +namespace CookingApp.Common +{ + public class SuccessMessages + { + public class ChatGPT + { + public const string ResponseSuccess = "Response received successully."; + } + } +} diff --git a/src/server/CookingApp/Common/TaskInformationMessages.cs b/src/server/CookingApp/Common/TaskInformationMessages.cs new file mode 100644 index 00000000..d091155e --- /dev/null +++ b/src/server/CookingApp/Common/TaskInformationMessages.cs @@ -0,0 +1,10 @@ +namespace CookingApp.Common +{ + public class TaskInformationMessages + { + public class ChatGPT + { + public const string ConnectionAttempt = "Attempting to establish connection with ChatGPT API."; + } + } +} diff --git a/src/server/CookingApp/Controllers/ChatController.cs b/src/server/CookingApp/Controllers/ChatController.cs new file mode 100644 index 00000000..1b26b12a --- /dev/null +++ b/src/server/CookingApp/Controllers/ChatController.cs @@ -0,0 +1,72 @@ +namespace CookingApp.Controllers +{ + using CookingApp.Common; + using CookingApp.Models.DTOs; + using CookingApp.Services.ChatHistory; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + + [ApiController] + public class ChatController : ControllerBase + { + private readonly IChatService _chatService; + private readonly ILogger _logger; + + public ChatController(IChatService chatService, ILogger logger) + { + _chatService = chatService; + _logger = logger; + } + + [HttpGet("chats")] + public async Task GetChats() + { + //TODO: implement Azure Entra Id functions + //var user = _userService.GetUser(); + var chats = await _chatService.GetAllByUserId("user.Id"); + return Ok(chats); + } + + [HttpPost("chat-request")] + public async Task SendQuery([FromBody] string message, [FromHeader] string? chatId = null) + { + try + { + if (!ModelState.IsValid) + { + return BadRequest(); + } + + _logger.LogInformation(TaskInformationMessages.ChatGPT.ConnectionAttempt); + + var result = + String.IsNullOrEmpty(chatId) + ? await _chatService.CreateChat(message) + : await _chatService.UpdateChat(message, chatId); + + if (result == null) + { + _logger.LogError(ExceptionMessages.ChatGPT.ConnectionError); + _logger.LogError(ExceptionMessages.ChatGPT.ResponseError); + + return NoContent(); + } + + _logger.LogInformation(SuccessMessages.ChatGPT.ResponseSuccess); + // To display the message you need to get into result.Choices[0].Message.Content. + // The chat id is also contained inside the result + + return Ok(result); + } + catch (Exception e) + { + _logger.LogError(ExceptionMessages.ChatGPT.ConnectionError); + _logger.LogError($"{e.Message}"); + + return BadRequest(); + } + + return Ok(); + } + } +} diff --git a/src/server/CookingApp/Controllers/ChatGPTController.cs b/src/server/CookingApp/Controllers/ChatGPTController.cs deleted file mode 100644 index dadc2e2d..00000000 --- a/src/server/CookingApp/Controllers/ChatGPTController.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace CookingApp.Controllers -{ - using CookingApp.Common; - using CookingApp.Services.OpenAI.Completions; - using Microsoft.AspNetCore.Authorization; - using Microsoft.AspNetCore.Mvc; - using OpenAI.ObjectModels.ResponseModels; - - [ApiController] - [AllowAnonymous] - public class GPTController : ControllerBase - { - private readonly ICompletionService _completionService; - private readonly ILogger _logger; - - public GPTController(ICompletionService completionService, ILogger logger) - { - _completionService = completionService; - _logger = logger; - } - - [HttpPost("chat-request")] - [AllowAnonymous] - public async Task SendQuery([FromBody] string message, [FromHeader] string? chatId = null) - { - try - { - _logger.LogInformation("Attempting ChatGPT API connection."); - - var result = - String.IsNullOrEmpty(chatId) - ? await _completionService.CreateCompletion(message) - : await _completionService.UpdateCompletion(message, chatId); - - if (result == null) - { - return BadRequest(ExceptionMessages.ResponseRequestFailed); - } - - _logger.LogInformation("Successfully connected to ChatGPT API."); - _logger.LogInformation("Response received."); - // To display the message you need to get into result.Choices[0].Message.Content. - // The chat id is also contained inside the result - - return Ok(result); - } - catch (Exception ex) - { - _logger.LogError("Failed attempt to contact ChatGPT API."); - return BadRequest(ex.Message); - } - } - } -} diff --git a/src/server/CookingApp/Infrastructure/Extensions/ServiceCollectionExtensions.cs b/src/server/CookingApp/Infrastructure/Extensions/ServiceCollectionExtensions.cs index 074b8840..c45f8c56 100644 --- a/src/server/CookingApp/Infrastructure/Extensions/ServiceCollectionExtensions.cs +++ b/src/server/CookingApp/Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -15,10 +15,10 @@ using CookingApp.Services.Stripe; using Stripe; using CookingApp.Infrastructure.Configurations.Stripe; -using CookingApp.Services.OpenAI.Completions; using OpenAI.Extensions; using OpenAI; using Microsoft.AspNetCore.DataProtection.KeyManagement; +using CookingApp.Services.ChatHistory; namespace CookingApp.Infrastructure.Extensions { @@ -148,7 +148,7 @@ public static IHostApplicationBuilder AddOpenAIIntegration(this WebApplicationBu options.DefaultModelId = builder.Configuration.GetValue("OpenAPIOptions:Model") ?? string.Empty; }); - builder.Services.AddScoped(); + builder.Services.AddScoped(); return builder; } diff --git a/src/server/CookingApp/Models/DTOs/CreateChatDTO.cs b/src/server/CookingApp/Models/DTOs/CreateChatDTO.cs new file mode 100644 index 00000000..1ed6db28 --- /dev/null +++ b/src/server/CookingApp/Models/DTOs/CreateChatDTO.cs @@ -0,0 +1,18 @@ +namespace CookingApp.Models.DTOs +{ + using MongoDB.Bson.Serialization.Attributes; + + public class CreateChatDTO + { + public string Id { get; set; } + public string? Title { get; set; } + + public string? UserId { get; set; } + + public DateTime CreatedTime { get; set; } + + public List Requests { get; set; } = new List(); + + public List Responses { get; set; } = new List(); + } +} diff --git a/src/server/CookingApp/Services/ChatService/ChatService.cs b/src/server/CookingApp/Services/ChatService/ChatService.cs index 82fa9320..5ebbd7f8 100644 --- a/src/server/CookingApp/Services/ChatService/ChatService.cs +++ b/src/server/CookingApp/Services/ChatService/ChatService.cs @@ -1,18 +1,45 @@ namespace CookingApp.Services.ChatHistory { + using global::OpenAI.Interfaces; + using global::OpenAI.ObjectModels; + using global::OpenAI.ObjectModels.RequestModels; + using global::OpenAI.ObjectModels.ResponseModels; + using CookingApp.Common.CompletionConstants; + using CookingApp.Common; using CookingApp.Infrastructure.Interfaces; + using CookingApp.Models.DTOs; + using System.Text.Json; + using CookingApp.Models; public class ChatService : IChatService { private readonly IRepository _chatRepository; + private readonly ILogger _logger; + private readonly IChatService _chatService; + private readonly IOpenAIService _openAIService; - public ChatService(IRepository chatRepository) + public ChatService(IOpenAIService openAIService, + IRepository userRepo, + IChatService chatService, + ILogger logger, + IRepository chatRepository) { + _openAIService = openAIService; + _chatService = chatService; + _logger = logger; _chatRepository = chatRepository; } - public async Task InsertAsync(Chat chat) + public async Task InsertAsync(CreateChatDTO chatModel) { + var chat = new Chat() + { + Title = chatModel.Title, + UserId = chatModel.UserId, + Requests = new List(), + Responses = new List() + }; + await _chatRepository.InsertAsync(chat); } @@ -30,5 +57,158 @@ public async Task>> GetAllByUserId(string userId) public async Task UpdateAsync(Chat chat) => await _chatRepository.UpdateAsync(chat); + + public async Task CreateChat(string request) + { + try + { + _logger.LogInformation("Attempting to find user"); + //TODO: get the userId through JWT Bearer + //var user = await _userRepo.GetByIdAsync("userId"); + + // Get the user allergies + //var userAllergies = user.Allergies; + var userAllergies = new List { "bananas", "oats", "peanuts" }; + + // Case if the converstaion is new and the chat doesn't exist + var completionResult = await _openAIService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest + { + Messages = new List + { + ChatMessage.FromSystem(Completions.AssistantInstructions + + userAllergies + "." + + Completions.PromptEngineeringPrevention), + //ChatMessage.FromUser(Completions.Suggestion), + //ChatMessage.FromAssistant(Completions.ExampleResponse), + ChatMessage.FromUser(request) + }, + Model = Models.Gpt_3_5_Turbo_0125, + MaxTokens = 5, + N = 1, + }); + + // Creates a new Chat where later interaction will be stored + //var userChat = CreateNewChat(completionResult); + + if (completionResult.Successful) + { + //var response = completionResult.Choices[0].Message.Content; + //UpdateUserChat(userChat, request, response); + return completionResult; + } + } + catch (Exception e) + { + _logger.LogInformation("Something went wrong."); + _logger.LogInformation($"{e.Message}"); + } + + return null; + } + + public async Task UpdateChat(string request, string? chatId) + { + try + { + var userChat = await _chatService.GetByIdAsync(chatId); + + var completionResult = await _openAIService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest + { + Messages = new List + { + ChatMessage.FromUser(request) + }, + Model = Models.Gpt_3_5_Turbo_0125 + }); + + if (completionResult.Successful) + { + _logger.LogInformation("Successfully received a response from the ChatGPT API."); + // workout if info is needed inside the logger + _logger.LogInformation($"{JsonSerializer.Serialize(completionResult)}"); + var response = completionResult.Choices[0].Message.Content; + UpdateUserChat(userChat, request, response); + + return completionResult; + } + } + catch (Exception e) + { + _logger.LogInformation(ExceptionMessages.ChatGPT.ConnectionError); + _logger.LogInformation(e.Message); + } + + return null; + } + + private static bool ChatExists(string chatId, User? user) + => user.Chats.Any(x => x.Id == chatId); + + private Request CreateNewRequest(string message) + => new Request() + { + Message = message, + Timestamp = DateTime.UtcNow, + }; + + private Response CreateNewResponse(string message) + => new Response() + { + Message = message, + Timestamp = DateTime.UtcNow, + }; + + // Creates a new chat using the ID originating from the ChatGPT API + private async Task CreateNewChat(ChatCompletionCreateResponse completionResult, string userId) + { + // send another ChatGPT API Request to config the title. + var title = await GenerateTitle(completionResult.Choices.First().Message.Content); + + var chat = new CreateChatDTO() + { + Id = completionResult.Id, + UserId = userId, + Title = title, + Requests = new List(), + Responses = new List() + }; + + await _chatService.InsertAsync(chat); + } + + private async Task GenerateTitle(string message) + { + try + { + var result = await _openAIService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest + { + Model = Models.Gpt_3_5_Turbo_0125, + Messages = new List + { + ChatMessage.FromUser(message) + }, + MaxTokens = 10 + }); + + if (result.Successful) + { + return result.Choices.First().Message.Content; + } + } + catch (Exception e) + { + _logger.LogInformation(ExceptionMessages.ChatGPT.ConnectionError); + _logger.LogInformation(e.Message); + } + + return null; + } + + private async void UpdateUserChat(Chat? userChat, string? request, string? response) + { + userChat?.Requests.Add(CreateNewRequest(request)); + userChat?.Responses.Add(CreateNewResponse(response)); + await _chatService.UpdateAsync(userChat); + } } } diff --git a/src/server/CookingApp/Services/ChatService/IChatService.cs b/src/server/CookingApp/Services/ChatService/IChatService.cs index bad20511..2c1caaaa 100644 --- a/src/server/CookingApp/Services/ChatService/IChatService.cs +++ b/src/server/CookingApp/Services/ChatService/IChatService.cs @@ -1,8 +1,11 @@ namespace CookingApp.Services.ChatHistory { + using CookingApp.Models.DTOs; + using OpenAI.ObjectModels.ResponseModels; + public interface IChatService { - Task InsertAsync(Chat chat); + Task InsertAsync(CreateChatDTO chat); Task> GetAllChatsAsync(); @@ -11,5 +14,9 @@ public interface IChatService Task GetByIdAsync(string id); Task UpdateAsync(Chat chat); + + Task CreateChat(string request); + + Task UpdateChat(string request, string? chatId); } } diff --git a/src/server/CookingApp/Services/OpenAI/Completions/CompletionService.cs b/src/server/CookingApp/Services/OpenAI/Completions/CompletionService.cs deleted file mode 100644 index af651837..00000000 --- a/src/server/CookingApp/Services/OpenAI/Completions/CompletionService.cs +++ /dev/null @@ -1,154 +0,0 @@ -namespace CookingApp.Services.OpenAI.Completions -{ - using global::OpenAI.Interfaces; - using global::OpenAI.ObjectModels.ResponseModels; - using global::OpenAI.ObjectModels.RequestModels; - using global::OpenAI.ObjectModels; - using CookingApp.Common.CompletionConstants; - using CookingApp.Infrastructure.Interfaces; - using System.Linq; - using CookingApp.Services.ChatHistory; - using MongoDB.Bson.Serialization; - using System.Text.Json; - - /// - /// This class it to assist with the personal needs of the user. - /// After defining their dietary/allergic needs the chat completion - /// will fill them in for the chatbot to take into account. - /// - public class CompletionService : ICompletionService - { - private readonly IRepository _userRepo; - private readonly ILogger _logger; - private readonly IChatService _chatService; - private readonly IOpenAIService _openAIService; - - public CompletionService(IOpenAIService openAIService, - IRepository userRepo, - IChatService chatService, - ILogger logger) - { - _openAIService = openAIService; - _userRepo = userRepo; - _chatService = chatService; - _logger = logger; - } - - public async Task CreateCompletion(string request) - { - _logger.LogInformation("Attempting to find user"); - //TODO: get the userId through JWT Bearer - //TODO: insert try-catch - //var user = await _userRepo.GetByIdAsync("userId"); - - // Get the user allergies - //var userAllergies = user.Allergies; - var userAllergies = new List { "bananas", "oats", "peanuts" }; - - // Case if the converstaion is new and the chat doesn't exist - var completionResult = await _openAIService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest - { - Messages = new List - { - // Creating a prompt for the chatboot to answer a question about cooking/diatery needs. - ChatMessage.FromSystem(Completions.Instructions - + userAllergies + "." - + Completions.PromptEngineeringPrevention), - //ChatMessage.FromUser(Completions.Suggestion), - //ChatMessage.FromAssistant(Completions.ExampleResponse), - ChatMessage.FromUser(request) - }, - Model = Models.Gpt_3_5_Turbo_0125, - MaxTokens = 5, - N = 1, - }); - - // Creates a new Chat where later interaction will be stored - //var userChat = CreateNewChat(completionResult.Id); - - if (completionResult.Successful) - { - //var response = completionResult.Choices[0].Message.Content; - //UpdateUserChat(userChat, request, response); - return completionResult; - } - - return null; - - } - - public async Task UpdateCompletion(string request, string? chatId) - { - try - { - var userChat = await _chatService.GetByIdAsync(chatId); - - var completionResult = await _openAIService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest - { - Messages = new List - { - ChatMessage.FromUser(request) - }, - Model = Models.Gpt_3_5_Turbo_0125 - }); - - if (completionResult.Successful) - { - _logger.LogInformation("Successfully received a response from the ChatGPT API."); - // workout if info is needed inside the logger - _logger.LogInformation($"{JsonSerializer.Serialize(completionResult)}"); - var response = completionResult.Choices[0].Message.Content; - UpdateUserChat(userChat, request, response); - - return completionResult; - } - } - catch (Exception e) - { - _logger.LogInformation("An error occurred while sending a query to ChatGPT API."); - _logger.LogInformation(e.Message); - } - - return null; - } - - private static bool ChatExists(string chatID, User? user) - => user.Chats.Any(x => x.Id == chatID); - - private Request CreateNewRequest(string message) - => new Request() - { - Message = message, - Timestamp = DateTime.UtcNow, - }; - - private Response CreateNewResponse(string message) - => new Response() - { - Message = message, - Timestamp = DateTime.UtcNow, - }; - - // Creates a new chat using the ID originating from the ChatGPT API - private async Task CreateNewChat(string id) - { - var chat = new Chat() - { - Id = id, - Requests = new List(), - Responses = new List() - }; - - await _chatService.InsertAsync(chat); - - return chat; - } - - private async void UpdateUserChat(Chat? userChat, string? request, string? response) - { - userChat?.Requests.Add(CreateNewRequest(request)); - userChat?.Responses.Add(CreateNewResponse(response)); - await _chatService.UpdateAsync(userChat); - } - } -} diff --git a/src/server/CookingApp/Services/OpenAI/Completions/ICompletionService.cs b/src/server/CookingApp/Services/OpenAI/Completions/ICompletionService.cs deleted file mode 100644 index 78d870ad..00000000 --- a/src/server/CookingApp/Services/OpenAI/Completions/ICompletionService.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace CookingApp.Services.OpenAI.Completions -{ - using global::OpenAI.ObjectModels.ResponseModels; - - public interface ICompletionService - { - Task CreateCompletion(string message); - - Task UpdateCompletion(string request, string? chatId = null); - } -}