diff --git a/Examples/Examples/Chat/ChatFromExistingExample.cs b/Examples/Examples/Chat/ChatFromExistingExample.cs index ce123f2..6b77ac7 100644 --- a/Examples/Examples/Chat/ChatFromExistingExample.cs +++ b/Examples/Examples/Chat/ChatFromExistingExample.cs @@ -1,5 +1,6 @@ using System.Text.Json; using MaIN.Core.Hub; +using MaIN.Domain.Exceptions; namespace Examples; @@ -18,8 +19,16 @@ public async Task Start() await result.WithMessage("And about physics?") .CompleteAsync(); - var chatNewContext = await AIHub.Chat().FromExisting(result.GetChatId()); - var messages = chatNewContext.GetChatHistory(); - Console.WriteLine(JsonSerializer.Serialize(messages)); + try + { + var chatNewContext = await AIHub.Chat().FromExisting(result.GetChatId()); + var messages = chatNewContext.GetChatHistory(); + Console.WriteLine(JsonSerializer.Serialize(messages)); + } + catch (ChatNotFoundException ex) + { + Console.WriteLine(ex.PublicErrorMessage); + } + } } \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/ChatContext.cs b/src/MaIN.Core/Hub/Contexts/ChatContext.cs index 8e7dd25..b4a6026 100644 --- a/src/MaIN.Core/Hub/Contexts/ChatContext.cs +++ b/src/MaIN.Core/Hub/Contexts/ChatContext.cs @@ -1,5 +1,6 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions; using MaIN.Domain.Models; using MaIN.Services; using MaIN.Services.Constants; @@ -169,8 +170,9 @@ public async Task CompleteAsync( { if (_chat.Messages.Count == 0) { - throw new InvalidOperationException("Chat has no messages."); //TODO good candidate for domain exception + throw new EmptyChatException(_chat.Id); } + _chat.Messages.Last().Files = _files; if(_preProcess) { @@ -190,7 +192,9 @@ public async Task CompleteAsync( public async Task GetCurrentChat() { if (_chat.Id == null) - throw new InvalidOperationException("Chat has not been created yet. Call CompleteAsync first."); + { + throw new ChatNotInitializedException(); + } return await _chatService.GetById(_chat.Id); } @@ -203,8 +207,10 @@ public async Task> GetAllChats() public async Task DeleteChat() { if (_chat.Id == null) - throw new InvalidOperationException("Chat has not been created yet."); - + { + throw new ChatNotInitializedException(); + } + await _chatService.Delete(_chat.Id); } @@ -227,7 +233,7 @@ public async Task FromExisting(string chatId) var existingChat = await _chatService.GetById(chatId); if (existingChat == null) { - throw new Exception("Chat not found"); + throw new ChatNotFoundException(chatId); } return new ChatContext(_chatService, existingChat); } diff --git a/src/MaIN.Domain/Exceptions/AgentContextNotFoundException.cs b/src/MaIN.Domain/Exceptions/AgentContextNotFoundException.cs new file mode 100644 index 0000000..0126e2e --- /dev/null +++ b/src/MaIN.Domain/Exceptions/AgentContextNotFoundException.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions; + +public class AgentContextNotFoundException(string agentId) : MaINCustomException($"Context of agent with id: '{agentId}' not found.") +{ + public override string PublicErrorMessage => "Agent context not found."; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.NotFound; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/AgentFlowNotFoundException.cs b/src/MaIN.Domain/Exceptions/AgentFlowNotFoundException.cs new file mode 100644 index 0000000..bb27181 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/AgentFlowNotFoundException.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions; + +public class AgentFlowNotFoundException(string flowId) : MaINCustomException($"Agent flow with id: '{flowId}' not found.") +{ + public override string PublicErrorMessage => "Agent flow not found."; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.NotFound; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/AgentNotFoundException.cs b/src/MaIN.Domain/Exceptions/AgentNotFoundException.cs new file mode 100644 index 0000000..7a2556d --- /dev/null +++ b/src/MaIN.Domain/Exceptions/AgentNotFoundException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions; + +public class AgentNotFoundException(string agentId) + : MaINCustomException($"Agent with id: '{agentId}' not found.") +{ + public override string PublicErrorMessage => "Agent not found."; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.NotFound; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/ApiRequestFailedException.cs b/src/MaIN.Domain/Exceptions/ApiRequestFailedException.cs new file mode 100644 index 0000000..903136b --- /dev/null +++ b/src/MaIN.Domain/Exceptions/ApiRequestFailedException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions; + +public class ApiRequestFailedException(HttpStatusCode statusCode, string requestUrl, string httpMethod) + : MaINCustomException($"API request failed with status code: {statusCode}. Request url: {requestUrl}. Http method: {httpMethod}.") +{ + public override string PublicErrorMessage => "An error occurred while processing an external API request"; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.InternalServerError; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/ChatNotFoundException.cs b/src/MaIN.Domain/Exceptions/ChatNotFoundException.cs new file mode 100644 index 0000000..e01d301 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/ChatNotFoundException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions; + +public class ChatNotFoundException(string chatId) + : MaINCustomException($"Chat with id: '{chatId}' not found.") +{ + public override string PublicErrorMessage => "Chat not found."; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.NotFound; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/ChatNotInitializedException.cs b/src/MaIN.Domain/Exceptions/ChatNotInitializedException.cs new file mode 100644 index 0000000..4750eb9 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/ChatNotInitializedException.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions; + +public class ChatNotInitializedException() : MaINCustomException("Chat has not been created yet. Call 'CompleteAsync' operation first.") +{ + public override string PublicErrorMessage => Message; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.Conflict; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/CommandFailedException.cs b/src/MaIN.Domain/Exceptions/CommandFailedException.cs new file mode 100644 index 0000000..25ce01e --- /dev/null +++ b/src/MaIN.Domain/Exceptions/CommandFailedException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions; + +public class CommandFailedException(string commandName) + : MaINCustomException($"{commandName} command execution failed.") +{ + public override string PublicErrorMessage => Message; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.InternalServerError; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/EmptyChatException.cs b/src/MaIN.Domain/Exceptions/EmptyChatException.cs new file mode 100644 index 0000000..5157132 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/EmptyChatException.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions; + +public class EmptyChatException(string chatId) : MaINCustomException($"Chat with id: '{chatId}' is empty. Complete operation is impossible.") +{ + public override string PublicErrorMessage => "Complete operation is impossible, because chat has no message."; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.Conflict; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/MaINCustomException.cs b/src/MaIN.Domain/Exceptions/MaINCustomException.cs new file mode 100644 index 0000000..2530f8b --- /dev/null +++ b/src/MaIN.Domain/Exceptions/MaINCustomException.cs @@ -0,0 +1,20 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace MaIN.Domain.Exceptions; + +public abstract class MaINCustomException(string message) : Exception(message) +{ + public string ErrorCode => GenerateErrorCode(); + public string LogMessage { get; private set; } = message; + public abstract string PublicErrorMessage { get; } + public abstract HttpStatusCode HttpStatusCode { get; } + + private string GenerateErrorCode() + { + var typeName = GetType().Name; + var snakeCaseTypeName = Regex.Replace(typeName, "(? Message; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.NotFound; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/ModelNotSupportedException.cs b/src/MaIN.Domain/Exceptions/ModelNotSupportedException.cs new file mode 100644 index 0000000..4ac460e --- /dev/null +++ b/src/MaIN.Domain/Exceptions/ModelNotSupportedException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions; + +public class ModelNotSupportedException(string? modelName) + : MaINCustomException($"Given model {modelName ?? string.Empty} is not supported.") +{ + public override string PublicErrorMessage => Message; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.BadRequest; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Models/SupportedModels.cs b/src/MaIN.Domain/Models/SupportedModels.cs index 7a34326..1714278 100644 --- a/src/MaIN.Domain/Models/SupportedModels.cs +++ b/src/MaIN.Domain/Models/SupportedModels.cs @@ -1,3 +1,5 @@ +using MaIN.Domain.Exceptions; + namespace MaIN.Domain.Models; public class Model @@ -207,8 +209,7 @@ public static Model GetModel(string path, string? name) StringComparison.InvariantCultureIgnoreCase)); if (model is null) { - //todo support domain specific exceptions - throw new Exception($"Model {name} is not supported"); + throw new ModelNotSupportedException(name); } if (File.Exists(Path.Combine(path, model.FileName))) @@ -216,7 +217,7 @@ public static Model GetModel(string path, string? name) return model; } - throw new Exception($"Model {name} is not downloaded"); + throw new ModelNotDownloadedException(name); } public static Model? GetModelByFileName(string path, string fileName) @@ -224,8 +225,7 @@ public static Model GetModel(string path, string? name) var isPresent = Models.Exists(x => x.FileName == fileName); if (!isPresent) { - //todo support domain specific exceptions - Console.WriteLine($"Model {fileName} is not supported"); + Console.WriteLine($"{new ModelNotSupportedException(fileName).PublicErrorMessage}"); return null; } @@ -234,7 +234,7 @@ public static Model GetModel(string path, string? name) return Models.First(x => x.FileName == fileName); } - throw new Exception($"Model {fileName} is not downloaded"); + throw new ModelNotDownloadedException(fileName); } public static void AddModel(string model, string path, string? mmProject = null) @@ -257,8 +257,7 @@ public static Model GetModel(string modelName) StringComparison.InvariantCultureIgnoreCase)); if (model is null) { - //todo support domain specific exceptions - throw new NotSupportedException($"Model {modelName} is not supported"); + throw new ModelNotSupportedException(modelName); } return model; diff --git a/src/MaIN.Services/Services/AgentFlowService.cs b/src/MaIN.Services/Services/AgentFlowService.cs index 762a166..1ab7e8c 100644 --- a/src/MaIN.Services/Services/AgentFlowService.cs +++ b/src/MaIN.Services/Services/AgentFlowService.cs @@ -1,4 +1,5 @@ using MaIN.Domain.Entities.Agents.AgentSource; +using MaIN.Domain.Exceptions; using MaIN.Infrastructure.Models; using MaIN.Infrastructure.Repositories.Abstract; using MaIN.Services.Mappers; @@ -11,8 +12,10 @@ public class AgentFlowService(IAgentFlowRepository flowRepository, IAgentService public async Task GetFlowById(string id) { var flow = await flowRepository.GetFlowById(id); - if(flow is null) - throw new Exception("Flow not found"); + if (flow is null) + { + throw new AgentFlowNotFoundException(id); + } return flow.ToDomain(); } diff --git a/src/MaIN.Services/Services/AgentService.cs b/src/MaIN.Services/Services/AgentService.cs index d0c0827..6c991b5 100644 --- a/src/MaIN.Services/Services/AgentService.cs +++ b/src/MaIN.Services/Services/AgentService.cs @@ -1,6 +1,7 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Entities.Agents; +using MaIN.Domain.Exceptions; using MaIN.Infrastructure.Repositories.Abstract; using MaIN.Services.Constants; using MaIN.Services.Mappers; @@ -28,10 +29,15 @@ public class AgentService( public async Task Process(Chat chat, string agentId, bool translatePrompt = false) { var agent = await agentRepository.GetAgentById(agentId); - if (agent == null) - throw new ArgumentException("Agent not found."); //TODO candidate for NotFound domain exception - if (agent.Context == null) - throw new ArgumentException("Agent context not found."); + if (agent == null) + { + throw new AgentNotFoundException(agentId); + } + + if (agent.Context == null) + { + throw new AgentContextNotFoundException(agentId); + } await notificationService.DispatchNotification( NotificationMessageBuilder.ProcessingStarted(agentId, agent.CurrentBehaviour), "ReceiveAgentUpdate"); @@ -115,7 +121,9 @@ public async Task GetChatByAgent(string agentId) { var agent = await agentRepository.GetAgentById(agentId); if (agent == null) - throw new Exception("Agent not found."); //TODO good candidate for custom exception + { + throw new AgentNotFoundException(agentId); + } var chat = await chatRepository.GetChatById(agent.ChatId); return chat!.ToDomain(); @@ -125,7 +133,9 @@ public async Task Restart(string agentId) { var agent = await agentRepository.GetAgentById(agentId); if (agent == null) - throw new Exception("Agent not found."); //TODO good candidate for custom exception + { + throw new AgentNotFoundException(agentId); + } var chat = (await chatRepository.GetChatById(agent.ChatId))!.ToDomain(); var llmService = llmServiceFactory.CreateService(agent.Backend ?? maInSettings.BackendType); diff --git a/src/MaIN.Services/Services/ChatService.cs b/src/MaIN.Services/Services/ChatService.cs index 20079b2..c40cbae 100644 --- a/src/MaIN.Services/Services/ChatService.cs +++ b/src/MaIN.Services/Services/ChatService.cs @@ -1,5 +1,6 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions; using MaIN.Domain.Models; using MaIN.Infrastructure.Repositories.Abstract; using MaIN.Services.Mappers; @@ -89,7 +90,11 @@ public async Task Delete(string id) public async Task GetById(string id) { var chatDocument = await chatProvider.GetChatById(id); - if(chatDocument == null) throw new Exception("Chat not found"); //TODO good candidate for custom exception + if (chatDocument == null) + { + throw new ChatNotFoundException(id); + } + return chatDocument.ToDomain(); } diff --git a/src/MaIN.Services/Services/DataSourceProvider.cs b/src/MaIN.Services/Services/DataSourceProvider.cs index e92c5b3..b9bbd79 100644 --- a/src/MaIN.Services/Services/DataSourceProvider.cs +++ b/src/MaIN.Services/Services/DataSourceProvider.cs @@ -2,6 +2,7 @@ using System.Text; using System.Text.Json; using MaIN.Domain.Entities.Agents.AgentSource; +using MaIN.Domain.Exceptions; using MaIN.Services.Services.Abstract; using MaIN.Services.Utils; using MongoDB.Bson; @@ -76,8 +77,10 @@ public async Task FetchApiData(object? details, string? filter, var result = await httpClient.SendAsync(request); if (!result.IsSuccessStatusCode) { - throw new Exception( - $"API request failed with status code: {result.StatusCode}"); //TODO candidate for domain exception + throw new ApiRequestFailedException( + result.StatusCode, + apiDetails?.Url + apiDetails?.Query, + apiDetails?.Method ?? string.Empty); } var data = await result.Content.ReadAsStringAsync(); diff --git a/src/MaIN.Services/Services/Steps/AnswerStepHandler.cs b/src/MaIN.Services/Services/Steps/AnswerStepHandler.cs index a2fc7f8..18dc4aa 100644 --- a/src/MaIN.Services/Services/Steps/AnswerStepHandler.cs +++ b/src/MaIN.Services/Services/Steps/AnswerStepHandler.cs @@ -1,6 +1,7 @@ using System.Text.RegularExpressions; using DocumentFormat.OpenXml.Wordprocessing; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; using MaIN.Services.Services.Models.Commands; @@ -24,9 +25,10 @@ public async Task Handle(StepContext context) }; var answerResponse = await commandDispatcher.DispatchAsync(answerCommand); - if (answerResponse == null) - throw new Exception("Answer command failed"); //TODO proper candidate for custom exception - + if (answerResponse == null) + { + throw new CommandFailedException(answerCommand.CommandName); + } var filterVal = GetFilter(answerResponse.Content); if (!string.IsNullOrEmpty(filterVal)) diff --git a/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs b/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs index 6c70818..f8745d3 100644 --- a/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs +++ b/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs @@ -1,5 +1,6 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions; using MaIN.Services.Mappers; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; @@ -35,7 +36,7 @@ public async Task Handle(StepContext context) var response = await commandDispatcher.DispatchAsync(fetchCommand); if (response == null) { - throw new InvalidOperationException("Data fetch command failed"); //TODO proper candidate for custom exception + throw new CommandFailedException(fetchCommand.CommandName); } if (context.StepName == "FETCH_DATA*") diff --git a/src/MaIN.Services/Services/Steps/McpStepHandler.cs b/src/MaIN.Services/Services/Steps/McpStepHandler.cs index b3cb686..9c501a7 100644 --- a/src/MaIN.Services/Services/Steps/McpStepHandler.cs +++ b/src/MaIN.Services/Services/Steps/McpStepHandler.cs @@ -1,6 +1,7 @@ using System.Text.RegularExpressions; using DocumentFormat.OpenXml.Wordprocessing; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; using MaIN.Services.Services.Models.Commands; @@ -29,7 +30,7 @@ public async Task Handle(StepContext context) var mcpResponse = await commandDispatcher.DispatchAsync(mcpCommand); if (mcpResponse == null) { - throw new Exception("MCP command failed"); //TODO proper candidate for custom exception + throw new CommandFailedException(mcpCommand.CommandName); } var filterVal = GetFilter(mcpResponse.Content);