diff --git a/Examples/Examples.SimpleConsole/Program.cs b/Examples/Examples.SimpleConsole/Program.cs index 3ace6527..8662cdcd 100644 --- a/Examples/Examples.SimpleConsole/Program.cs +++ b/Examples/Examples.SimpleConsole/Program.cs @@ -1,7 +1,5 @@ using MaIN.Core; using MaIN.Core.Hub; -using MaIN.Domain.Entities; -using OpenAI.Models; MaINBootstrapper.Initialize(); diff --git a/Examples/Examples/Agents/AgentExample.cs b/Examples/Examples/Agents/AgentExample.cs index 2f9764a5..3678efad 100644 --- a/Examples/Examples/Agents/AgentExample.cs +++ b/Examples/Examples/Agents/AgentExample.cs @@ -1,6 +1,4 @@ using MaIN.Core.Hub; -using MaIN.Core.Hub.Utils; -using MaIN.Domain.Entities.Agents.Knowledge; namespace Examples.Agents; diff --git a/Examples/Examples/Agents/AgentWithApiDataSourceExample.cs b/Examples/Examples/Agents/AgentWithApiDataSourceExample.cs index c94ae8cb..b2a05fb6 100644 --- a/Examples/Examples/Agents/AgentWithApiDataSourceExample.cs +++ b/Examples/Examples/Agents/AgentWithApiDataSourceExample.cs @@ -1,7 +1,6 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; using MaIN.Domain.Entities.Agents.AgentSource; -using MaIN.Services.Services.Models.Commands; namespace Examples.Agents; diff --git a/Examples/Examples/Agents/AgentWithKnowledgeFileExample.cs b/Examples/Examples/Agents/AgentWithKnowledgeFileExample.cs index 9bb72aa4..ffd55025 100644 --- a/Examples/Examples/Agents/AgentWithKnowledgeFileExample.cs +++ b/Examples/Examples/Agents/AgentWithKnowledgeFileExample.cs @@ -1,8 +1,5 @@ -using Examples.Utils; using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; -using MaIN.Domain.Entities.Agents.AgentSource; -using Microsoft.Identity.Client; namespace Examples.Agents; diff --git a/Examples/Examples/Agents/AgentWithKnowledgeWebExample.cs b/Examples/Examples/Agents/AgentWithKnowledgeWebExample.cs index 29027c6b..8844e061 100644 --- a/Examples/Examples/Agents/AgentWithKnowledgeWebExample.cs +++ b/Examples/Examples/Agents/AgentWithKnowledgeWebExample.cs @@ -1,9 +1,6 @@ -using Examples.Utils; using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; using MaIN.Domain.Entities; -using MaIN.Domain.Entities.Agents.AgentSource; -using Microsoft.Identity.Client; namespace Examples.Agents; diff --git a/Examples/Examples/Agents/AgentWithWebDataSourceOpenAiExample.cs b/Examples/Examples/Agents/AgentWithWebDataSourceOpenAiExample.cs index 8d8e7769..a1d0b585 100644 --- a/Examples/Examples/Agents/AgentWithWebDataSourceOpenAiExample.cs +++ b/Examples/Examples/Agents/AgentWithWebDataSourceOpenAiExample.cs @@ -1,7 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; -using MaIN.Domain.Configuration; using MaIN.Domain.Entities.Agents.AgentSource; namespace Examples.Agents; diff --git a/Examples/Examples/Agents/Flows/AgentsComposedAsFlowExample.cs b/Examples/Examples/Agents/Flows/AgentsComposedAsFlowExample.cs index b92b8bc6..5ba8f6ab 100644 --- a/Examples/Examples/Agents/Flows/AgentsComposedAsFlowExample.cs +++ b/Examples/Examples/Agents/Flows/AgentsComposedAsFlowExample.cs @@ -1,7 +1,7 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; -namespace Examples.Agents; +namespace Examples.Agents.Flows; public class AgentsComposedAsFlowExample : IExample { diff --git a/Examples/Examples/Agents/MultiBackendAgentsWithRedirectExample.cs b/Examples/Examples/Agents/MultiBackendAgentsWithRedirectExample.cs index ba4eeeab..07b4d8b8 100644 --- a/Examples/Examples/Agents/MultiBackendAgentsWithRedirectExample.cs +++ b/Examples/Examples/Agents/MultiBackendAgentsWithRedirectExample.cs @@ -26,9 +26,9 @@ You need to use a lot of it. Imagine you are the voice of youth. """; var contextSecond = await AIHub.Agent() - .WithBackend(BackendType.OpenAi) .WithModel("gpt-4o") .WithInitialPrompt(systemPromptSecond) + .WithBackend(BackendType.OpenAi) .CreateAsync(interactiveResponse: true); var context = await AIHub.Agent() diff --git a/Examples/Examples/Chat/ChatCustomGrammarExample.cs b/Examples/Examples/Chat/ChatCustomGrammarExample.cs index ee812e03..ec58c25c 100644 --- a/Examples/Examples/Chat/ChatCustomGrammarExample.cs +++ b/Examples/Examples/Chat/ChatCustomGrammarExample.cs @@ -21,12 +21,12 @@ public async Task Start() """, GrammarFormat.GBNF); await AIHub.Chat() + .WithModel("gemma2:2b") + .WithMessage("Generate random person") .WithInferenceParams(new InferenceParams { Grammar = personGrammar }) - .WithModel("gemma2:2b") - .WithMessage("Generate random person") .CompleteAsync(interactive: true); } } \ No newline at end of file diff --git a/Examples/Examples/Chat/ChatExampleAnthropic.cs b/Examples/Examples/Chat/ChatExampleAnthropic.cs index 1e2315e6..b24baf59 100644 --- a/Examples/Examples/Chat/ChatExampleAnthropic.cs +++ b/Examples/Examples/Chat/ChatExampleAnthropic.cs @@ -1,6 +1,5 @@ using Examples.Utils; using MaIN.Core.Hub; -using MaIN.Domain.Configuration; namespace Examples.Chat; diff --git a/Examples/Examples/Chat/ChatExampleToolsSimple.cs b/Examples/Examples/Chat/ChatExampleToolsSimple.cs index 9e0226d1..d3a6e375 100644 --- a/Examples/Examples/Chat/ChatExampleToolsSimple.cs +++ b/Examples/Examples/Chat/ChatExampleToolsSimple.cs @@ -1,7 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; -using MaIN.Domain.Configuration; namespace Examples.Chat; diff --git a/Examples/Examples/Chat/ChatExampleXai.cs b/Examples/Examples/Chat/ChatExampleXai.cs index 9f0e7581..2f8df21c 100644 --- a/Examples/Examples/Chat/ChatExampleXai.cs +++ b/Examples/Examples/Chat/ChatExampleXai.cs @@ -1,7 +1,7 @@ using Examples.Utils; using MaIN.Core.Hub; -namespace Examples; +namespace Examples.Chat; public class ChatExampleXai : IExample { diff --git a/Examples/Examples/Chat/ChatFromExistingExample.cs b/Examples/Examples/Chat/ChatFromExistingExample.cs index 774638cf..adeba5a0 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.Chats; namespace Examples.Chat; @@ -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/Examples/Examples/Chat/ChatGrammarExampleGemini.cs b/Examples/Examples/Chat/ChatGrammarExampleGemini.cs index ba1a9f42..43ee0ebb 100644 --- a/Examples/Examples/Chat/ChatGrammarExampleGemini.cs +++ b/Examples/Examples/Chat/ChatGrammarExampleGemini.cs @@ -38,12 +38,12 @@ public async Task Start() """; await AIHub.Chat() - .WithInferenceParams(new InferenceParams - { - Grammar = new Grammar(grammarValue, GrammarFormat.JSONSchema) - }) - .WithModel("gemini-2.5-flash") - .WithMessage("Generate random person") - .CompleteAsync(interactive: true); + .WithModel("gemini-2.5-flash") + .WithMessage("Generate random person") + .WithInferenceParams(new InferenceParams + { + Grammar = new Grammar(grammarValue, GrammarFormat.JSONSchema) + }) + .CompleteAsync(interactive: true); } } \ No newline at end of file diff --git a/Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs b/Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs index 1edef812..3f3529c3 100644 --- a/Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs +++ b/Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs @@ -11,8 +11,8 @@ public async Task Start() OpenAiExample.Setup(); // We need to provide OpenAi API key var result = await AIHub.Chat() - .EnableVisual() .WithModel("dall-e-3") + .EnableVisual() .WithMessage("Generate rock style cow playing guitar") .CompleteAsync(); diff --git a/Examples/Examples/IExample.cs b/Examples/Examples/IExample.cs index 6144d16c..25357fa9 100644 --- a/Examples/Examples/IExample.cs +++ b/Examples/Examples/IExample.cs @@ -1,3 +1,5 @@ +namespace Examples; + public interface IExample { Task Start(); diff --git a/Examples/Examples/Mcp/AgentWithKnowledgeMcpExample.cs b/Examples/Examples/Mcp/AgentWithKnowledgeMcpExample.cs index b836b3f6..a9a3c544 100644 --- a/Examples/Examples/Mcp/AgentWithKnowledgeMcpExample.cs +++ b/Examples/Examples/Mcp/AgentWithKnowledgeMcpExample.cs @@ -14,8 +14,8 @@ public async Task Start() AIHub.Extensions.DisableLLamaLogs(); var context = await AIHub.Agent() - .WithBackend(BackendType.OpenAi) .WithModel("gpt-4.1-mini") + .WithBackend(BackendType.OpenAi) .WithKnowledge(KnowledgeBuilder.Instance .AddMcp(new MaIN.Domain.Entities.Mcp { diff --git a/Examples/Examples/Mcp/McpAgentsExample.cs b/Examples/Examples/Mcp/McpAgentsExample.cs index 155d2312..44ee7f81 100644 --- a/Examples/Examples/Mcp/McpAgentsExample.cs +++ b/Examples/Examples/Mcp/McpAgentsExample.cs @@ -2,7 +2,7 @@ using MaIN.Core.Hub.Utils; using MaIN.Domain.Configuration; -namespace Examples; +namespace Examples.Mcp; public class McpAgentsExample : IExample { @@ -17,6 +17,7 @@ public async Task Start() .CreateAsync(interactiveResponse: true); var context = await AIHub.Agent() + .WithModel("gpt-4o-mini") .WithBackend(BackendType.OpenAi) .WithMcpConfig(new MaIN.Domain.Entities.Mcp { @@ -29,7 +30,6 @@ public async Task Start() Command = "docker", Model = "gpt-4o-mini" }) - .WithModel("gpt-4o-mini") .WithSteps(StepBuilder.Instance .Mcp() .Redirect(agentId: contextSecond.GetAgentId()) diff --git a/Examples/Examples/Mcp/McpExample.cs b/Examples/Examples/Mcp/McpExample.cs index 75b3b6e6..e3b97a24 100644 --- a/Examples/Examples/Mcp/McpExample.cs +++ b/Examples/Examples/Mcp/McpExample.cs @@ -2,7 +2,7 @@ using MaIN.Core.Hub; using MaIN.Domain.Configuration; -namespace Examples; +namespace Examples.Mcp; public class McpExample : IExample { diff --git a/Examples/Examples/Program.cs b/Examples/Examples/Program.cs index 80cc3d62..58946225 100644 --- a/Examples/Examples/Program.cs +++ b/Examples/Examples/Program.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; - var Banner = @" ███╗ ███╗ █████╗ ██╗███╗ ██╗ ███████╗██╗ ██╗ █████╗ ███╗ ███╗██████╗ ██╗ ███████╗███████╗ ████╗ ████║██╔══██╗██║████╗ ██║ ██╔════╝╚██╗██╔╝██╔══██╗████╗ ████║██╔══██╗██║ ██╔════╝██╔════╝ @@ -121,7 +120,20 @@ async Task RunSelectedExample(IServiceProvider serviceProvider) Console.ResetColor(); var selectedExample = examples[selection - 1].Instance; - await selectedExample.Start(); + try + { + await selectedExample.Start(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("╔════════════════════════════════════════════════════════════════════╗"); + Console.WriteLine("║ Error ║"); + Console.WriteLine("╚════════════════════════════════════════════════════════════════════╝"); + Console.ResetColor(); + + Console.WriteLine(ex.Message); + } } else { @@ -134,50 +146,53 @@ async Task RunSelectedExample(IServiceProvider serviceProvider) } -public class ExampleRegistry(IServiceProvider serviceProvider) +namespace Examples { - public List<(string Name, IExample Instance)> GetAvailableExamples() + public class ExampleRegistry(IServiceProvider serviceProvider) { - return new List<(string, IExample)> + public List<(string Name, IExample Instance)> GetAvailableExamples() { - ("\u25a0 Basic Chat", serviceProvider.GetRequiredService()), - ("\u25a0 Chat with Files", serviceProvider.GetRequiredService()), - ("\u25a0 Chat with custom grammar", serviceProvider.GetRequiredService()), - ("\u25a0 Chat with Files from stream", serviceProvider.GetRequiredService()), - ("\u25a0 Chat with Vision", serviceProvider.GetRequiredService()), - ("\u25a0 Chat with Tools (simple)", serviceProvider.GetRequiredService()), - ("\u25a0 Chat with Image Generation", serviceProvider.GetRequiredService()), - ("\u25a0 Chat from Existing", serviceProvider.GetRequiredService()), - ("\u25a0 Chat with reasoning", serviceProvider.GetRequiredService()), - ("\u25a0 Basic Agent", serviceProvider.GetRequiredService()), - ("\u25a0 Conversation Agent", serviceProvider.GetRequiredService()), - ("\u25a0 Agent with Redirect", serviceProvider.GetRequiredService()), - ("\u25a0 Agent with Redirect (Multi backends)", serviceProvider.GetRequiredService()), - ("\u25a0 Agent with Redirect Image", serviceProvider.GetRequiredService()), - ("\u25a0 Agent with Become", serviceProvider.GetRequiredService()), - ("\u25a0 Agent with Tools (advanced)", serviceProvider.GetRequiredService()), - ("\u25a0 Agent with Knowledge", serviceProvider.GetRequiredService()), - ("\u25a0 Agent with Web Knowledge", serviceProvider.GetRequiredService()), - ("\u25a0 Agent with Mcp Knowledge", serviceProvider.GetRequiredService()), - ("\u25a0 Agent with API Data Source", serviceProvider.GetRequiredService()), - ("\u25a0 Agents Talking to Each Other", serviceProvider.GetRequiredService()), - ("\u25a0 Agents Composed as Flow", serviceProvider.GetRequiredService()), - ("\u25a0 Agents Flow Loaded", serviceProvider.GetRequiredService()), - ("\u25a0 OpenAi Chat", serviceProvider.GetRequiredService()), - ("\u25a0 OpenAi Chat with image", serviceProvider.GetRequiredService()), - ("\u25a0 OpenAi Agent with Web Data Source", serviceProvider.GetRequiredService()), - ("\u25a0 Gemini Chat", serviceProvider.GetRequiredService()), - ("\u25a0 Gemini Chat with grammar", serviceProvider.GetRequiredService()), - ("\u25a0 Gemini Chat with image", serviceProvider.GetRequiredService()), - ("\u25a0 Gemini Chat with files", serviceProvider.GetRequiredService()), - ("\u25a0 DeepSeek Chat with reasoning", serviceProvider.GetRequiredService()), - ("\u25a0 GroqCloud Chat", serviceProvider.GetRequiredService()), - ("\u25a0 Anthropic Chat", serviceProvider.GetRequiredService()), - ("\u25a0 xAI Chat", serviceProvider.GetRequiredService()), - ("\u25a0 McpClient example", serviceProvider.GetRequiredService()), - ("\u25a0 McpAgent example", serviceProvider.GetRequiredService()), - ("\u25a0 Chat with TTS example", serviceProvider.GetRequiredService()), - ("\u25a0 McpAgent example", serviceProvider.GetRequiredService()) - }; + return + [ + ("\u25a0 Basic Chat", serviceProvider.GetRequiredService()), + ("\u25a0 Chat with Files", serviceProvider.GetRequiredService()), + ("\u25a0 Chat with custom grammar", serviceProvider.GetRequiredService()), + ("\u25a0 Chat with Files from stream", serviceProvider.GetRequiredService()), + ("\u25a0 Chat with Vision", serviceProvider.GetRequiredService()), + ("\u25a0 Chat with Tools (simple)", serviceProvider.GetRequiredService()), + ("\u25a0 Chat with Image Generation", serviceProvider.GetRequiredService()), + ("\u25a0 Chat from Existing", serviceProvider.GetRequiredService()), + ("\u25a0 Chat with reasoning", serviceProvider.GetRequiredService()), + ("\u25a0 Basic Agent", serviceProvider.GetRequiredService()), + ("\u25a0 Conversation Agent", serviceProvider.GetRequiredService()), + ("\u25a0 Agent with Redirect", serviceProvider.GetRequiredService()), + ("\u25a0 Agent with Redirect (Multi backends)", serviceProvider.GetRequiredService()), + ("\u25a0 Agent with Redirect Image", serviceProvider.GetRequiredService()), + ("\u25a0 Agent with Become", serviceProvider.GetRequiredService()), + ("\u25a0 Agent with Tools (advanced)", serviceProvider.GetRequiredService()), + ("\u25a0 Agent with Knowledge", serviceProvider.GetRequiredService()), + ("\u25a0 Agent with Web Knowledge", serviceProvider.GetRequiredService()), + ("\u25a0 Agent with Mcp Knowledge", serviceProvider.GetRequiredService()), + ("\u25a0 Agent with API Data Source", serviceProvider.GetRequiredService()), + ("\u25a0 Agents Talking to Each Other", serviceProvider.GetRequiredService()), + ("\u25a0 Agents Composed as Flow", serviceProvider.GetRequiredService()), + ("\u25a0 Agents Flow Loaded", serviceProvider.GetRequiredService()), + ("\u25a0 OpenAi Chat", serviceProvider.GetRequiredService()), + ("\u25a0 OpenAi Chat with image", serviceProvider.GetRequiredService()), + ("\u25a0 OpenAi Agent with Web Data Source", serviceProvider.GetRequiredService()), + ("\u25a0 Gemini Chat", serviceProvider.GetRequiredService()), + ("\u25a0 Gemini Chat with grammar", serviceProvider.GetRequiredService()), + ("\u25a0 Gemini Chat with image", serviceProvider.GetRequiredService()), + ("\u25a0 Gemini Chat with files", serviceProvider.GetRequiredService()), + ("\u25a0 DeepSeek Chat with reasoning", serviceProvider.GetRequiredService()), + ("\u25a0 GroqCloud Chat", serviceProvider.GetRequiredService()), + ("\u25a0 Anthropic Chat", serviceProvider.GetRequiredService()), + ("\u25a0 xAI Chat", serviceProvider.GetRequiredService()), + ("\u25a0 McpClient example", serviceProvider.GetRequiredService()), + ("\u25a0 McpAgent example", serviceProvider.GetRequiredService()), + ("\u25a0 Chat with TTS example", serviceProvider.GetRequiredService()), + ("\u25a0 McpAgent example", serviceProvider.GetRequiredService()) + ]; + } } } \ No newline at end of file diff --git a/Examples/Examples/Utils/Tools.cs b/Examples/Examples/Utils/Tools.cs index 166413e7..89033ae3 100644 --- a/Examples/Examples/Utils/Tools.cs +++ b/Examples/Examples/Utils/Tools.cs @@ -1,5 +1,3 @@ -using System.Text.Json; - namespace Examples.Utils; public static class Tools diff --git a/MaIN.Core.IntegrationTests/ChatTests.cs b/MaIN.Core.IntegrationTests/ChatTests.cs index 89952c05..f0b78969 100644 --- a/MaIN.Core.IntegrationTests/ChatTests.cs +++ b/MaIN.Core.IntegrationTests/ChatTests.cs @@ -64,11 +64,12 @@ public async Task Should_AnswerGameFromImage_ChatWithVision() var result = await AIHub.Chat() .WithModel("llama3.2:3b") + .WithMessage("What is the title of game?") + .WithMemoryParams(new MemoryParams { AnswerTokens = 1000 }) - .WithMessage("What is the title of game?") .WithFiles(images) .CompleteAsync(); diff --git a/MaIN.Core.IntegrationTests/IntegrationTestBase.cs b/MaIN.Core.IntegrationTests/IntegrationTestBase.cs index 19e76807..bfbcaed9 100644 --- a/MaIN.Core.IntegrationTests/IntegrationTestBase.cs +++ b/MaIN.Core.IntegrationTests/IntegrationTestBase.cs @@ -1,9 +1,5 @@ using System.Net.Sockets; -using MaIN.Services; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/src/MaIN.Core.UnitTests/AgentContextTests.cs b/src/MaIN.Core.UnitTests/AgentContextTests.cs index a31712ce..18694336 100644 --- a/src/MaIN.Core.UnitTests/AgentContextTests.cs +++ b/src/MaIN.Core.UnitTests/AgentContextTests.cs @@ -2,7 +2,6 @@ using MaIN.Domain.Entities; using MaIN.Domain.Entities.Agents; using MaIN.Domain.Entities.Agents.Knowledge; -using MaIN.Services.Dtos; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; using Moq; diff --git a/src/MaIN.Core.UnitTests/ChatContextTests.cs b/src/MaIN.Core.UnitTests/ChatContextTests.cs index fc91b895..6da951c6 100644 --- a/src/MaIN.Core.UnitTests/ChatContextTests.cs +++ b/src/MaIN.Core.UnitTests/ChatContextTests.cs @@ -1,6 +1,5 @@ using MaIN.Core.Hub.Contexts; using MaIN.Domain.Entities; -using MaIN.Services.Dtos; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; using Moq; diff --git a/src/MaIN.Core/Hub/AiHub.cs b/src/MaIN.Core/Hub/AiHub.cs index 30200c5d..888946f3 100644 --- a/src/MaIN.Core/Hub/AiHub.cs +++ b/src/MaIN.Core/Hub/AiHub.cs @@ -2,6 +2,7 @@ using MaIN.Core.Hub.Contexts; using MaIN.Core.Interfaces; using MaIN.Domain.Configuration; +using MaIN.Domain.Exceptions; using MaIN.Services.Services.Abstract; namespace MaIN.Core.Hub; @@ -23,10 +24,9 @@ internal static void Initialize(IAIHubServices services, private static IAIHubServices Services => _services ?? - throw new InvalidOperationException( - "AIHub has not been initialized. Make sure to call AddAIHub() in your service configuration."); + throw new AIHubNotInitializedException(); - public static ModelContext Model() => new ModelContext(_settings, _httpClientFactory); + public static ModelContext Model() => new(_settings, _httpClientFactory); public static ChatContext Chat() => new(Services.ChatService); public static AgentContext Agent() => new(Services.AgentService); public static FlowContext Flow() => new(Services.FlowService, Services.AgentService); diff --git a/src/MaIN.Core/Hub/Contexts/AgentContext.cs b/src/MaIN.Core/Hub/Contexts/AgentContext.cs index 79341b10..aa313622 100644 --- a/src/MaIN.Core/Hub/Contexts/AgentContext.cs +++ b/src/MaIN.Core/Hub/Contexts/AgentContext.cs @@ -1,3 +1,4 @@ +using MaIN.Core.Hub.Contexts.Interfaces.AgentContext; using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Entities.Agents; @@ -8,11 +9,12 @@ using MaIN.Core.Hub.Utils; using MaIN.Domain.Entities.Agents.Knowledge; using MaIN.Domain.Entities.Tools; +using MaIN.Domain.Exceptions.Agents; using MaIN.Services.Constants; namespace MaIN.Core.Hub.Contexts; -public sealed class AgentContext +public sealed class AgentContext : IAgentBuilderEntryPoint, IAgentConfigurationBuilder, IAgentContextExecutor { private readonly IAgentService _agentService; private InferenceParams? _inferenceParams; @@ -48,31 +50,69 @@ internal AgentContext(IAgentService agentService, Agent existingAgent) _agent = existingAgent; } - public AgentContext WithId(string id) + // --- IAgentActions --- + public string GetAgentId() => _agent.Id; + public Agent GetAgent() => _agent; + public Knowledge? GetKnowledge() => _knowledge; + public async Task GetChat() => await _agentService.GetChatByAgent(_agent.Id); + public async Task RestartChat() => await _agentService.Restart(_agent.Id); + public async Task> GetAllAgents() => await _agentService.GetAgents(); + public async Task GetAgentById(string id) => await _agentService.GetAgentById(id); + public async Task Delete() => await _agentService.DeleteAgent(_agent.Id); + public async Task Exists() => await _agentService.AgentExists(_agent.Id); + + + public IAgentConfigurationBuilder WithModel(string model) { - _agent.Id = id; + _agent.Model = model; return this; } + + public IAgentConfigurationBuilder WithCustomModel(string model, string path, string? mmProject = null) + { + KnownModels.AddModel(model, path, mmProject); + _agent.Model = model; + return this; + } + + public async Task FromExisting(string agentId) + { + var existingAgent = await _agentService.GetAgentById(agentId); + if (existingAgent == null) + { + throw new AgentNotFoundException(agentId); + } + + var context = new AgentContext(_agentService, existingAgent); + context.LoadExistingKnowledgeIfExists(); + return context; + } - public string GetAgentId() => _agent.Id; - - public Agent GetAgent() => _agent; - - public Knowledge? GetKnowledge() => _knowledge; + public IAgentConfigurationBuilder WithInitialPrompt(string prompt) + { + _agent.Context.Instruction = prompt; + return this; + } - public AgentContext WithOrder(int order) + public IAgentConfigurationBuilder WithId(string id) + { + _agent.Id = id; + return this; + } + + public IAgentConfigurationBuilder WithOrder(int order) { _agent.Order = order; return this; } - public AgentContext DisableCache() + public IAgentConfigurationBuilder DisableCache() { _disableCache = true; return this; } - public AgentContext WithSource(IAgentSource source, AgentSourceType type) + public IAgentConfigurationBuilder WithSource(IAgentSource source, AgentSourceType type) { _agent.Context.Source = new AgentSource() { @@ -82,25 +122,19 @@ public AgentContext WithSource(IAgentSource source, AgentSourceType type) return this; } - public AgentContext WithName(string name) + public IAgentConfigurationBuilder WithName(string name) { _agent.Name = name; return this; } - public AgentContext WithBackend(BackendType backendType) + public IAgentConfigurationBuilder WithBackend(BackendType backendType) { _agent.Backend = backendType; return this; } - public AgentContext WithModel(string model) - { - _agent.Model = model; - return this; - } - - public AgentContext WithMcpConfig(Mcp mcpConfig) + public IAgentConfigurationBuilder WithMcpConfig(Mcp mcpConfig) { if (_agent.Backend != null) { @@ -110,57 +144,44 @@ public AgentContext WithMcpConfig(Mcp mcpConfig) return this; } - public AgentContext WithInferenceParams(InferenceParams inferenceParams) + public IAgentConfigurationBuilder WithInferenceParams(InferenceParams inferenceParams) { _inferenceParams = inferenceParams; return this; } - public AgentContext WithMemoryParams(MemoryParams memoryParams) + public IAgentConfigurationBuilder WithMemoryParams(MemoryParams memoryParams) { _memoryParams = memoryParams; return this; } - public AgentContext WithCustomModel(string model, string path, string? mmProject = null) - { - KnownModels.AddModel(model, path, mmProject); - _agent.Model = model; - return this; - } - - public AgentContext WithInitialPrompt(string prompt) - { - _agent.Context.Instruction = prompt; - return this; - } - - public AgentContext WithSteps(List? steps) + public IAgentConfigurationBuilder WithSteps(List? steps) { _agent.Context.Steps = steps; return this; } - public AgentContext WithKnowledge(Func knowledgeConfig) + public IAgentConfigurationBuilder WithKnowledge(Func knowledgeConfig) { var builder = KnowledgeBuilder.Instance.ForAgent(_agent); _knowledge = knowledgeConfig(builder).Build(); return this; } - public AgentContext WithKnowledge(KnowledgeBuilder knowledge) + public IAgentConfigurationBuilder WithKnowledge(KnowledgeBuilder knowledge) { _knowledge = knowledge.ForAgent(_agent).Build(); return this; } - public AgentContext WithKnowledge(Knowledge knowledge) + public IAgentConfigurationBuilder WithKnowledge(Knowledge knowledge) { _knowledge = knowledge; return this; } - public AgentContext WithInMemoryKnowledge(Func knowledgeConfig) + public IAgentConfigurationBuilder WithInMemoryKnowledge(Func knowledgeConfig) { var builder = KnowledgeBuilder.Instance .ForAgent(_agent) @@ -169,7 +190,7 @@ public AgentContext WithInMemoryKnowledge(Func(); _agent.Behaviours[name] = instruction; @@ -177,19 +198,19 @@ public AgentContext WithBehaviour(string name, string instruction) return this; } - public async Task CreateAsync(bool flow = false, bool interactiveResponse = false) + public async Task CreateAsync(bool flow = false, bool interactiveResponse = false) { await _agentService.CreateAgent(_agent, flow, interactiveResponse, _inferenceParams, _memoryParams, _disableCache); return this; } - public AgentContext Create(bool flow = false, bool interactiveResponse = false) + public IAgentContextExecutor Create(bool flow = false, bool interactiveResponse = false) { _ = _agentService.CreateAgent(_agent, flow, interactiveResponse, _inferenceParams, _memoryParams, _disableCache).Result; return this; } - public AgentContext WithTools(ToolsConfiguration toolsConfiguration) + public IAgentConfigurationBuilder WithTools(ToolsConfiguration toolsConfiguration) { _agent.ToolsConfiguration = toolsConfiguration; return this; @@ -307,41 +328,13 @@ public async Task ProcessAsync( }; } - public async Task GetChat() - { - return await _agentService.GetChatByAgent(_agent.Id); - } - - public async Task RestartChat() - { - return await _agentService.Restart(_agent.Id); - } - - public async Task> GetAllAgents() - { - return await _agentService.GetAgents(); - } - - public async Task GetAgentById(string id) - { - return await _agentService.GetAgentById(id); - } - - public async Task Delete() - { - await _agentService.DeleteAgent(_agent.Id); - } - - public async Task Exists() - { - return await _agentService.AgentExists(_agent.Id); - } - public static async Task FromExisting(IAgentService agentService, string agentId) { var existingAgent = await agentService.GetAgentById(agentId); if (existingAgent == null) - throw new ArgumentException("Agent not found", nameof(agentId)); + { + throw new AgentNotFoundException(agentId); + } var context = new AgentContext(agentService, existingAgent); context.LoadExistingKnowledgeIfExists(); diff --git a/src/MaIN.Core/Hub/Contexts/ChatContext.cs b/src/MaIN.Core/Hub/Contexts/ChatContext.cs index 07a1aa3b..8f58c83e 100644 --- a/src/MaIN.Core/Hub/Contexts/ChatContext.cs +++ b/src/MaIN.Core/Hub/Contexts/ChatContext.cs @@ -1,6 +1,8 @@ +using MaIN.Core.Hub.Contexts.Interfaces.ChatContext; using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Entities.Tools; +using MaIN.Domain.Exceptions.Chats; using MaIN.Domain.Models; using MaIN.Services; using MaIN.Services.Constants; @@ -10,7 +12,7 @@ namespace MaIN.Core.Hub.Contexts; -public sealed class ChatContext +public sealed class ChatContext : IChatBuilderEntryPoint, IChatMessageBuilder, IChatConfigurationBuilder { private readonly IChatService _chatService; private bool _preProcess; @@ -36,72 +38,78 @@ internal ChatContext(IChatService chatService, Chat existingChat) _chat = existingChat; } - public ChatContext WithModel(string model) + + public IChatMessageBuilder WithModel(string model) { _chat.Model = model; return this; } - - public ChatContext WithInferenceParams(InferenceParams inferenceParams) + + public IChatMessageBuilder WithCustomModel(string model, string path, string? mmProject = null) { - _chat.InterferenceParams = inferenceParams; + KnownModels.AddModel(model, path, mmProject); + _chat.Model = model; return this; } - public ChatContext WithTools(ToolsConfiguration toolsConfiguration) + public IChatMessageBuilder EnableVisual() { - _chat.ToolsConfiguration = toolsConfiguration; + _chat.Visual = true; return this; } - public ChatContext WithMemoryParams(MemoryParams memoryParams) + public IChatConfigurationBuilder WithInferenceParams(InferenceParams inferenceParams) { - _chat.MemoryParams = memoryParams; + _chat.InterferenceParams = inferenceParams; return this; } - public ChatContext WithCustomModel(string model, string path, string? mmProject = null) + public IChatConfigurationBuilder WithTools(ToolsConfiguration toolsConfiguration) { - KnownModels.AddModel(model, path, mmProject); - _chat.Model = model; + _chat.ToolsConfiguration = toolsConfiguration; return this; } - public ChatContext Speak(TextToSpeechParams textToSpeechParams) + public IChatConfigurationBuilder WithMemoryParams(MemoryParams memoryParams) { - _chat.Visual = false; - _chat.TextToSpeechParams = textToSpeechParams; - + _chat.MemoryParams = memoryParams; return this; } - public ChatContext WithBackend(BackendType backendType) + public IChatConfigurationBuilder Speak(TextToSpeechParams speechParams) { - _chat.Backend = backendType; + _chat.Visual = false; + _chat.TextToSpeechParams = speechParams; return this; } - public ChatContext WithMessages(IEnumerable messages) + public IChatConfigurationBuilder WithBackend(BackendType backendType) { - _chat.Messages.AddRange(messages); + _chat.Backend = backendType; return this; } - - public ChatContext WithMessage(string content) + + public IChatConfigurationBuilder WithSystemPrompt(string systemPrompt) { var message = new Message { - Role = "User", - Content = content, - Type = MessageType.LocalLLM, + Role = "System", + Content = systemPrompt, + Type = MessageType.NotSet, Time = DateTime.Now }; - - _chat.Messages.Add(message); + + _chat.Messages.Insert(0, message); return this; } - - public ChatContext WithMessage(string content, byte[] image) + + public IChatConfigurationBuilder WithMessage(string content) + { + _chat.Messages.Add(new Message { Role = "User", Content = content, Type = MessageType.LocalLLM, Time = DateTime.Now }); + return this; + } + + public IChatConfigurationBuilder WithMessage(string content, byte[] image) { var message = new Message { @@ -111,77 +119,53 @@ public ChatContext WithMessage(string content, byte[] image) Time = DateTime.Now, Image = image }; - + _chat.Messages.Add(message); return this; } - public ChatContext WithSystemPrompt(string systemPrompt) + public IChatConfigurationBuilder WithMessages(IEnumerable messages) { - var message = new Message - { - Role = "System", - Content = systemPrompt, - Type = MessageType.NotSet, - Time = DateTime.Now - }; - - // Insert system message at the beginning - _chat.Messages.Insert(0, message); + _chat.Messages.AddRange(messages); return this; } - public ChatContext WithFiles(List fileStreams, bool preProcess = false) + public IChatConfigurationBuilder WithFiles(List file, bool preProcess = false) { - var files = fileStreams.Select(p => new FileInfo() - { - Name = Path.GetFileName(p.Name), - Path = null, - Extension = Path.GetExtension(p.Name), - StreamContent = p - }).ToList(); - + _files = file.Select(f => new FileInfo { Name = Path.GetFileName(f.Name), StreamContent = f, Extension = Path.GetExtension(f.Name) }) + .ToList(); _preProcess = preProcess; - _files = files; return this; } - public ChatContext WithFiles(List files, bool preProcess = false) + public IChatConfigurationBuilder WithFiles(List file, bool preProcess = false) { - _files = files; + _files = file; _preProcess = preProcess; return this; } - - public ChatContext WithFiles(List filePaths, bool preProcess = false) - { - var files = filePaths.Select(p => new FileInfo() - { - Name = Path.GetFileName(p), - Path = p, - Extension = Path.GetExtension(p) - }).ToList(); - _preProcess = preProcess; - _files = files; - return this; - } - - public ChatContext EnableVisual() + public IChatConfigurationBuilder WithFiles(List file, bool preProcess = false) { - _chat.Visual = true; - _chat.TextToSpeechParams = null; + _files = file + .Select(path => + new FileInfo + { + Name = Path.GetFileName(path), + Path = path, + Extension = Path.GetExtension(path) + }) + .ToList(); + _preProcess = preProcess; return this; } - public ChatContext DisableCache() + public IChatConfigurationBuilder DisableCache() { _chat.Properties.AddProperty(ServiceConstants.Properties.DisableCacheProperty); return this; } - public string GetChatId() => _chat.Id; - public async Task CompleteAsync( bool translate = false, bool interactive = false, @@ -189,8 +173,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) { @@ -205,27 +190,13 @@ public async Task CompleteAsync( _files = []; return result; } - - - public async Task GetCurrentChat() - { - if (_chat.Id == null) - throw new InvalidOperationException("Chat has not been created yet. Call CompleteAsync first."); - - return await _chatService.GetById(_chat.Id); - } - - public async Task> GetAllChats() - { - return await _chatService.GetAll(); - } - public async Task DeleteChat() + public async Task FromExisting(string chatId) { - if (_chat.Id == null) - throw new InvalidOperationException("Chat has not been created yet."); - - await _chatService.Delete(_chat.Id); + var existing = await _chatService.GetById(chatId); + return existing == null + ? throw new ChatNotFoundException(chatId) + : new ChatContext(_chatService, existing); } private async Task ChatExists(string id) @@ -240,18 +211,37 @@ private async Task ChatExists(string id) return false; } } + + IChatMessageBuilder IChatMessageBuilder.EnableVisual() => EnableVisual(); - // Static methods to create builder from existing chat - public async Task FromExisting(string chatId) + + public string GetChatId() => _chat.Id; + + public async Task GetCurrentChat() { - var existingChat = await _chatService.GetById(chatId); - if (existingChat == null) + if (_chat.Id == null) { - throw new Exception("Chat not found"); + throw new ChatNotInitializedException(); } - return new ChatContext(_chatService, existingChat); + + return await _chatService.GetById(_chat.Id); } + public async Task> GetAllChats() + { + return await _chatService.GetAll(); + } + + public async Task DeleteChat() + { + if (_chat.Id == null) + { + throw new ChatNotInitializedException(); + } + + await _chatService.Delete(_chat.Id); + } + public List GetChatHistory() { return _chat.Messages.Select(x => new MessageShort() diff --git a/src/MaIN.Core/Hub/Contexts/FlowContext.cs b/src/MaIN.Core/Hub/Contexts/FlowContext.cs index 4cd60c05..449176bd 100644 --- a/src/MaIN.Core/Hub/Contexts/FlowContext.cs +++ b/src/MaIN.Core/Hub/Contexts/FlowContext.cs @@ -1,15 +1,17 @@ using System.IO.Compression; using System.Text.Json; +using MaIN.Core.Hub.Contexts.Interfaces.FlowContext; using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Entities.Agents; using MaIN.Domain.Entities.Agents.AgentSource; +using MaIN.Domain.Exceptions.Flows; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; namespace MaIN.Core.Hub.Contexts; -public sealed class FlowContext +public sealed class FlowContext : IFlowContext { private readonly IAgentFlowService _flowService; private readonly IAgentService _agentService; @@ -35,25 +37,25 @@ internal FlowContext(IAgentFlowService flowService, IAgentService agentService, _flow = existingFlow; } - public FlowContext WithId(string id) + public IFlowContext WithId(string id) { _flow.Id = id; return this; } - public FlowContext WithName(string name) + public IFlowContext WithName(string name) { _flow.Name = name; return this; } - public FlowContext WithDescription(string description) + public IFlowContext WithDescription(string description) { _flow.Description = description; return this; } - public FlowContext Save(string path) + public IFlowContext Save(string path) { Directory.CreateDirectory(Path.GetDirectoryName(path)!); @@ -83,7 +85,7 @@ public FlowContext Save(string path) return this; } - public FlowContext Load(string path) + public IFlowContext Load(string path) { var fileName = Path.GetFileNameWithoutExtension(path); string description = ""; @@ -132,7 +134,7 @@ public FlowContext Load(string path) return this; } - public FlowContext AddAgent(Agent agent) + public IFlowContext AddAgent(Agent agent) { _flow.Agents.Add(agent); return this; @@ -189,7 +191,7 @@ public async Task ProcessAsync(Message message, bool translate = fal }; } - public FlowContext AddAgents(IEnumerable agents) + public IFlowContext AddAgents(IEnumerable agents) { foreach (var agent in agents) { @@ -208,7 +210,7 @@ public async Task CreateAsync() public async Task Delete() { if (_flow.Id == null) - throw new InvalidOperationException("Flow has not been created yet."); + throw new FlowNotInitializedException(); await _flowService.DeleteFlow(_flow.Id); } @@ -217,7 +219,7 @@ public async Task Delete() public async Task GetCurrentFlow() { if (_flow.Id == null) - throw new InvalidOperationException("Flow has not been created yet."); + throw new FlowNotInitializedException(); return await _flowService.GetFlowById(_flow.Id); } @@ -228,12 +230,11 @@ public async Task> GetAllFlows() } // Static factory methods - public async Task FromExisting(string flowId) + public async Task FromExisting(string flowId) { var existingFlow = await _flowService.GetFlowById(flowId); - if (existingFlow == null) - throw new ArgumentException("Flow not found", nameof(flowId)); - - return this; + return existingFlow == null + ? throw new FlowFoundException(flowId) + : this; } } \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentActions.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentActions.cs new file mode 100644 index 00000000..204477fc --- /dev/null +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentActions.cs @@ -0,0 +1,62 @@ +using MaIN.Domain.Entities; +using MaIN.Domain.Entities.Agents; +using MaIN.Domain.Entities.Agents.Knowledge; + +namespace MaIN.Core.Hub.Contexts.Interfaces.AgentContext; + +public interface IAgentActions +{ + /// + /// Gets the unique identifier (GUID) of the current Agent. + /// + /// The Agent's ID as a string. + string GetAgentId(); + + /// + /// Retrieves the full Agent entity with its current configuration and state. + /// + /// An object. + Agent GetAgent(); + + /// + /// Gets the knowledge base (Knowledge) assigned to this Agent. + /// + /// A object or null if not configured. + Knowledge? GetKnowledge(); + + /// + /// Retrieves the current chat session associated with this Agent. + /// + /// A task representing the asynchronous operation, containing the . + Task GetChat(); + + /// + /// Clears the conversation history and restarts the chat session for this Agent. + /// + /// A task representing the asynchronous operation, containing a new . + Task RestartChat(); + + /// + /// Lists all agents available in the system. + /// + /// A task containing a list of objects. + Task> GetAllAgents(); + + /// + /// Retrieves a specific agent by its unique identifier. + /// + /// The unique identifier of the Agent. + /// The object or null if not found. + Task GetAgentById(string id); + + /// + /// Permanently deletes the current Agent and all associated data. + /// + Task Delete(); + + /// + /// Checks if an agent with the current identifier exists in the system. + /// + /// True if the agent exists, otherwise false. + Task Exists(); +} \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentBuilderEntryPoint.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentBuilderEntryPoint.cs new file mode 100644 index 00000000..19990bb2 --- /dev/null +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentBuilderEntryPoint.cs @@ -0,0 +1,25 @@ +namespace MaIN.Core.Hub.Contexts.Interfaces.AgentContext; + +public interface IAgentBuilderEntryPoint : IAgentActions +{ + /// + /// Sets the LLM model to be used by the Agent. + /// + /// The name or identifier of the model (e.g., "llama3.2"). + IAgentConfigurationBuilder WithModel(string model); + + /// + /// Configures a custom model from a specific local path. + /// + /// A custom name for the model. + /// The file system path to the model files. + /// Optional multi-modal project identifier. + IAgentConfigurationBuilder WithCustomModel(string model, string path, string? mmProject = null); + + /// + /// Loads an existing Agent from the database. + /// + /// The unique identifier of the Agent to load. + /// An execution interface ready for processing messages. + Task FromExisting(string agentId); +} \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentConfigurationBuilder.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentConfigurationBuilder.cs new file mode 100644 index 00000000..bba02aaa --- /dev/null +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentConfigurationBuilder.cs @@ -0,0 +1,128 @@ +using MaIN.Core.Hub.Utils; +using MaIN.Domain.Configuration; +using MaIN.Domain.Entities; +using MaIN.Domain.Entities.Agents.AgentSource; +using MaIN.Domain.Entities.Agents.Knowledge; +using MaIN.Domain.Entities.Tools; + +namespace MaIN.Core.Hub.Contexts.Interfaces.AgentContext; + +public interface IAgentConfigurationBuilder : IAgentActions +{ + /// + /// Sets the system-level instruction or persona for the Agent. This prompt defines how the Agent should behave and respond. + /// + /// The text content of the system instructions or Agent persona. + IAgentConfigurationBuilder WithInitialPrompt(string prompt); + + /// + /// Sets a custom unique identifier for the Agent. + /// + /// The new Agent ID. + IAgentConfigurationBuilder WithId(string id); + + /// + /// Sets the Agent's execution order (relevant in multi-agent flows). + /// + /// The sequence number. + IAgentConfigurationBuilder WithOrder(int order); + + /// + /// Disables the caching mechanism for this Agent's requests. + /// + IAgentConfigurationBuilder DisableCache(); + + /// + /// Defines the data source from which the Agent derives its identity or knowledge. + /// + /// The implementation of the agent source. + /// The type of source (e.g., PDF, Web). + IAgentConfigurationBuilder WithSource(IAgentSource source, AgentSourceType type); + + /// + /// Sets a friendly display name for the Agent. + /// + /// The name of the Agent. + IAgentConfigurationBuilder WithName(string name); + + /// + /// Selects a specific processing engine (Backend) for LLM requests. + /// + /// The backend type. + IAgentConfigurationBuilder WithBackend(BackendType backendType); + + /// + /// Configures integration with the Model Context Protocol (MCP). + /// + /// The MCP configuration object. + IAgentConfigurationBuilder WithMcpConfig(Mcp mcpConfig); + + /// + /// Sets inference parameters such as temperature, top-p, or max tokens. + /// + /// An object containing LLM technical settings. + IAgentConfigurationBuilder WithInferenceParams(InferenceParams inferenceParams); + + /// + /// Configures memory and context window settings for the Agent. + /// + /// Memory management settings. + IAgentConfigurationBuilder WithMemoryParams(MemoryParams memoryParams); + + /// + /// Defines a sequence of steps (pipeline) that the Agent must execute for each request. + /// + /// A list of step names. + IAgentConfigurationBuilder WithSteps(List? steps); + + /// + /// Assigns tools (functions) that the Agent can autonomously invoke. + /// + /// The configuration for available tools. + IAgentConfigurationBuilder WithTools(ToolsConfiguration toolsConfiguration); + + /// + /// Configures the Agent's knowledge base using a configuration delegate. + /// + /// A function to configure the knowledge builder. + IAgentConfigurationBuilder WithKnowledge(Func knowledgeConfig); + + /// + /// Initializes the knowledge base using a pre-configured builder. + /// + /// The knowledge builder instance. + IAgentConfigurationBuilder WithKnowledge(KnowledgeBuilder knowledge); + + /// + /// Assigns a pre-built knowledge base object to the Agent. + /// + /// The knowledge instance. + IAgentConfigurationBuilder WithKnowledge(Knowledge knowledge); + + /// + /// Creates a volatile knowledge base stored only in memory (not persisted to disk). + /// + /// A function to configure the in-memory knowledge builder. + IAgentConfigurationBuilder WithInMemoryKnowledge(Func knowledgeConfig); + + /// + /// Defines a specific personality or task (Behavior) under a unique name. + /// + /// The name of the behavior. + /// The system instructions for this behavior. + IAgentConfigurationBuilder WithBehaviour(string name, string instruction); + + /// + /// Finalizes the build process and creates the Agent in the system synchronously. + /// + /// Indicates if the Agent is part of a larger flow. + /// Specifies if responses should be streamed. + IAgentContextExecutor Create(bool flow = false, bool interactiveResponse = false); + + /// + /// Finalizes the build process and creates the Agent in the system asynchronously. + /// + /// Indicates if the Agent is part of a larger flow. + /// Specifies if responses should be interactive. + Task CreateAsync(bool flow = false, bool interactiveResponse = false); +} \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentContextExecutor.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentContextExecutor.cs new file mode 100644 index 00000000..bb85cfcc --- /dev/null +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentContextExecutor.cs @@ -0,0 +1,47 @@ +using MaIN.Domain.Entities; +using MaIN.Domain.Entities.Tools; +using MaIN.Domain.Models; +using MaIN.Services.Services.Models; + +namespace MaIN.Core.Hub.Contexts.Interfaces.AgentContext; + +public interface IAgentContextExecutor : IAgentActions +{ + /// + /// Processes a request based on a full chat object including history. + /// + /// The chat object with history. + /// Indicates if the result should be translated. + /// A containing the model's response. + Task ProcessAsync(Chat chat, bool translate = false); + + /// + /// Processes a simple text message from the user. + /// + /// The text content of the message. + /// Indicates if the result should be translated. + /// Optional callback for receiving streaming tokens. + /// Optional callback for tool invocation events. + /// A containing the model's response. + Task ProcessAsync(string message, bool translate = false, Func? tokenCallback = null, Func? toolCallback = null); + + /// + /// Processes a single message object (may include images or files). + /// + /// The object. + /// Indicates if the result should be translated. + /// Callback for streaming tokens. + /// Callback for tool invocations. + /// A containing the model's response. + Task ProcessAsync(Message message, bool translate = false, Func? tokenCallback = null, Func? toolCallback = null); + + /// + /// Processes a collection of messages, updating the Agent's chat state. + /// + /// A list of messages to process. + /// Indicates if the result should be translated. + /// Callback for streaming tokens. + /// Callback for tool invocations. + /// A containing the model's response. + Task ProcessAsync(IEnumerable messages, bool translate = false, Func? tokenCallback = null, Func? toolCallback = null); +} \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatActions.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatActions.cs new file mode 100644 index 00000000..18b1d811 --- /dev/null +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatActions.cs @@ -0,0 +1,31 @@ +using MaIN.Domain.Entities; + +namespace MaIN.Core.Hub.Contexts.Interfaces.ChatContext; + +public interface IChatActions +{ + /// + /// Gets the unique identifier (GUID) of the current chat session. + /// + string GetChatId(); + + /// + /// Retrieves the full chat object with all its messages and properties. + /// + Task GetCurrentChat(); + + /// + /// Lists all chat sessions stored in the system. + /// + Task> GetAllChats(); + + /// + /// Permanently deletes the current chat session and its history. + /// + Task DeleteChat(); + + /// + /// Provides a lightweight summary of the chat history (Role, Content, Time). + /// + List GetChatHistory(); +} \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatBuilderEntryPoint.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatBuilderEntryPoint.cs new file mode 100644 index 00000000..2cbbf69d --- /dev/null +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatBuilderEntryPoint.cs @@ -0,0 +1,30 @@ +namespace MaIN.Core.Hub.Contexts.Interfaces.ChatContext; + +public interface IChatBuilderEntryPoint : IChatActions +{ + /// + /// Sets the standard model to be used for the chat session. + /// + /// The name or identifier of the LLM model. + IChatMessageBuilder WithModel(string model); + + /// + /// Configures a custom model with a specific path and project context. + /// + /// The name of the custom model. + /// The path to the model files. + /// Optional multi-modal project identifier. + + IChatMessageBuilder WithCustomModel(string model, string path, string? mmProject = null); + /// + /// Enables visual/image generation mode. Use this method now if you do not plan to explicitly define the model. + /// Otherwise, you will be able to use this method after defining the model. + /// + IChatMessageBuilder EnableVisual(); + + /// + /// Loads an existing chat session from the database using its unique identifier. + /// + /// The GUID of the existing chat. + Task FromExisting(string chatId); +} \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatConfigurationBuilder.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatConfigurationBuilder.cs new file mode 100644 index 00000000..d12dfc60 --- /dev/null +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatConfigurationBuilder.cs @@ -0,0 +1,84 @@ +using MaIN.Domain.Configuration; +using MaIN.Domain.Entities; +using MaIN.Domain.Entities.Tools; +using MaIN.Domain.Models; +using MaIN.Services.Services.Models; +using FileInfo = MaIN.Domain.Entities.FileInfo; + +namespace MaIN.Core.Hub.Contexts.Interfaces.ChatContext; + +public interface IChatConfigurationBuilder : IChatActions +{ + /// + /// Configures low-level LLM parameters like temperature, context size, max tokens etc. + /// + /// An object containing detailed inference settings for the LLM. + IChatConfigurationBuilder WithInferenceParams(InferenceParams inferenceParams); + + /// + /// Attaches external tools/functions that the model can invoke during the conversation. + /// + /// Configuration defining available tools and their execution modes. + IChatConfigurationBuilder WithTools(ToolsConfiguration toolsConfiguration); + + /// + /// Defines memory parameters for the chat. + /// + /// Configuration for how the chat context and memory should be handled. + IChatConfigurationBuilder WithMemoryParams(MemoryParams memoryParams); + + /// + /// Configures the session to use Text-to-Speech for the model's responses. + /// + /// Parameters for the voice synthesis. + IChatConfigurationBuilder Speak(TextToSpeechParams speechParams); + + /// + /// Sets the specific execution backend for the inference (e.g., Local, Cloud). + /// + /// The type of backend to be used for processing the request. + IChatConfigurationBuilder WithBackend(BackendType backendType); + + /// + /// Sets a system-level prompt to guide the model's behavior, persona, and constraints. + /// + /// The text content of the system instructions. + IChatConfigurationBuilder WithSystemPrompt(string systemPrompt); + + /// + /// Attaches a list of files provided as to the message context. + /// + /// A list of open file streams to be uploaded or analyzed. + /// If true, the files will be pre-processed (e.g., indexed) before sending. + IChatConfigurationBuilder WithFiles(List file, bool preProcess = false); + + /// + /// Attaches a list of files provided as objects to the message context. + /// + /// A list of file metadata and content references. + /// If true, the files will be pre-processed (e.g., indexed) before sending. + IChatConfigurationBuilder WithFiles(List file, bool preProcess = false); + + /// + /// Attaches a list of files from provided local system paths to the message context. + /// + /// A list of absolute or relative paths to the files. + /// If true, the files will be pre-processed (e.g., indexed) before sending. + /// The builder instance implementing to enable method chaining. + IChatConfigurationBuilder WithFiles(List file, bool preProcess = false); + + /// + /// Disables the internal caching mechanism for the upcoming request. + /// + IChatConfigurationBuilder DisableCache(); + + /// + /// Sends the configured chat context to the service for completion. + /// + /// Indicates whether the response should be automatically translated. + /// If true, the response will be processed in streaming mode. + /// An optional callback invoked whenever a new token or update is received during streaming. + /// A task representing the asynchronous operation, containing the . + Task CompleteAsync(bool translate = false, bool interactive = false, Func? changeOfValue = null); + +} \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatMessageBuilder.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatMessageBuilder.cs new file mode 100644 index 00000000..c160d8f3 --- /dev/null +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatMessageBuilder.cs @@ -0,0 +1,30 @@ +using MaIN.Domain.Entities; + +namespace MaIN.Core.Hub.Contexts.Interfaces.ChatContext; + +public interface IChatMessageBuilder : IChatActions +{ + /// + /// Enables visual/image generation mode. + /// + IChatMessageBuilder EnableVisual(); + + /// + /// Adds a single text message to the chat context. + /// + /// The text content of the message. + IChatConfigurationBuilder WithMessage(string content); + + /// + /// Adds a message containing both text and image data. + /// + /// The text description or prompt. + /// The byte array containing image data. + IChatConfigurationBuilder WithMessage(string content, byte[] image); + + /// + /// Appends a collection of messages to the chat. + /// + /// An enumerable list of message objects. + IChatConfigurationBuilder WithMessages(IEnumerable messages); +} \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/FlowContext/IFlowContext.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/FlowContext/IFlowContext.cs new file mode 100644 index 00000000..af64aa51 --- /dev/null +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/FlowContext/IFlowContext.cs @@ -0,0 +1,118 @@ +using MaIN.Domain.Entities; +using MaIN.Domain.Entities.Agents; +using MaIN.Domain.Entities.Agents.AgentSource; +using MaIN.Services.Services.Models; + +namespace MaIN.Core.Hub.Contexts.Interfaces.FlowContext; + +public interface IFlowContext +{ + /// + /// Assigns a unique identifier to the flow. + /// + /// The unique identifier for the flow. + /// The IFlowContext instance to enable method chaining. + IFlowContext WithId(string id); + + /// + /// Assigns a custom name to the flow. + /// + /// The custom name for the flow. + /// The IFlowContext instance to enable method chaining. + IFlowContext WithName(string name); + + /// + /// Sets a description for the flow. + /// + /// A brief description of the flow's purpose. + /// The IFlowContext instance to enable method chaining. + IFlowContext WithDescription(string description); + + /// + /// Saves the current flow and its associated agents to a zip archive at the specified path. + /// This method also includes a text file for the flow description. + /// + /// The file path where the flow and its agents should be saved. + /// The IFlowContext instance to enable method chaining. + IFlowContext Save(string path); + + /// + /// Loads an existing flow from a zip archive located at the specified path. + /// This archive should contain the flow description and agent files in JSON format. + /// + /// The file path where the flow archive is stored. + /// The IFlowContext instance to enable method chaining. + IFlowContext Load(string path); + + /// + /// Adds an agent to the flow. This allows you to dynamically update the flow with new agents. + /// + /// The Agent to be added to the flow. + /// The IFlowContext instance to enable method chaining. + IFlowContext AddAgent(Agent agent); + + /// + /// Adds a collection of agents to the flow. This method enables the batch addition of multiple agents at once. + /// + /// The collection of agents to be added to the flow. + /// The IFlowContext instance to enable method chaining. + IFlowContext AddAgents(IEnumerable agents); + + /// + /// Processes a chat through the first agent in the flow, generating a response based on the chat's messages and the agent's context. + /// + /// The Chat object to process. + /// A flag indicating whether the response should be translated. Default is false. + /// A ChatResult object containing the processed message and other related information. + Task ProcessAsync(Chat chat, bool translate = false); + + /// + /// Processes a user-provided message through the first agent in the flow, generating a response based on the agent's context. + /// + /// The message to be processed by the agent. + /// A flag indicating whether the response should be translated. Default is false. + /// A ChatResult object containing the processed message and other related information. + Task ProcessAsync(string message, bool translate = false); + + /// + /// Processes a message object through the first agent in the flow, generating a response based on the agent's context and message data. + /// + /// The Message object to be processed. + /// A flag indicating whether the response should be translated. Default is false. + /// A ChatResult object containing the processed message and other related information. + Task ProcessAsync(Message message, bool translate = false); + + /// + /// Creates and persists the current flow asynchronously. + /// + /// The created AgentFlow object. + Task CreateAsync(); + + /// + /// Deletes the current flow from the system. + /// + /// A task representing the asynchronous delete operation. + /// Thrown if the flow ID is not set. + Task Delete(); + + /// + /// Retrieves the current flow with its latest state from the system. + /// + /// The current AgentFlow object. + /// Thrown if the flow ID is not set. + Task GetCurrentFlow(); + + /// + /// Retrieves all flows available in the system. + /// + /// A list of all AgentFlow objects. + Task> GetAllFlows(); + + /// + /// Loads an existing flow from the system by its ID and initializes the context with it. + /// + /// The unique identifier of the flow to load. + /// The IFlowContext instance initialized with the existing flow. + /// Thrown if the flow with the specified ID is not found. + Task FromExisting(string flowId); +} \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/McpContext/IMcpContext.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/McpContext/IMcpContext.cs new file mode 100644 index 00000000..1de8f5bd --- /dev/null +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/McpContext/IMcpContext.cs @@ -0,0 +1,29 @@ +using MaIN.Domain.Configuration; +using MaIN.Domain.Entities; +using MaIN.Services.Services.Models; + +namespace MaIN.Core.Hub.Contexts.Interfaces.McpContext; + +public interface IMcpContext +{ + /// + /// Sets the MCP configuration for the context. This configuration defines the connection parameters and settings required + /// to interact with MCP servers. + /// + /// The configuration object containing server connection details and settings. + IMcpContext WithConfig(Mcp mcpConfig); + + /// + /// Specifies the backend type to be used for MCP operations. This allows you to select different backend implementations + /// based on your requirements. + /// + /// The enum value specifying which backend implementation to use. + IMcpContext WithBackend(BackendType backendType); + + /// + /// Asynchronously processes a prompt through the configured MCP service, sending the prompt to the MCP server and returning the processed result. + /// + /// The text prompt to be processed by the MCP service + /// A object containing the processed response from the MCP server. + Task PromptAsync(string prompt); +} \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/ModelContext/IModelContext.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/ModelContext/IModelContext.cs new file mode 100644 index 00000000..3ec46647 --- /dev/null +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/ModelContext/IModelContext.cs @@ -0,0 +1,88 @@ +using MaIN.Domain.Models; + +namespace MaIN.Core.Hub.Contexts.Interfaces.ModelContext; + +public interface IModelContext +{ + /// + /// Retrieves a complete list of all available models in the system. This method returns all known models that + /// can be used within the MaIN framework. + /// + /// A list of containing all available models in the system + List GetAll(); + + /// + /// Retrieves information about a specific model by its name. This method allows you to get detailed information about a particular model, + /// including its configuration and metadata. + /// + /// The name of the model to retrieve. + /// A object containing the model's information and configuration. + Model GetModel(string model); + + /// + /// Retrieves the designated embedding model used for generating vector representations of text. + /// + /// A Model object representing the embedding model. + Model GetEmbeddingModel(); + + /// + /// Checks whether a specific model exists locally on the filesystem. + /// + /// The name of the model to check for existence. + /// True if the model file exists locally; otherwise, false. + /// Thrown if the model name is null or empty. + bool Exists(string modelName); + + /// + /// Asynchronously downloads a known model from its configured download URL. + /// + /// The name of the model to download. + /// Optional cancellation token to abort the download operation. + /// A task that represents the asynchronous download operation, returning the IModelContext instance for method chaining. + /// Thrown if the model name is null or empty. + Task DownloadAsync(string modelName, CancellationToken cancellationToken = default); + + /// + /// Asynchronously downloads a custom model from a specified URL. + /// + /// The name to assign to the downloaded model. + /// The URL from which to download the model. + /// A task that represents the asynchronous download operation, returning the IModelContext instance for method chaining. + /// Thrown if the model name or URL is null or empty. + Task DownloadAsync(string model, string url); + + /// + /// Synchronously downloads a known model from its configured download URL. + /// + /// The name of the model to download. + /// The IModelContext instance for method chaining. + /// Thrown if the model name is null or empty. + [Obsolete("Use DownloadAsync instead")] + IModelContext Download(string modelName); + + /// + /// Synchronously downloads a custom model from a specified URL. + /// + /// The name to assign to the downloaded model. + /// The URL from which to download the model. + /// The IModelContext instance for method chaining. + /// Thrown if the model name or URL is null or empty. + [Obsolete("Use DownloadAsync instead")] + IModelContext Download(string model, string url); + + /// + /// Loads a model into the memory cache for faster access during inference operations. + /// + /// The Model object to load into cache. + /// The IModelContext instance for method chaining. + /// Thrown if the model parameter is null. + IModelContext LoadToCache(Model model); + + /// + /// Asynchronously loads a model into the memory cache for faster access during inference operations. + /// + /// The Model object to load into a cache. + /// A task that represents the asynchronous load operation, returning the IModelContext instance for method chaining. + /// Thrown if the model parameter is null. + Task LoadToCacheAsync(Model model); +} \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/McpContext.cs b/src/MaIN.Core/Hub/Contexts/McpContext.cs index 0f748ab7..069da167 100644 --- a/src/MaIN.Core/Hub/Contexts/McpContext.cs +++ b/src/MaIN.Core/Hub/Contexts/McpContext.cs @@ -1,12 +1,14 @@ +using MaIN.Core.Hub.Contexts.Interfaces.McpContext; using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions.MPC; using MaIN.Services.Constants; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; namespace MaIN.Core.Hub.Contexts; -public sealed class McpContext +public sealed class McpContext : IMcpContext { private readonly IMcpService _mcpService; private Mcp? _mcpConfig; @@ -17,13 +19,13 @@ internal McpContext(IMcpService mcpService) _mcpConfig = Mcp.NotSet; } - public McpContext WithConfig(Mcp mcpConfig) + public IMcpContext WithConfig(Mcp mcpConfig) { _mcpConfig = mcpConfig; return this; } - public McpContext WithBackend(BackendType backendType) + public IMcpContext WithBackend(BackendType backendType) { _mcpConfig!.Backend = backendType; return this; @@ -33,7 +35,7 @@ public async Task PromptAsync(string prompt) { if (_mcpConfig == null) { - throw new InvalidOperationException("MCP config not found"); + throw new MPCConfigNotFoundException(); } return await _mcpService.Prompt(_mcpConfig!, [new Message() diff --git a/src/MaIN.Core/Hub/Contexts/ModelContext.cs b/src/MaIN.Core/Hub/Contexts/ModelContext.cs index 000f7af7..5bfa66ca 100644 --- a/src/MaIN.Core/Hub/Contexts/ModelContext.cs +++ b/src/MaIN.Core/Hub/Contexts/ModelContext.cs @@ -1,13 +1,15 @@ using System.Diagnostics; using System.Net; +using MaIN.Core.Hub.Contexts.Interfaces.ModelContext; using MaIN.Domain.Configuration; +using MaIN.Domain.Exceptions.Models; using MaIN.Domain.Models; using MaIN.Services.Constants; using MaIN.Services.Services.LLMService.Utils; namespace MaIN.Core.Hub.Contexts; -public sealed class ModelContext +public sealed class ModelContext : IModelContext { private readonly MaINSettings _settings; private readonly IHttpClientFactory _httpClientFactory; @@ -15,7 +17,6 @@ public sealed class ModelContext private const int DefaultBufferSize = 8192; private const int FileStreamBufferSize = 65536; private const int ProgressUpdateIntervalMilliseconds = 1000; - private const string MissingModelName = "Model name cannot be null or empty"; private static readonly TimeSpan DefaultHttpTimeout = TimeSpan.FromMinutes(30); internal ModelContext(MaINSettings settings, IHttpClientFactory httpClientFactory) @@ -42,11 +43,11 @@ public bool Exists(string modelName) return File.Exists(modelPath); } - public async Task DownloadAsync(string modelName, CancellationToken cancellationToken = default) + public async Task DownloadAsync(string modelName, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(modelName)) { - throw new ArgumentException(MissingModelName, nameof(modelName)); + throw new MissingModelNameException(nameof(modelName)); } var model = KnownModels.GetModel(modelName); @@ -54,11 +55,11 @@ public async Task DownloadAsync(string modelName, CancellationToke return this; } - public async Task DownloadAsync(string model, string url) + public async Task DownloadAsync(string model, string url) { if (string.IsNullOrWhiteSpace(model)) { - throw new ArgumentException(MissingModelName, nameof(model)); + throw new MissingModelNameException(nameof(model)); } if (string.IsNullOrWhiteSpace(url)) @@ -75,11 +76,11 @@ public async Task DownloadAsync(string model, string url) } [Obsolete("Use async method instead")] - public ModelContext Download(string modelName) + public IModelContext Download(string modelName) { if (string.IsNullOrWhiteSpace(modelName)) { - throw new ArgumentException(MissingModelName, nameof(modelName)); + throw new MissingModelNameException(nameof(modelName)); } var model = KnownModels.GetModel(modelName); @@ -88,11 +89,11 @@ public ModelContext Download(string modelName) } [Obsolete("Obsolete async method instead")] - public ModelContext Download(string model, string url) + public IModelContext Download(string model, string url) { if (string.IsNullOrWhiteSpace(model)) { - throw new ArgumentException(MissingModelName, nameof(model)); + throw new MissingModelNameException(nameof(model)); } if (string.IsNullOrWhiteSpace(url)) @@ -108,7 +109,7 @@ public ModelContext Download(string model, string url) return this; } - public ModelContext LoadToCache(Model model) + public IModelContext LoadToCache(Model model) { ArgumentNullException.ThrowIfNull(model); @@ -117,7 +118,7 @@ public ModelContext LoadToCache(Model model) return this; } - public async Task LoadToCacheAsync(Model model) + public async Task LoadToCacheAsync(Model model) { ArgumentNullException.ThrowIfNull(model); @@ -319,5 +320,5 @@ private static string FormatBytes(long bytes) private string ResolvePath(string? settingsModelsPath) => settingsModelsPath ?? Environment.GetEnvironmentVariable("MaIN_ModelsPath") - ?? throw new InvalidOperationException("Models path not found in settings or environment variables"); + ?? throw new ModelsPathNotFoundException(); } \ No newline at end of file diff --git a/src/MaIN.Domain/Configuration/FileSystemSettings.cs b/src/MaIN.Domain/Configuration/FileSystemSettings.cs index 1058adad..caa6a8dc 100644 --- a/src/MaIN.Domain/Configuration/FileSystemSettings.cs +++ b/src/MaIN.Domain/Configuration/FileSystemSettings.cs @@ -1,4 +1,4 @@ -namespace MaIN.Services.Configuration; +namespace MaIN.Domain.Configuration; public class FileSystemSettings { diff --git a/src/MaIN.Domain/Configuration/MaINSettings.cs b/src/MaIN.Domain/Configuration/MaINSettings.cs index 17f7e062..feb2961a 100644 --- a/src/MaIN.Domain/Configuration/MaINSettings.cs +++ b/src/MaIN.Domain/Configuration/MaINSettings.cs @@ -1,6 +1,4 @@ -using MaIN.Services.Configuration; - namespace MaIN.Domain.Configuration; public class MaINSettings diff --git a/src/MaIN.Domain/Configuration/MongoDbSettings.cs b/src/MaIN.Domain/Configuration/MongoDbSettings.cs index 05710380..624f2be5 100644 --- a/src/MaIN.Domain/Configuration/MongoDbSettings.cs +++ b/src/MaIN.Domain/Configuration/MongoDbSettings.cs @@ -1,4 +1,4 @@ -namespace MaIN.Services.Configuration; +namespace MaIN.Domain.Configuration; public class MongoDbSettings { diff --git a/src/MaIN.Domain/Entities/Agents/Knowledge/Knowledge.cs b/src/MaIN.Domain/Entities/Agents/Knowledge/Knowledge.cs index 80df043c..9ccb204f 100644 --- a/src/MaIN.Domain/Entities/Agents/Knowledge/Knowledge.cs +++ b/src/MaIN.Domain/Entities/Agents/Knowledge/Knowledge.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using MaIN.Domain.Configuration; namespace MaIN.Domain.Entities.Agents.Knowledge; diff --git a/src/MaIN.Domain/Exceptions/AIHubNotInitializedException.cs b/src/MaIN.Domain/Exceptions/AIHubNotInitializedException.cs new file mode 100644 index 00000000..118ce30d --- /dev/null +++ b/src/MaIN.Domain/Exceptions/AIHubNotInitializedException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions; + +public class AIHubNotInitializedException() + : MaINCustomException("AIHub has not been initialized. Make sure to call 'AddAIHub' in your service configuration.") +{ + public override string PublicErrorMessage => LogMessage; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.Conflict; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/APIKeyNotConfiguredException.cs b/src/MaIN.Domain/Exceptions/APIKeyNotConfiguredException.cs new file mode 100644 index 00000000..dcef4c48 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/APIKeyNotConfiguredException.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions; + +public class APIKeyNotConfiguredException(string apiName) : MaINCustomException($"The API key of '{apiName}' has not been configured.") +{ + public override string PublicErrorMessage => Message; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.InternalServerError; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/Agents/AgentAlreadyExistsException.cs b/src/MaIN.Domain/Exceptions/Agents/AgentAlreadyExistsException.cs new file mode 100644 index 00000000..25781f0e --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Agents/AgentAlreadyExistsException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Agents; + +public class AgentAlreadyExistsException(string agentId) + : MaINCustomException($"Agent with id: '{agentId}' already exists.") +{ + public override string PublicErrorMessage => "Agent already exists."; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.Conflict; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/Agents/AgentContextNotFoundException.cs b/src/MaIN.Domain/Exceptions/Agents/AgentContextNotFoundException.cs new file mode 100644 index 00000000..d852cba2 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Agents/AgentContextNotFoundException.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Agents; + +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/Agents/AgentFlowNotFoundException.cs b/src/MaIN.Domain/Exceptions/Agents/AgentFlowNotFoundException.cs new file mode 100644 index 00000000..dd6f9fe8 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Agents/AgentFlowNotFoundException.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Agents; + +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/Agents/AgentNotFoundException.cs b/src/MaIN.Domain/Exceptions/Agents/AgentNotFoundException.cs new file mode 100644 index 00000000..926d6869 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Agents/AgentNotFoundException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Agents; + +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 00000000..903136ba --- /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/Chats/ChatAlreadyExistsException.cs b/src/MaIN.Domain/Exceptions/Chats/ChatAlreadyExistsException.cs new file mode 100644 index 00000000..7f9e0044 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Chats/ChatAlreadyExistsException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Chats; + +public class ChatAlreadyExistsException(string chatId) + : MaINCustomException($"Chat with id: '{chatId}' already exists.") +{ + public override string PublicErrorMessage => "Chat already exists."; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.Conflict; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/Chats/ChatNotFoundException.cs b/src/MaIN.Domain/Exceptions/Chats/ChatNotFoundException.cs new file mode 100644 index 00000000..0d10f489 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Chats/ChatNotFoundException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Chats; + +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/Chats/ChatNotInitializedException.cs b/src/MaIN.Domain/Exceptions/Chats/ChatNotInitializedException.cs new file mode 100644 index 00000000..47f54292 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Chats/ChatNotInitializedException.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Chats; + +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/Chats/EmptyChatException.cs b/src/MaIN.Domain/Exceptions/Chats/EmptyChatException.cs new file mode 100644 index 00000000..147d62c6 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Chats/EmptyChatException.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Chats; + +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/CommandFailedException.cs b/src/MaIN.Domain/Exceptions/CommandFailedException.cs new file mode 100644 index 00000000..25ce01e7 --- /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/Flows/FlowAlreadyExistsException.cs b/src/MaIN.Domain/Exceptions/Flows/FlowAlreadyExistsException.cs new file mode 100644 index 00000000..530715bb --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Flows/FlowAlreadyExistsException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Flows; + +public class FlowAlreadyExistsException(string flowId) + : MaINCustomException($"Flow with id: '{flowId}' already exists.") +{ + public override string PublicErrorMessage => "Flow already exists."; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.Conflict; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/Flows/FlowFoundException.cs b/src/MaIN.Domain/Exceptions/Flows/FlowFoundException.cs new file mode 100644 index 00000000..cc0d2492 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Flows/FlowFoundException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Flows; + +public class FlowFoundException(string flowId) + : MaINCustomException($"Flow with id: '{flowId}' not found.") +{ + public override string PublicErrorMessage => "Flow not found."; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.NotFound; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/Flows/FlowNotInitializedException.cs b/src/MaIN.Domain/Exceptions/Flows/FlowNotInitializedException.cs new file mode 100644 index 00000000..801b4949 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Flows/FlowNotInitializedException.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Flows; + +public class FlowNotInitializedException() : MaINCustomException("Flow has not been created yet.") +{ + public override string PublicErrorMessage => LogMessage; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.Conflict; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/LLMApiException.cs b/src/MaIN.Domain/Exceptions/LLMApiException.cs new file mode 100644 index 00000000..bc5a35c1 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/LLMApiException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions; + +public class LLMApiException(string llmApiName, HttpStatusCode llmApiHttpStatusCode, string? errorMessage) + : MaINCustomException($"{llmApiName} error. {errorMessage ?? string.Empty}") +{ + public override string PublicErrorMessage => Message; + public override HttpStatusCode HttpStatusCode => llmApiHttpStatusCode; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/MPC/MPCConfigNotFoundException.cs b/src/MaIN.Domain/Exceptions/MPC/MPCConfigNotFoundException.cs new file mode 100644 index 00000000..b89e8e60 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/MPC/MPCConfigNotFoundException.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.MPC; + +public class MPCConfigNotFoundException() : MaINCustomException("MPC configuration not found.") +{ + public override string PublicErrorMessage => LogMessage; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.NotFound; +} \ 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 00000000..2530f8be --- /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, "(? "Model name cannot be null or empty"; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.BadRequest; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/Models/ModelNotDownloadedException.cs b/src/MaIN.Domain/Exceptions/Models/ModelNotDownloadedException.cs new file mode 100644 index 00000000..ebb7e1b0 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Models/ModelNotDownloadedException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Models; + +public class ModelNotDownloadedException(string? modelName) + : MaINCustomException($"Given model {modelName ?? string.Empty} is not downloaded.") +{ + public override string PublicErrorMessage => Message; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.NotFound; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/Models/ModelNotSupportedException.cs b/src/MaIN.Domain/Exceptions/Models/ModelNotSupportedException.cs new file mode 100644 index 00000000..a97c71c7 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Models/ModelNotSupportedException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Models; + +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/Exceptions/Models/ModelsPathNotFoundException.cs b/src/MaIN.Domain/Exceptions/Models/ModelsPathNotFoundException.cs new file mode 100644 index 00000000..c6ae6503 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Models/ModelsPathNotFoundException.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Models; + +public class ModelsPathNotFoundException() : MaINCustomException($"Models path not found in configuration or environment variables.") +{ + public override string PublicErrorMessage => Message; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.NotFound; +} \ No newline at end of file diff --git a/src/MaIN.Domain/Models/SupportedModels.cs b/src/MaIN.Domain/Models/SupportedModels.cs index 6a774a32..d2fd3875 100644 --- a/src/MaIN.Domain/Models/SupportedModels.cs +++ b/src/MaIN.Domain/Models/SupportedModels.cs @@ -1,3 +1,5 @@ +using MaIN.Domain.Exceptions.Models; + namespace MaIN.Domain.Models; public class Model @@ -263,8 +265,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))) @@ -272,7 +273,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) @@ -280,8 +281,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; } @@ -290,7 +290,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) @@ -313,8 +313,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.InferPage/Components/Layout/MainLayout.razor b/src/MaIN.InferPage/Components/Layout/MainLayout.razor index fdf39a6d..0481fa40 100644 --- a/src/MaIN.InferPage/Components/Layout/MainLayout.razor +++ b/src/MaIN.InferPage/Components/Layout/MainLayout.razor @@ -1,5 +1,4 @@ -@using Microsoft.FluentUI.AspNetCore.Components.Icons.Regular -@inherits LayoutComponentBase +@inherits LayoutComponentBase
diff --git a/src/MaIN.InferPage/Components/Layout/NavBar.razor b/src/MaIN.InferPage/Components/Layout/NavBar.razor index 3bc5ea8e..5216c58e 100644 --- a/src/MaIN.InferPage/Components/Layout/NavBar.razor +++ b/src/MaIN.InferPage/Components/Layout/NavBar.razor @@ -1,5 +1,4 @@ @using Microsoft.FluentUI.AspNetCore.Components.Icons.Regular -@using Size32 = Microsoft.FluentUI.AspNetCore.Components.Icons.Filled.Size32 @inject NavigationManager _navigationManager @rendermode @(new InteractiveServerRenderMode(prerender: false)) diff --git a/src/MaIN.InferPage/Components/Pages/ErrorNotification.razor b/src/MaIN.InferPage/Components/Pages/ErrorNotification.razor new file mode 100644 index 00000000..ddf0f5d5 --- /dev/null +++ b/src/MaIN.InferPage/Components/Pages/ErrorNotification.razor @@ -0,0 +1,29 @@ +
+ @if (!string.IsNullOrEmpty(ErrorMessage)) + { +
+ + @ErrorMessage + +
+ } +
+ +@code { + [Parameter] + public string? ErrorMessage { get; set; } + + [Parameter] + public EventCallback ErrorMessageChanged { get; set; } + + private async Task HandleDismiss() + { + ErrorMessage = null; + await ErrorMessageChanged.InvokeAsync(null); + } +} \ No newline at end of file diff --git a/src/MaIN.InferPage/Components/Pages/Home.razor b/src/MaIN.InferPage/Components/Pages/Home.razor index f7496366..0c9c2722 100644 --- a/src/MaIN.InferPage/Components/Pages/Home.razor +++ b/src/MaIN.InferPage/Components/Pages/Home.razor @@ -3,7 +3,9 @@ @inject IJSRuntime JS @using MaIN.Core.Hub @using MaIN.Core.Hub.Contexts +@using MaIN.Core.Hub.Contexts.Interfaces.ChatContext @using MaIN.Domain.Entities +@using MaIN.Domain.Exceptions @using MaIN.Domain.Models @using Markdig @using Microsoft.FluentUI.AspNetCore.Components.Icons.Regular @@ -13,6 +15,8 @@ MaIN Infer + + @@ -183,10 +187,12 @@ private bool _isLoading; private bool _isThinking; private bool _reasoning; + private string? _errorMessage; private string? _incomingMessage = null; private string? _incomingReasoning = null; private readonly string? _displayName = Utils.Model; - private ChatContext? ctx; + private IChatMessageBuilder? ctxBuilder; + // private ChatContext? ctx; private Chat Chat { get; } = new() { Name = "MaIN Infer", Model = Utils.Model! }; private List Messages { get; set; } = new(); private ElementReference? _bottomElement; @@ -202,7 +208,7 @@ protected override Task OnInitializedAsync() { - ctx = Utils.Visual + ctxBuilder = Utils.Visual ? AIHub.Chat().EnableVisual() : Utils.Path != null ? AIHub.Chat().WithCustomModel(model: Utils.Model!, path: Utils.Path) @@ -249,38 +255,59 @@ _prompt = string.Empty; StateHasChanged(); bool wasAtBottom = await JS.InvokeAsync("scrollManager.isAtBottom", "messages-container"); - await ctx!.WithMessage(msg) - .CompleteAsync(changeOfValue: async message => - { - if (message?.Type == TokenType.Reason) - { - _isThinking = true; - _incomingReasoning += message.Text; - } - else if (message?.Type == TokenType.Message) - { - _isThinking = false; - _incomingMessage += message.Text; - } - StateHasChanged(); - if (wasAtBottom) + try + { + await ctxBuilder!.WithMessage(msg) + .CompleteAsync(changeOfValue: async message => { - await JS.InvokeVoidAsync("scrollManager.scrollToBottomSmooth", _bottomElement); - } - }); + if (message?.Type == TokenType.Reason) + { + _isThinking = true; + _incomingReasoning += message.Text; + } + else if (message?.Type == TokenType.Message) + { + _isThinking = false; + _incomingMessage += message.Text; + } - _isLoading = false; - var currentChat = (await ctx.GetCurrentChat()); - Chat.Messages.Add(currentChat.Messages.Last()); - Messages = Chat.Messages.Select(x => new MessageExt() + StateHasChanged(); + if (wasAtBottom) + { + await JS.InvokeVoidAsync("scrollManager.scrollToBottomSmooth", _bottomElement); + } + }); + + _isLoading = false; + var currentChat = (await ctxBuilder.GetCurrentChat()); + Chat.Messages.Add(currentChat.Messages.Last()); + Messages = Chat.Messages.Select(x => new MessageExt() + { + Message = x + }).ToList(); + _incomingReasoning = null; + _incomingMessage = null; + await JS.InvokeVoidAsync("scrollManager.scrollToBottomSmooth", _bottomElement); + StateHasChanged(); + + } + catch (Exception ex) { - Message = x - }).ToList(); - _incomingReasoning = null; - _incomingMessage = null; - await JS.InvokeVoidAsync("scrollManager.scrollToBottomSmooth", _bottomElement); - StateHasChanged(); + _errorMessage = null; + StateHasChanged(); + + _errorMessage = ex is MaINCustomException maInException + ? $"{maInException.PublicErrorMessage}" + : $"{ex.Message}"; + + StateHasChanged(); + } + finally + { + _isLoading = false; + _isThinking = false; + } } } @@ -288,5 +315,4 @@ { _prompt = obj.Value?.ToString()!; } - } \ No newline at end of file diff --git a/src/MaIN.InferPage/Program.cs b/src/MaIN.InferPage/Program.cs index 292101cd..01ad7079 100644 --- a/src/MaIN.InferPage/Program.cs +++ b/src/MaIN.InferPage/Program.cs @@ -3,6 +3,7 @@ using MaIN.Domain.Models; using Microsoft.FluentUI.AspNetCore.Components; using MaIN.InferPage.Components; +using MaIN.Services.Services.LLMService.Utils; using Utils = MaIN.InferPage.Utils; var builder = WebApplication.CreateBuilder(args); @@ -49,32 +50,32 @@ { case "openai": Utils.OpenAi = true; - apiKeyVariable = "OPENAI_API_KEY"; - apiName = "OpenAI"; + apiKeyVariable = LLMApiRegistry.OpenAi.ApiKeyEnvName; + apiName = LLMApiRegistry.OpenAi.ApiName; break; case "gemini": Utils.Gemini = true; - apiKeyVariable = "GEMINI_API_KEY"; - apiName = "Gemini"; + apiKeyVariable = LLMApiRegistry.Gemini.ApiKeyEnvName; + apiName = LLMApiRegistry.Gemini.ApiName; break; case "deepseek": Utils.DeepSeek = true; - apiKeyVariable = "DEEPSEEK_API_KEY"; - apiName = "Deepseek"; + apiKeyVariable = LLMApiRegistry.Deepseek.ApiKeyEnvName; + apiName = LLMApiRegistry.Deepseek.ApiName; break; case "groqcloud": Utils.GroqCloud = true; - apiKeyVariable = "GROQ_API_KEY"; - apiName = "GroqCloud"; + apiKeyVariable = LLMApiRegistry.Groq.ApiKeyEnvName; + apiName = LLMApiRegistry.Groq.ApiName; break; case "anthropic": Utils.Anthropic = true; - apiKeyVariable = "ANTHROPIC_API_KEY"; - apiName = "Anthropic"; + apiKeyVariable = LLMApiRegistry.Anthropic.ApiKeyEnvName; + apiName = LLMApiRegistry.Anthropic.ApiName; break; } diff --git a/src/MaIN.InferPage/wwwroot/home.css b/src/MaIN.InferPage/wwwroot/home.css index 50421e1d..f1101655 100644 --- a/src/MaIN.InferPage/wwwroot/home.css +++ b/src/MaIN.InferPage/wwwroot/home.css @@ -57,4 +57,54 @@ .message-card p { margin: 0; +} + +.error-notification-wrapper { + position: fixed; + top: 30px; + right: 30px; + z-index: 9999; + width: auto; + min-width: 300px; + max-width: 600px; + filter: drop-shadow(0 10px 15px rgba(0, 0, 0, 0.2)); + pointer-events: none; +} + +.error-notification-wrapper > div { + pointer-events: auto; +} + +.high-alert-error { + background-color: #d32f2f !important; + color: #ffffff !important; + border: none !important; + font-size: 1.1rem !important; + + overflow-wrap: break-word !important; + word-wrap: break-word !important; + word-break: break-word !important; + hyphens: auto; +} + +.high-alert-error * { + color: #ffffff !important; + fill: #ffffff !important; + overflow-wrap: break-word !important; +} + +.high-alert-error .content { + padding: 16px !important; + display: block !important; +} + +.error-shake { + animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both; +} + +@keyframes shake { + 10%, 90% { transform: translate3d(-1px, 0, 0); } + 20%, 80% { transform: translate3d(2px, 0, 0); } + 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } + 40%, 60% { transform: translate3d(4px, 0, 0); } } \ No newline at end of file diff --git a/src/MaIN.Infrastructure/Bootstrapper.cs b/src/MaIN.Infrastructure/Bootstrapper.cs index 6c9b4e93..08a12600 100644 --- a/src/MaIN.Infrastructure/Bootstrapper.cs +++ b/src/MaIN.Infrastructure/Bootstrapper.cs @@ -3,6 +3,7 @@ using MaIN.Infrastructure.Repositories; using MaIN.Infrastructure.Repositories.Abstract; using MaIN.Infrastructure.Repositories.FileSystem; +using MaIN.Infrastructure.Repositories.Mongo; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; diff --git a/src/MaIN.Infrastructure/Models/AgentContextDocument.cs b/src/MaIN.Infrastructure/Models/AgentContextDocument.cs index 0da9da33..b603c908 100644 --- a/src/MaIN.Infrastructure/Models/AgentContextDocument.cs +++ b/src/MaIN.Infrastructure/Models/AgentContextDocument.cs @@ -1,5 +1,4 @@ using MaIN.Domain.Entities; -using MaIN.Models.Rag; namespace MaIN.Infrastructure.Models; diff --git a/src/MaIN.Infrastructure/Models/AgentSourceDocument.cs b/src/MaIN.Infrastructure/Models/AgentSourceDocument.cs index 436185fe..50c38437 100644 --- a/src/MaIN.Infrastructure/Models/AgentSourceDocument.cs +++ b/src/MaIN.Infrastructure/Models/AgentSourceDocument.cs @@ -1,4 +1,4 @@ -namespace MaIN.Models.Rag; +namespace MaIN.Infrastructure.Models; public class AgentSourceDocument { diff --git a/src/MaIN.Infrastructure/Models/AgentSourceTypeDocument.cs b/src/MaIN.Infrastructure/Models/AgentSourceTypeDocument.cs index 9c8ef772..48999325 100644 --- a/src/MaIN.Infrastructure/Models/AgentSourceTypeDocument.cs +++ b/src/MaIN.Infrastructure/Models/AgentSourceTypeDocument.cs @@ -1,4 +1,4 @@ -namespace MaIN.Models.Rag; +namespace MaIN.Infrastructure.Models; public enum AgentSourceTypeDocument { diff --git a/src/MaIN.Infrastructure/Repositories/DefaultAgentFlowRepository.cs b/src/MaIN.Infrastructure/Repositories/DefaultAgentFlowRepository.cs index ac119f64..855cca2e 100644 --- a/src/MaIN.Infrastructure/Repositories/DefaultAgentFlowRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/DefaultAgentFlowRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using MaIN.Domain.Exceptions.Flows; using MaIN.Infrastructure.Models; using MaIN.Infrastructure.Repositories.Abstract; @@ -17,7 +18,7 @@ public async Task> GetAllFlows() => public async Task AddFlow(AgentFlowDocument flow) { if (!_flows.TryAdd(flow.Id, flow)) - throw new InvalidOperationException($"Flow with ID {flow.Id} already exists."); + throw new FlowAlreadyExistsException(flow.Id); await Task.CompletedTask; } diff --git a/src/MaIN.Infrastructure/Repositories/DefaultAgentRepository.cs b/src/MaIN.Infrastructure/Repositories/DefaultAgentRepository.cs index 0993f33f..8a94b3ff 100644 --- a/src/MaIN.Infrastructure/Repositories/DefaultAgentRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/DefaultAgentRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using MaIN.Domain.Exceptions.Agents; using MaIN.Infrastructure.Models; using MaIN.Infrastructure.Repositories.Abstract; @@ -20,7 +21,7 @@ public async Task AddAgent(AgentDocument agent) throw new ArgumentNullException(nameof(agent)); if (!_agents.TryAdd(agent.Id, agent)) - throw new InvalidOperationException($"Agent with ID {agent.Id} already exists."); + throw new AgentAlreadyExistsException(agent.Id); await Task.CompletedTask; } @@ -28,7 +29,7 @@ public async Task AddAgent(AgentDocument agent) public async Task UpdateAgent(string id, AgentDocument agent) { if (!_agents.TryUpdate(id, agent, _agents.GetValueOrDefault(id)!)) - throw new KeyNotFoundException($"Agent with ID {id} not found."); + throw new AgentNotFoundException(id); await Task.CompletedTask; } @@ -36,7 +37,7 @@ public async Task UpdateAgent(string id, AgentDocument agent) public async Task DeleteAgent(string id) { if (!_agents.TryRemove(id, out _)) - throw new KeyNotFoundException($"Agent with ID {id} not found."); + throw new AgentNotFoundException(id); await Task.CompletedTask; } diff --git a/src/MaIN.Infrastructure/Repositories/DefaultChatRepository.cs b/src/MaIN.Infrastructure/Repositories/DefaultChatRepository.cs index c8aff5bc..246c9eea 100644 --- a/src/MaIN.Infrastructure/Repositories/DefaultChatRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/DefaultChatRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using MaIN.Domain.Exceptions.Chats; using MaIN.Infrastructure.Models; using MaIN.Infrastructure.Repositories.Abstract; @@ -17,7 +18,7 @@ public async Task> GetAllChats() => public async Task AddChat(ChatDocument chat) { if (!_chats.TryAdd(chat.Id, chat)) - throw new InvalidOperationException($"Chat with ID {chat.Id} already exists."); + throw new ChatAlreadyExistsException(chat.Id); await Task.CompletedTask; } diff --git a/src/MaIN.Infrastructure/Repositories/FileSystem/FileSystemAgentFlowRepository.cs b/src/MaIN.Infrastructure/Repositories/FileSystem/FileSystemAgentFlowRepository.cs index 956219e4..73a2db71 100644 --- a/src/MaIN.Infrastructure/Repositories/FileSystem/FileSystemAgentFlowRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/FileSystem/FileSystemAgentFlowRepository.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using MaIN.Domain.Exceptions.Flows; using MaIN.Infrastructure.Models; using MaIN.Infrastructure.Repositories.Abstract; @@ -45,7 +46,7 @@ public async Task AddFlow(AgentFlowDocument flow) { var filePath = GetFilePath(flow.Id); if (File.Exists(filePath)) - throw new InvalidOperationException($"Flow with ID {flow.Id} already exists."); + throw new FlowAlreadyExistsException(flow.Id); var json = JsonSerializer.Serialize(flow, JsonOptions); await File.WriteAllTextAsync(filePath, json); diff --git a/src/MaIN.Infrastructure/Repositories/FileSystem/FileSystemAgentRepository.cs b/src/MaIN.Infrastructure/Repositories/FileSystem/FileSystemAgentRepository.cs index 8f674666..62f7e805 100644 --- a/src/MaIN.Infrastructure/Repositories/FileSystem/FileSystemAgentRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/FileSystem/FileSystemAgentRepository.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using MaIN.Domain.Exceptions.Agents; using MaIN.Infrastructure.Models; using MaIN.Infrastructure.Repositories.Abstract; @@ -48,7 +49,7 @@ public async Task AddAgent(AgentDocument agent) var filePath = GetFilePath(agent.Id); if (File.Exists(filePath)) - throw new InvalidOperationException($"Agent with ID {agent.Id} already exists."); + throw new AgentAlreadyExistsException(agent.Id); var json = JsonSerializer.Serialize(agent, JsonOptions); await File.WriteAllTextAsync(filePath, json); diff --git a/src/MaIN.Infrastructure/Repositories/FileSystem/FileSystemChatRepository.cs b/src/MaIN.Infrastructure/Repositories/FileSystem/FileSystemChatRepository.cs index 00dc42ad..1748dc11 100644 --- a/src/MaIN.Infrastructure/Repositories/FileSystem/FileSystemChatRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/FileSystem/FileSystemChatRepository.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using MaIN.Domain.Exceptions.Chats; using MaIN.Infrastructure.Models; using MaIN.Infrastructure.Repositories.Abstract; @@ -45,7 +46,7 @@ public async Task AddChat(ChatDocument chat) { var filePath = GetFilePath(chat.Id); if (File.Exists(filePath)) - throw new InvalidOperationException($"Chat with ID {chat.Id} already exists."); + throw new ChatAlreadyExistsException(chat.Id); var json = JsonSerializer.Serialize(chat, JsonOptions); await File.WriteAllTextAsync(filePath, json); diff --git a/src/MaIN.Infrastructure/Repositories/Mongo/MongoAgentFlowRepository.cs b/src/MaIN.Infrastructure/Repositories/Mongo/MongoAgentFlowRepository.cs index ec9e1772..04b91cc9 100644 --- a/src/MaIN.Infrastructure/Repositories/Mongo/MongoAgentFlowRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/Mongo/MongoAgentFlowRepository.cs @@ -2,7 +2,7 @@ using MaIN.Infrastructure.Repositories.Abstract; using MongoDB.Driver; -namespace MaIN.Infrastructure.Repositories; +namespace MaIN.Infrastructure.Repositories.Mongo; public class MongoAgentFlowRepository(IMongoDatabase database, string collectionName) : IAgentFlowRepository { diff --git a/src/MaIN.Infrastructure/Repositories/Mongo/MongoAgentRepository.cs b/src/MaIN.Infrastructure/Repositories/Mongo/MongoAgentRepository.cs index 11435a88..5134adfe 100644 --- a/src/MaIN.Infrastructure/Repositories/Mongo/MongoAgentRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/Mongo/MongoAgentRepository.cs @@ -2,7 +2,7 @@ using MaIN.Infrastructure.Repositories.Abstract; using MongoDB.Driver; -namespace MaIN.Infrastructure.Repositories; +namespace MaIN.Infrastructure.Repositories.Mongo; public class MongoAgentRepository(IMongoDatabase database, string collectionName) : IAgentRepository { diff --git a/src/MaIN.Infrastructure/Repositories/Mongo/MongoChatRepository.cs b/src/MaIN.Infrastructure/Repositories/Mongo/MongoChatRepository.cs index f844fd3b..a395199a 100644 --- a/src/MaIN.Infrastructure/Repositories/Mongo/MongoChatRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/Mongo/MongoChatRepository.cs @@ -2,7 +2,7 @@ using MaIN.Infrastructure.Repositories.Abstract; using MongoDB.Driver; -namespace MaIN.Infrastructure.Repositories; +namespace MaIN.Infrastructure.Repositories.Mongo; public class MongoChatRepository(IMongoDatabase database, string collectionName) : IChatRepository { diff --git a/src/MaIN.Infrastructure/Repositories/Sqlite/SqliteChatRepository.cs b/src/MaIN.Infrastructure/Repositories/Sqlite/SqliteChatRepository.cs index cb74b115..0fbdfdfa 100644 --- a/src/MaIN.Infrastructure/Repositories/Sqlite/SqliteChatRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/Sqlite/SqliteChatRepository.cs @@ -5,6 +5,8 @@ using MaIN.Infrastructure.Models; using MaIN.Infrastructure.Repositories.Abstract; +namespace MaIN.Infrastructure.Repositories.Sqlite; + public class SqliteChatRepository(IDbConnection connection) : IChatRepository { private readonly JsonSerializerOptions? _jsonOptions = new() diff --git a/src/MaIN.Services/Bootstrapper.cs b/src/MaIN.Services/Bootstrapper.cs index 52ab940e..6f6ea373 100644 --- a/src/MaIN.Services/Bootstrapper.cs +++ b/src/MaIN.Services/Bootstrapper.cs @@ -11,6 +11,7 @@ using MaIN.Services.Services.Models.Commands; using MaIN.Services.Services.Steps; using MaIN.Services.Services.Steps.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; using MaIN.Services.Services.TTSService; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/src/MaIN.Services/Mappers/AgentMapper.cs b/src/MaIN.Services/Mappers/AgentMapper.cs index 1365cebf..87503f8c 100644 --- a/src/MaIN.Services/Mappers/AgentMapper.cs +++ b/src/MaIN.Services/Mappers/AgentMapper.cs @@ -2,7 +2,6 @@ using MaIN.Domain.Entities.Agents; using MaIN.Domain.Entities.Agents.AgentSource; using MaIN.Infrastructure.Models; -using MaIN.Models.Rag; using MaIN.Services.Dtos.Rag; using MaIN.Services.Dtos.Rag.AgentSource; diff --git a/src/MaIN.Services/Mappers/ChatMapper.cs b/src/MaIN.Services/Mappers/ChatMapper.cs index c35819ce..4ccff914 100644 --- a/src/MaIN.Services/Mappers/ChatMapper.cs +++ b/src/MaIN.Services/Mappers/ChatMapper.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using LLama.Batched; using MaIN.Domain.Entities; using MaIN.Domain.Models; diff --git a/src/MaIN.Services/Services/Abstract/IChatService.cs b/src/MaIN.Services/Services/Abstract/IChatService.cs index 1984306c..c5ffb5f6 100644 --- a/src/MaIN.Services/Services/Abstract/IChatService.cs +++ b/src/MaIN.Services/Services/Abstract/IChatService.cs @@ -1,6 +1,5 @@ using MaIN.Domain.Entities; using MaIN.Domain.Models; -using MaIN.Services.Dtos; using MaIN.Services.Services.Models; namespace MaIN.Services.Services.Abstract; diff --git a/src/MaIN.Services/Services/Abstract/IImageGenService.cs b/src/MaIN.Services/Services/Abstract/IImageGenService.cs index ca60763f..fc268787 100644 --- a/src/MaIN.Services/Services/Abstract/IImageGenService.cs +++ b/src/MaIN.Services/Services/Abstract/IImageGenService.cs @@ -1,5 +1,4 @@ using MaIN.Domain.Entities; -using MaIN.Services.Dtos; using MaIN.Services.Services.Models; namespace MaIN.Services.Services.Abstract; diff --git a/src/MaIN.Services/Services/Abstract/ILLMService.cs b/src/MaIN.Services/Services/Abstract/ILLMService.cs index 1547c43c..2722162c 100644 --- a/src/MaIN.Services/Services/Abstract/ILLMService.cs +++ b/src/MaIN.Services/Services/Abstract/ILLMService.cs @@ -1,6 +1,4 @@ using MaIN.Domain.Entities; -using MaIN.Domain.Models; -using MaIN.Services.Dtos; using MaIN.Services.Services.LLMService; using MaIN.Services.Services.Models; diff --git a/src/MaIN.Services/Services/AgentFlowService.cs b/src/MaIN.Services/Services/AgentFlowService.cs index 762a1663..b6937111 100644 --- a/src/MaIN.Services/Services/AgentFlowService.cs +++ b/src/MaIN.Services/Services/AgentFlowService.cs @@ -1,5 +1,5 @@ using MaIN.Domain.Entities.Agents.AgentSource; -using MaIN.Infrastructure.Models; +using MaIN.Domain.Exceptions.Agents; using MaIN.Infrastructure.Repositories.Abstract; using MaIN.Services.Mappers; using MaIN.Services.Services.Abstract; @@ -11,8 +11,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 a0274da8..c43c1db6 100644 --- a/src/MaIN.Services/Services/AgentService.cs +++ b/src/MaIN.Services/Services/AgentService.cs @@ -5,6 +5,7 @@ using MaIN.Domain.Entities.Agents.Knowledge; using MaIN.Domain.Entities.Tools; using MaIN.Domain.Models; +using MaIN.Domain.Exceptions.Agents; using MaIN.Infrastructure.Repositories.Abstract; using MaIN.Services.Constants; using MaIN.Services.Mappers; @@ -12,7 +13,7 @@ using MaIN.Services.Services.ImageGenServices; using MaIN.Services.Services.LLMService.Factory; using MaIN.Services.Services.Models.Commands; -using MaIN.Services.Services.Steps.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; using MaIN.Services.Utils; using Microsoft.Extensions.Logging; using static System.Text.RegularExpressions.Regex; @@ -39,10 +40,15 @@ public async Task Process( Func? callbackTool = null) { 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, "STARTED"), "ReceiveAgentUpdate"); @@ -138,7 +144,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(); @@ -148,7 +156,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 cc766ef6..193d8808 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.Chats; using MaIN.Domain.Models; using MaIN.Infrastructure.Repositories.Abstract; using MaIN.Services.Mappers; @@ -99,7 +100,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 8b26de46..1dce392c 100644 --- a/src/MaIN.Services/Services/DataSourceProvider.cs +++ b/src/MaIN.Services/Services/DataSourceProvider.cs @@ -1,4 +1,5 @@ using MaIN.Domain.Entities.Agents.AgentSource; +using MaIN.Domain.Exceptions; using MaIN.Services.Services.Abstract; using MaIN.Services.Utils; using Microsoft.IdentityModel.Tokens; @@ -158,8 +159,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/ImageGenServices/GeminiImageGenService.cs b/src/MaIN.Services/Services/ImageGenServices/GeminiImageGenService.cs index 2926238f..8faffa08 100644 --- a/src/MaIN.Services/Services/ImageGenServices/GeminiImageGenService.cs +++ b/src/MaIN.Services/Services/ImageGenServices/GeminiImageGenService.cs @@ -6,6 +6,8 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json.Serialization; +using MaIN.Domain.Exceptions; +using MaIN.Services.Services.LLMService.Utils; namespace MaIN.Services.Services.ImageGenServices; @@ -17,8 +19,8 @@ internal class GeminiImageGenService(IHttpClientFactory httpClientFactory, MaINS public async Task Send(Chat chat) { var client = _httpClientFactory.CreateClient(ServiceConstants.HttpClients.GeminiClient); - string apiKey = _settings.GeminiKey ?? Environment.GetEnvironmentVariable("GEMINI_API_KEY") - ?? throw new InvalidOperationException("Gemini API key is not configured"); + string apiKey = _settings.GeminiKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.Gemini.ApiKeyEnvName) + ?? throw new APIKeyNotConfiguredException(LLMApiRegistry.Gemini.ApiName); if (string.IsNullOrEmpty(chat.Model)) { diff --git a/src/MaIN.Services/Services/ImageGenServices/ImageGenDalleService.cs b/src/MaIN.Services/Services/ImageGenServices/ImageGenDalleService.cs index eb1c91f5..7a0e2543 100644 --- a/src/MaIN.Services/Services/ImageGenServices/ImageGenDalleService.cs +++ b/src/MaIN.Services/Services/ImageGenServices/ImageGenDalleService.cs @@ -2,8 +2,10 @@ using System.Net.Http.Json; using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions; using MaIN.Services.Constants; using MaIN.Services.Services.Abstract; +using MaIN.Services.Services.LLMService.Utils; using MaIN.Services.Services.Models; namespace MaIN.Services.Services.ImageGenServices; @@ -19,8 +21,8 @@ public class OpenAiImageGenService( public async Task Send(Chat chat) { var client = _httpClientFactory.CreateClient(ServiceConstants.HttpClients.OpenAiClient); - string apiKey = _settings.OpenAiKey ?? Environment.GetEnvironmentVariable("OPENAI_API_KEY") - ?? throw new InvalidOperationException("OpenAI API key is not configured"); + string apiKey = _settings.OpenAiKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.OpenAi.ApiKeyEnvName) + ?? throw new APIKeyNotConfiguredException(LLMApiRegistry.OpenAi.ApiName); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); var requestBody = new @@ -84,7 +86,6 @@ private struct Models } } - file class OpenAiImageResponse { public ImageData[] Data { get; set; } = []; diff --git a/src/MaIN.Services/Services/ImageGenServices/ImageGenService.cs b/src/MaIN.Services/Services/ImageGenServices/ImageGenService.cs index d060a4f6..56e67055 100644 --- a/src/MaIN.Services/Services/ImageGenServices/ImageGenService.cs +++ b/src/MaIN.Services/Services/ImageGenServices/ImageGenService.cs @@ -1,7 +1,3 @@ -using System; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Services.Constants; diff --git a/src/MaIN.Services/Services/ImageGenServices/XaiImageGenService.cs b/src/MaIN.Services/Services/ImageGenServices/XaiImageGenService.cs index e6be9f6c..ac3ce8c7 100644 --- a/src/MaIN.Services/Services/ImageGenServices/XaiImageGenService.cs +++ b/src/MaIN.Services/Services/ImageGenServices/XaiImageGenService.cs @@ -6,6 +6,8 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; +using MaIN.Domain.Exceptions; +using MaIN.Services.Services.LLMService.Utils; namespace MaIN.Services.Services.ImageGenServices; @@ -20,8 +22,8 @@ public class XaiImageGenService( public async Task Send(Chat chat) { var client = _httpClientFactory.CreateClient(ServiceConstants.HttpClients.XaiClient); - string apiKey = _settings.XaiKey ?? Environment.GetEnvironmentVariable("XAI_API_KEY") ?? - throw new InvalidOperationException("xAI Key not configured"); + string apiKey = _settings.XaiKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.Xai.ApiKeyEnvName) ?? + throw new APIKeyNotConfiguredException(LLMApiRegistry.Xai.ApiName); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); var requestBody = new diff --git a/src/MaIN.Services/Services/LLMService/AnthropicService.cs b/src/MaIN.Services/Services/LLMService/AnthropicService.cs index 56b4554f..9685cd5b 100644 --- a/src/MaIN.Services/Services/LLMService/AnthropicService.cs +++ b/src/MaIN.Services/Services/LLMService/AnthropicService.cs @@ -11,8 +11,8 @@ using LLama.Common; using MaIN.Domain.Configuration; using MaIN.Domain.Entities.Tools; +using MaIN.Domain.Exceptions; using MaIN.Services.Services.LLMService.Utils; -using MaIN.Services.Services.LLMService; namespace MaIN.Services.Services.LLMService; @@ -42,15 +42,16 @@ private HttpClient CreateAnthropicHttpClient() private string GetApiKey() { - return _settings.AnthropicKey ?? Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? - throw new InvalidOperationException("Anthropic Key not configured"); + return _settings.AnthropicKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.Anthropic.ApiKeyEnvName) ?? + throw new APIKeyNotConfiguredException(LLMApiRegistry.Anthropic.ApiName); } private void ValidateApiKey() { - if (string.IsNullOrEmpty(_settings.AnthropicKey) && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY"))) + if (string.IsNullOrEmpty(_settings.AnthropicKey) && + string.IsNullOrEmpty(Environment.GetEnvironmentVariable(LLMApiRegistry.Anthropic.ApiKeyEnvName))) { - throw new InvalidOperationException("Anthropic Key not configured"); + throw new APIKeyNotConfiguredException(LLMApiRegistry.Anthropic.ApiName); } } @@ -83,6 +84,7 @@ await options.TokenCallback(new LLMTokenValue() Type = TokenType.FullAnswer }); } + return CreateChatResult(chat, resultBuilder.ToString(), tokens); } @@ -154,7 +156,7 @@ private async Task ProcessWithToolsAsync( { var spaceToken = new LLMTokenValue { Text = " ", Type = TokenType.Message }; tokens.Add(spaceToken); - + if (options.TokenCallback != null) await options.TokenCallback(spaceToken); @@ -193,6 +195,7 @@ await notificationService.DispatchNotification( { fullResponseBuilder.Append(" "); } + fullResponseBuilder.Append(resultBuilder); } @@ -206,6 +209,7 @@ await notificationService.DispatchNotification( { assistantContent.Add(new { type = "text", text = resultBuilder.ToString() }); } + assistantContent.AddRange(currentToolUses.Select(tu => new { type = "tool_use", @@ -227,7 +231,7 @@ await notificationService.DispatchNotification( toolUse.Name), ServiceConstants.Notifications.ReceiveAgentUpdate); } - + var executor = chat.ToolsConfiguration?.GetExecutor(toolUse.Name); if (executor == null) @@ -327,7 +331,10 @@ await notificationService.DispatchNotification( HttpCompletionOption.ResponseHeadersRead, cancellationToken); - response.EnsureSuccessStatusCode(); + if (!response.IsSuccessStatusCode) + { + await HandleApiError(response, cancellationToken); + } await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); using var reader = new StreamReader(stream); @@ -389,7 +396,7 @@ await notificationService.DispatchNotification( } else if (chunk.Delta?.Type == "input_json_delta") { - if (toolUseBuilders.ContainsKey(chunk.Index) && + if (toolUseBuilders.ContainsKey(chunk.Index) && !string.IsNullOrEmpty(chunk.Delta.PartialJson)) { toolUseBuilders[chunk.Index].InputJson.Append(chunk.Delta.PartialJson); @@ -415,6 +422,34 @@ await notificationService.DispatchNotification( return null; } + + private async Task HandleApiError(HttpResponseMessage response, CancellationToken cancellationToken = default) + { + var errorResponseBody = await response.Content.ReadAsStringAsync(cancellationToken); + var errorMessage = ExtractApiErrorMessage(errorResponseBody); + + throw new LLMApiException(LLMApiRegistry.Anthropic.ApiName, response.StatusCode, errorMessage ?? errorResponseBody); + } + + private static string? ExtractApiErrorMessage(string json) + { + try + { + using var jasonDocument = JsonDocument.Parse(json); + if (jasonDocument.RootElement.TryGetProperty("error", out var error) && + error.TryGetProperty("message", out var message)) + { + return message.GetString(); + } + } + catch (JsonException) + { + // If the response is not a valid JSON or doesn't match the expected schema, + // we fall back to the raw response body in the calling method. + } + + return null; + } private async Task?> ProcessNonStreamingChatWithToolsAsync( Chat chat, @@ -430,7 +465,11 @@ await notificationService.DispatchNotification( var content = new StringContent(requestJson, Encoding.UTF8, "application/json"); using var response = await httpClient.PostAsync(CompletionsUrl, content, cancellationToken); - response.EnsureSuccessStatusCode(); + + if (!response.IsSuccessStatusCode) + { + await HandleApiError(response, cancellationToken); + } var responseJson = await response.Content.ReadAsStringAsync(cancellationToken); var chatResponse = JsonSerializer.Deserialize(responseJson, @@ -470,10 +509,10 @@ private object BuildAnthropicRequestBody(Chat chat, List conversati ["stream"] = stream, ["messages"] = BuildAnthropicMessages(conversation) }; - - var systemMessage = conversation.FirstOrDefault(m => + + var systemMessage = conversation.FirstOrDefault(m => m.Role.Equals("system", StringComparison.OrdinalIgnoreCase)); - + if (systemMessage != null && systemMessage.Content is string systemContent) { requestBody["system"] = systemContent; @@ -481,7 +520,8 @@ private object BuildAnthropicRequestBody(Chat chat, List conversati if (chat.InterferenceParams.Grammar is not null) { - requestBody["system"] = $"Respond only using the following grammar format: \n{chat.InterferenceParams.Grammar.Value}\n. Do not add explanations, code tags, or any extra content."; + requestBody["system"] = + $"Respond only using the following grammar format: \n{chat.InterferenceParams.Grammar.Value}\n. Do not add explanations, code tags, or any extra content."; } if (chat.ToolsConfiguration?.Tools != null && chat.ToolsConfiguration.Tools.Any()) @@ -505,7 +545,7 @@ private List BuildAnthropicMessages(List conversation) { if (msg.Role.Equals("system", StringComparison.OrdinalIgnoreCase)) continue; - + object content; if (msg.Content is string textContent) @@ -531,7 +571,8 @@ private List BuildAnthropicMessages(List conversation) return messages; } - public async Task AskMemory(Chat chat, ChatMemoryOptions memoryOptions, ChatRequestOptions requestOptions, CancellationToken cancellationToken = default) + public async Task AskMemory(Chat chat, ChatMemoryOptions memoryOptions, ChatRequestOptions requestOptions, + CancellationToken cancellationToken = default) { throw new NotSupportedException("Embeddings are not supported by the Anthropic. Document reading requires embedding support."); } @@ -542,10 +583,15 @@ public async Task GetCurrentModels() var httpClient = CreateAnthropicHttpClient(); using var response = await httpClient.GetAsync(ModelsUrl); - response.EnsureSuccessStatusCode(); + + if (!response.IsSuccessStatusCode) + { + await HandleApiError(response); + } var json = await response.Content.ReadAsStringAsync(); - var modelResponse = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + var modelResponse = + JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return modelResponse?.Data?.Select(m => m.Id).ToArray() ?? []; } @@ -602,7 +648,9 @@ private async Task ProcessStreamingChatAsync( model = chat.Model, max_tokens = chat.InterferenceParams.MaxTokens < 0 ? 4096 : chat.InterferenceParams.MaxTokens, stream = true, - system = chat.InterferenceParams.Grammar is not null ? $"Respond only using the following grammar format: \n{chat.InterferenceParams.Grammar.Value}\n. Do not add explanations, code tags, or any extra content." : "", + system = chat.InterferenceParams.Grammar is not null + ? $"Respond only using the following grammar format: \n{chat.InterferenceParams.Grammar.Value}\n. Do not add explanations, code tags, or any extra content." + : "", messages = await OpenAiCompatibleService.BuildMessagesArray(conversation, chat, ImageType.AsBase64) }; @@ -619,7 +667,10 @@ private async Task ProcessStreamingChatAsync( HttpCompletionOption.ResponseHeadersRead, cancellationToken); - response.EnsureSuccessStatusCode(); + if (!response.IsSuccessStatusCode) + { + await HandleApiError(response, cancellationToken); + } await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); using var reader = new StreamReader(stream); @@ -636,9 +687,9 @@ private async Task ProcessStreamingChatAsync( try { - var chunk = JsonSerializer.Deserialize(data, + var chunk = JsonSerializer.Deserialize(data, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - + if (chunk?.Delta?.Type == "text_delta" && !string.IsNullOrEmpty(chunk.Delta.Text)) { var token = new LLMTokenValue @@ -646,7 +697,7 @@ private async Task ProcessStreamingChatAsync( Text = chunk.Delta.Text, Type = TokenType.Message }; - + tokens.Add(token); if (tokenCallback != null) @@ -685,7 +736,9 @@ private async Task ProcessNonStreamingChatAsync( model = chat.Model, max_tokens = chat.InterferenceParams.MaxTokens < 0 ? 4096 : chat.InterferenceParams.MaxTokens, stream = false, - system = chat.InterferenceParams.Grammar is not null ? $"Respond only using the following grammar format: \n{chat.InterferenceParams.Grammar.Value}\n. Do not add explanations, code tags, or any extra content." : "", + system = chat.InterferenceParams.Grammar is not null + ? $"Respond only using the following grammar format: \n{chat.InterferenceParams.Grammar.Value}\n. Do not add explanations, code tags, or any extra content." + : "", messages = await OpenAiCompatibleService.BuildMessagesArray(conversation, chat, ImageType.AsBase64) }; @@ -693,10 +746,16 @@ private async Task ProcessNonStreamingChatAsync( var content = new StringContent(requestJson, Encoding.UTF8, "application/json"); using var response = await httpClient.PostAsync(CompletionsUrl, content, cancellationToken); - response.EnsureSuccessStatusCode(); + + if (!response.IsSuccessStatusCode) + { + await HandleApiError(response, cancellationToken); + } var responseJson = await response.Content.ReadAsStringAsync(cancellationToken); - var chatResponse = JsonSerializer.Deserialize(responseJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + var chatResponse = + JsonSerializer.Deserialize(responseJson, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); var message = chatResponse?.Content?.FirstOrDefault()?.Text; if (!string.IsNullOrWhiteSpace(message)) @@ -740,7 +799,7 @@ public AnthropicToolUse Build() { object input; var jsonString = InputJson.ToString().Trim(); - + if (string.IsNullOrWhiteSpace(jsonString)) { input = new { }; @@ -756,7 +815,7 @@ public AnthropicToolUse Build() input = new { }; } } - + return new AnthropicToolUse { Id = Id, @@ -784,13 +843,13 @@ file class AnthropicStreamChunk { [System.Text.Json.Serialization.JsonPropertyName("type")] public string? Type { get; set; } - + [System.Text.Json.Serialization.JsonPropertyName("index")] public int Index { get; set; } - + [System.Text.Json.Serialization.JsonPropertyName("delta")] public AnthropicDelta? Delta { get; set; } - + [System.Text.Json.Serialization.JsonPropertyName("content_block")] public AnthropicContentBlock? ContentBlock { get; set; } } @@ -799,13 +858,13 @@ file class AnthropicContentBlock { [System.Text.Json.Serialization.JsonPropertyName("type")] public string? Type { get; set; } - + [System.Text.Json.Serialization.JsonPropertyName("id")] public string? Id { get; set; } - + [System.Text.Json.Serialization.JsonPropertyName("name")] public string? Name { get; set; } - + [System.Text.Json.Serialization.JsonPropertyName("input")] public object? Input { get; set; } } @@ -814,10 +873,10 @@ file class AnthropicDelta { [System.Text.Json.Serialization.JsonPropertyName("type")] public string? Type { get; set; } - + [System.Text.Json.Serialization.JsonPropertyName("text")] public string? Text { get; set; } - + [System.Text.Json.Serialization.JsonPropertyName("partial_json")] public string? PartialJson { get; set; } } diff --git a/src/MaIN.Services/Services/LLMService/DeepSeekService.cs b/src/MaIN.Services/Services/LLMService/DeepSeekService.cs index ec617d5d..f632f138 100644 --- a/src/MaIN.Services/Services/LLMService/DeepSeekService.cs +++ b/src/MaIN.Services/Services/LLMService/DeepSeekService.cs @@ -9,6 +9,8 @@ using MaIN.Domain.Models; using System.Text.Json; using System.Text.Json.Serialization; +using MaIN.Domain.Exceptions; +using MaIN.Services.Services.LLMService.Utils; namespace MaIN.Services.Services.LLMService; @@ -32,16 +34,18 @@ public sealed class DeepSeekService( protected override string GetApiKey() { - return _settings.DeepSeekKey ?? Environment.GetEnvironmentVariable("DEEPSEEK_API_KEY") ?? - throw new InvalidOperationException("DeepSeek Key not configured"); + return _settings.DeepSeekKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.Deepseek.ApiKeyEnvName) ?? + throw new APIKeyNotConfiguredException(LLMApiRegistry.Deepseek.ApiName); } + protected override string GetApiName() => LLMApiRegistry.Deepseek.ApiName; + protected override void ValidateApiKey() { if (string.IsNullOrEmpty(_settings.DeepSeekKey) && - string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DEEPSEEK_API_KEY"))) + string.IsNullOrEmpty(Environment.GetEnvironmentVariable(LLMApiRegistry.Deepseek.ApiKeyEnvName))) { - throw new InvalidOperationException("DeepSeek Key not configured"); + throw new APIKeyNotConfiguredException(LLMApiRegistry.Deepseek.ApiName); } } diff --git a/src/MaIN.Services/Services/LLMService/GeminiService.cs b/src/MaIN.Services/Services/LLMService/GeminiService.cs index 9e62372a..5d741498 100644 --- a/src/MaIN.Services/Services/LLMService/GeminiService.cs +++ b/src/MaIN.Services/Services/LLMService/GeminiService.cs @@ -9,7 +9,9 @@ using System.Text.Json; using System.Text.Json.Serialization; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions; using MaIN.Domain.Models; +using MaIN.Services.Services.LLMService.Utils; using MaIN.Services.Utils; namespace MaIN.Services.Services.LLMService; @@ -58,16 +60,18 @@ public override async Task GetCurrentModels() protected override string GetApiKey() { - return _settings.GeminiKey ?? Environment.GetEnvironmentVariable("GEMINI_API_KEY") ?? - throw new InvalidOperationException("Gemini Key not configured"); + return _settings.GeminiKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.Gemini.ApiKeyEnvName) ?? + throw new APIKeyNotConfiguredException(LLMApiRegistry.Gemini.ApiName); } + protected override string GetApiName() => LLMApiRegistry.Gemini.ApiName; + protected override void ValidateApiKey() { if (string.IsNullOrEmpty(_settings.GeminiKey) && - string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GEMINI_API_KEY"))) + string.IsNullOrEmpty(Environment.GetEnvironmentVariable(LLMApiRegistry.Gemini.ApiKeyEnvName))) { - throw new InvalidOperationException("Gemini Key not configured"); + throw new APIKeyNotConfiguredException(LLMApiRegistry.Gemini.ApiName); } } diff --git a/src/MaIN.Services/Services/LLMService/GroqCloudService.cs b/src/MaIN.Services/Services/LLMService/GroqCloudService.cs index 79dbf304..c64f6593 100644 --- a/src/MaIN.Services/Services/LLMService/GroqCloudService.cs +++ b/src/MaIN.Services/Services/LLMService/GroqCloudService.cs @@ -1,10 +1,12 @@ using System.Text; using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions; using MaIN.Services.Services.Abstract; using Microsoft.Extensions.Logging; using MaIN.Services.Services.LLMService.Memory; using MaIN.Services.Constants; +using MaIN.Services.Services.LLMService.Utils; using MaIN.Services.Services.Models; namespace MaIN.Services.Services.LLMService; @@ -26,15 +28,18 @@ public sealed class GroqCloudService( protected override string GetApiKey() { - return _settings.GroqCloudKey ?? Environment.GetEnvironmentVariable("GROQ_API_KEY") ?? - throw new InvalidOperationException("GroqCloud Key not configured"); + return _settings.GroqCloudKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.Groq.ApiKeyEnvName) ?? + throw new APIKeyNotConfiguredException(LLMApiRegistry.Groq.ApiName); } + protected override string GetApiName() => LLMApiRegistry.Groq.ApiName; + protected override void ValidateApiKey() { - if (string.IsNullOrEmpty(_settings.GroqCloudKey) && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GROQ_API_KEY"))) + if (string.IsNullOrEmpty(_settings.GroqCloudKey) && + string.IsNullOrEmpty(Environment.GetEnvironmentVariable(LLMApiRegistry.Groq.ApiKeyEnvName))) { - throw new InvalidOperationException("GroqCloud Key not configured"); + throw new APIKeyNotConfiguredException(LLMApiRegistry.Groq.ApiName); } } @@ -63,14 +68,14 @@ protected override void ValidateApiKey() private string ComposeMessage(Message lastMsg, string[] filePaths) { var stringBuilder = new StringBuilder(); - stringBuilder.AppendLine($"== FILES IN MEMORY"); + stringBuilder.AppendLine("== FILES IN MEMORY"); foreach (var path in filePaths) { var doc = DocumentProcessor.ProcessDocument(path); stringBuilder.Append(doc); stringBuilder.AppendLine(); } - stringBuilder.AppendLine($"== END OF FILES"); + stringBuilder.AppendLine("== END OF FILES"); stringBuilder.AppendLine(); stringBuilder.Append(lastMsg.Content); return stringBuilder.ToString(); diff --git a/src/MaIN.Services/Services/LLMService/LLMService.cs b/src/MaIN.Services/Services/LLMService/LLMService.cs index f45bcea7..ceb539e9 100644 --- a/src/MaIN.Services/Services/LLMService/LLMService.cs +++ b/src/MaIN.Services/Services/LLMService/LLMService.cs @@ -7,6 +7,7 @@ using LLama.Sampling; using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions.Models; using MaIN.Domain.Models; using MaIN.Services.Constants; using MaIN.Services.Services.Abstract; @@ -436,12 +437,9 @@ private BaseSamplingPipeline CreateSampler(InferenceParams interferenceParams) private string GetModelsPath() { var path = options.ModelsPath ?? Environment.GetEnvironmentVariable(DEFAULT_MODEL_ENV_PATH); - if (string.IsNullOrEmpty(path)) - { - throw new InvalidOperationException("Models path not found in configuration or environment variables"); - } - - return path; + return string.IsNullOrEmpty(path) + ? throw new ModelsPathNotFoundException() + : path; } private async Task CreateChatResult(Chat chat, List tokens, diff --git a/src/MaIN.Services/Services/LLMService/Memory/Embeddings/LLamaSharpTextEmbeddingMaINClone.cs b/src/MaIN.Services/Services/LLMService/Memory/Embeddings/LLamaSharpTextEmbeddingMaINClone.cs index 93716eaa..b8b99d44 100644 --- a/src/MaIN.Services/Services/LLMService/Memory/Embeddings/LLamaSharpTextEmbeddingMaINClone.cs +++ b/src/MaIN.Services/Services/LLMService/Memory/Embeddings/LLamaSharpTextEmbeddingMaINClone.cs @@ -1,13 +1,13 @@ +using System.Text; using LLama; +using LLama.Abstractions; using LLama.Common; using LLama.Native; +using LLamaSharp.KernelMemory; using Microsoft.KernelMemory; using Microsoft.KernelMemory.AI; -using System.Text; -using LLama.Abstractions; -using MaIN.Services.Services.LLMService.Memory.Embeddings; -namespace LLamaSharp.KernelMemory +namespace MaIN.Services.Services.LLMService.Memory.Embeddings { /// /// Provides text embedding generation for LLamaSharp. - Clone in MaIN.Package to support embeddings in custom manner diff --git a/src/MaIN.Services/Services/LLMService/Memory/IMemoryFactory.cs b/src/MaIN.Services/Services/LLMService/Memory/IMemoryFactory.cs index e23ca8bc..c411ae63 100644 --- a/src/MaIN.Services/Services/LLMService/Memory/IMemoryFactory.cs +++ b/src/MaIN.Services/Services/LLMService/Memory/IMemoryFactory.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; using LLama; -using LLamaSharp.KernelMemory; using MaIN.Domain.Entities; +using MaIN.Services.Services.LLMService.Memory.Embeddings; using Microsoft.KernelMemory; namespace MaIN.Services.Services.LLMService.Memory; diff --git a/src/MaIN.Services/Services/LLMService/Memory/IMemoryService.cs b/src/MaIN.Services/Services/LLMService/Memory/IMemoryService.cs index 2521af32..84cfbba7 100644 --- a/src/MaIN.Services/Services/LLMService/Memory/IMemoryService.cs +++ b/src/MaIN.Services/Services/LLMService/Memory/IMemoryService.cs @@ -1,4 +1,3 @@ -using LLamaSharp.KernelMemory; using Microsoft.KernelMemory; using Microsoft.KernelMemory.AI; diff --git a/src/MaIN.Services/Services/LLMService/Memory/KernelMemoryLlamaExtensions.cs b/src/MaIN.Services/Services/LLMService/Memory/KernelMemoryLlamaExtensions.cs index d16141d5..fe5f90b5 100644 --- a/src/MaIN.Services/Services/LLMService/Memory/KernelMemoryLlamaExtensions.cs +++ b/src/MaIN.Services/Services/LLMService/Memory/KernelMemoryLlamaExtensions.cs @@ -2,8 +2,8 @@ using LLama.Batched; using LLama.Common; using LLama.Sampling; -using LLamaSharp.KernelMemory; using MaIN.Domain.Entities; +using MaIN.Services.Services.LLMService.Memory.Embeddings; using Microsoft.KernelMemory; using Microsoft.KernelMemory.AI; using InferenceParams = LLama.Common.InferenceParams; diff --git a/src/MaIN.Services/Services/LLMService/Memory/MemoryFactory.cs b/src/MaIN.Services/Services/LLMService/Memory/MemoryFactory.cs index a7ff70a7..c8af60b0 100644 --- a/src/MaIN.Services/Services/LLMService/Memory/MemoryFactory.cs +++ b/src/MaIN.Services/Services/LLMService/Memory/MemoryFactory.cs @@ -3,7 +3,9 @@ using LLama.Common; using LLamaSharp.KernelMemory; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions.Models; using MaIN.Domain.Models; +using MaIN.Services.Services.LLMService.Memory.Embeddings; using Microsoft.KernelMemory; using Microsoft.KernelMemory.Configuration; using Microsoft.KernelMemory.SemanticKernel; @@ -83,12 +85,9 @@ private string ResolvePath(string modelsPath) { var path = modelsPath; - if (string.IsNullOrEmpty(path)) - { - throw new InvalidOperationException("Models path not found"); - } - - return path; + return string.IsNullOrEmpty(path) + ? throw new ModelsPathNotFoundException() + : path; } private static LLamaSharpTextEmbeddingMaINClone ConfigureGeneratorOptions(string embeddingModelPath, diff --git a/src/MaIN.Services/Services/LLMService/Memory/MemoryService.cs b/src/MaIN.Services/Services/LLMService/Memory/MemoryService.cs index bcc51c0a..7485557a 100644 --- a/src/MaIN.Services/Services/LLMService/Memory/MemoryService.cs +++ b/src/MaIN.Services/Services/LLMService/Memory/MemoryService.cs @@ -1,5 +1,6 @@ using LLama.Native; -using LLamaSharp.KernelMemory; +using MaIN.Services.Services.LLMService.Memory.Embeddings; +using MaIN.Services.Utils; using Microsoft.KernelMemory; using Microsoft.KernelMemory.AI; diff --git a/src/MaIN.Services/Services/LLMService/OpenAiCompatibleService.cs b/src/MaIN.Services/Services/LLMService/OpenAiCompatibleService.cs index e2b527e9..feeb26b9 100644 --- a/src/MaIN.Services/Services/LLMService/OpenAiCompatibleService.cs +++ b/src/MaIN.Services/Services/LLMService/OpenAiCompatibleService.cs @@ -16,6 +16,7 @@ using MaIN.Services.Services.LLMService.Memory; using LLama.Common; using MaIN.Domain.Entities.Tools; +using MaIN.Domain.Exceptions; namespace MaIN.Services.Services.LLMService; @@ -43,6 +44,7 @@ public abstract class OpenAiCompatibleService( private const string ToolNameProperty = "ToolName"; protected abstract string GetApiKey(); + protected abstract string GetApiName(); protected abstract void ValidateApiKey(); protected virtual string HttpClientName => ServiceConstants.HttpClients.OpenAiClient; protected virtual string ChatCompletionsUrl => ServiceConstants.ApiUrls.OpenAiChatCompletions; @@ -309,7 +311,10 @@ await _notificationService.DispatchNotification( HttpCompletionOption.ResponseHeadersRead, cancellationToken); - response.EnsureSuccessStatusCode(); + if (!response.IsSuccessStatusCode) + { + await HandleApiError(response, cancellationToken); + } await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); using var reader = new StreamReader(stream); @@ -419,7 +424,11 @@ await _notificationService.DispatchNotification( var content = new StringContent(requestJson, Encoding.UTF8, MediaTypeNames.Application.Json); using var response = await client.PostAsync(ChatCompletionsUrl, content, cancellationToken); - response.EnsureSuccessStatusCode(); + + if (!response.IsSuccessStatusCode) + { + await HandleApiError(response, cancellationToken); + } var responseJson = await response.Content.ReadAsStringAsync(cancellationToken); var chatResponse = JsonSerializer.Deserialize( @@ -527,7 +536,11 @@ public virtual async Task GetCurrentModels() GetApiKey()); using var response = await client.GetAsync(ModelsUrl); - response.EnsureSuccessStatusCode(); + + if (!response.IsSuccessStatusCode) + { + await HandleApiError(response); + } var responseJson = await response.Content.ReadAsStringAsync(); var modelsResponse = JsonSerializer.Deserialize(responseJson, @@ -602,7 +615,10 @@ private async Task ProcessStreamingChatAsync( HttpCompletionOption.ResponseHeadersRead, cancellationToken); - response.EnsureSuccessStatusCode(); + if (!response.IsSuccessStatusCode) + { + await HandleApiError(response, cancellationToken); + } await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); using var reader = new StreamReader(stream); @@ -649,6 +665,34 @@ await _notificationService.DispatchNotification( } } + private async Task HandleApiError(HttpResponseMessage response, CancellationToken cancellationToken = default) + { + var errorResponseBody = await response.Content.ReadAsStringAsync(cancellationToken); + var errorMessage = ExtractApiErrorMessage(errorResponseBody); + + throw new LLMApiException(GetApiName(), response.StatusCode, errorMessage ?? errorResponseBody); + } + + private static string? ExtractApiErrorMessage(string json) + { + try + { + using var jasonDocument = JsonDocument.Parse(json); + if (jasonDocument.RootElement.TryGetProperty("error", out var error) && + error.TryGetProperty("message", out var message)) + { + return message.GetString(); + } + } + catch (JsonException) + { + // If the response is not a valid JSON or doesn't match the expected schema, + // we fall back to the raw response body in the calling method. + } + + return null; + } + protected virtual LLMTokenValue? ProcessChatCompletionChunk(string data) { var chunk = JsonSerializer.Deserialize(data, @@ -678,7 +722,11 @@ private async Task ProcessNonStreamingChatAsync( var content = new StringContent(requestJson, Encoding.UTF8, MediaTypeNames.Application.Json); using var response = await client.PostAsync(ChatCompletionsUrl, content, cancellationToken); - response.EnsureSuccessStatusCode(); + + if (!response.IsSuccessStatusCode) + { + await HandleApiError(response, cancellationToken); + } var responseJson = await response.Content.ReadAsStringAsync(cancellationToken); var chatResponse = diff --git a/src/MaIN.Services/Services/LLMService/OpenAiService.cs b/src/MaIN.Services/Services/LLMService/OpenAiService.cs index 8bd68b34..e64c9fd9 100644 --- a/src/MaIN.Services/Services/LLMService/OpenAiService.cs +++ b/src/MaIN.Services/Services/LLMService/OpenAiService.cs @@ -1,7 +1,9 @@ using MaIN.Domain.Configuration; +using MaIN.Domain.Exceptions; using MaIN.Services.Services.Abstract; using Microsoft.Extensions.Logging; using MaIN.Services.Services.LLMService.Memory; +using MaIN.Services.Services.LLMService.Utils; namespace MaIN.Services.Services.LLMService; @@ -18,15 +20,17 @@ public sealed class OpenAiService( protected override string GetApiKey() { - return _settings.OpenAiKey ?? Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? - throw new InvalidOperationException("OpenAi Key not configured"); + return _settings.OpenAiKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.OpenAi.ApiKeyEnvName) ?? + throw new APIKeyNotConfiguredException(LLMApiRegistry.OpenAi.ApiName); } + protected override string GetApiName() => LLMApiRegistry.OpenAi.ApiName; + protected override void ValidateApiKey() { - if (string.IsNullOrEmpty(_settings.OpenAiKey) && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("OPENAI_API_KEY"))) + if (string.IsNullOrEmpty(_settings.OpenAiKey) && string.IsNullOrEmpty(Environment.GetEnvironmentVariable(LLMApiRegistry.OpenAi.ApiKeyEnvName))) { - throw new InvalidOperationException("OpenAi Key not configured"); + throw new APIKeyNotConfiguredException(LLMApiRegistry.OpenAi.ApiName); } } diff --git a/src/MaIN.Services/Services/LLMService/Utils/LLMApiRegistry.cs b/src/MaIN.Services/Services/LLMService/Utils/LLMApiRegistry.cs new file mode 100644 index 00000000..bd69076e --- /dev/null +++ b/src/MaIN.Services/Services/LLMService/Utils/LLMApiRegistry.cs @@ -0,0 +1,13 @@ +namespace MaIN.Services.Services.LLMService.Utils; + +public static class LLMApiRegistry +{ + public static readonly LLMApiRegistryEntry OpenAi = new("OpenAI", "OPENAI_API_KEY"); + public static readonly LLMApiRegistryEntry Gemini = new("Gemini", "GEMINI_API_KEY"); + public static readonly LLMApiRegistryEntry Deepseek = new("Deepseek", "DEEPSEEK_API_KEY"); + public static readonly LLMApiRegistryEntry Groq = new("GroqCloud", "GROQ_API_KEY"); + public static readonly LLMApiRegistryEntry Anthropic = new("Anthropic", "ANTHROPIC_API_KEY"); + public static readonly LLMApiRegistryEntry Xai = new("xAI", "XAI_API_KEY"); +} + +public record LLMApiRegistryEntry(string ApiName, string ApiKeyEnvName); \ No newline at end of file diff --git a/src/MaIN.Services/Services/LLMService/XaiService.cs b/src/MaIN.Services/Services/LLMService/XaiService.cs index 9d7095f7..d61f9d18 100644 --- a/src/MaIN.Services/Services/LLMService/XaiService.cs +++ b/src/MaIN.Services/Services/LLMService/XaiService.cs @@ -6,6 +6,8 @@ using MaIN.Services.Services.LLMService.Memory; using Microsoft.Extensions.Logging; using System.Text; +using MaIN.Domain.Exceptions; +using MaIN.Services.Services.LLMService.Utils; namespace MaIN.Services.Services.LLMService; @@ -26,15 +28,17 @@ public sealed class XaiService( protected override string GetApiKey() { - return _settings.XaiKey ?? Environment.GetEnvironmentVariable("XAI_API_KEY") ?? - throw new InvalidOperationException("xAI Key not configured"); + return _settings.XaiKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.Xai.ApiKeyEnvName) ?? + throw new APIKeyNotConfiguredException(LLMApiRegistry.Xai.ApiName); } + + protected override string GetApiName() => LLMApiRegistry.Xai.ApiName; protected override void ValidateApiKey() { - if (string.IsNullOrEmpty(_settings.XaiKey) && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("XAI_API_KEY"))) + if (string.IsNullOrEmpty(_settings.XaiKey) && string.IsNullOrEmpty(Environment.GetEnvironmentVariable(LLMApiRegistry.Xai.ApiKeyEnvName))) { - throw new InvalidOperationException("xAI Key not configured"); + throw new APIKeyNotConfiguredException(LLMApiRegistry.Xai.ApiName); } } diff --git a/src/MaIN.Services/Services/McpService.cs b/src/MaIN.Services/Services/McpService.cs index 06de96ba..c7e94317 100644 --- a/src/MaIN.Services/Services/McpService.cs +++ b/src/MaIN.Services/Services/McpService.cs @@ -126,13 +126,13 @@ private PromptExecutionSettings InitializeChatCompletions(IKernelBuilder kernelB } string? GetOpenAiKey() - => settings.OpenAiKey ?? Environment.GetEnvironmentVariable("OPENAI_API_KEY"); + => settings.OpenAiKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.OpenAi.ApiKeyEnvName); string? GetGeminiKey() - => settings.GeminiKey ?? Environment.GetEnvironmentVariable("GEMINI_API_KEY"); + => settings.GeminiKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.Gemini.ApiKeyEnvName); string? GetGroqCloudKey() - => settings.GroqCloudKey ?? Environment.GetEnvironmentVariable("GROQ_API_KEY"); + => settings.GroqCloudKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.Groq.ApiKeyEnvName); string? GetAnthropicKey() - => settings.AnthropicKey ?? Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY"); + => settings.AnthropicKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.Anthropic.ApiKeyEnvName); string? GetXaiKey() - => settings.XaiKey ?? Environment.GetEnvironmentVariable("XAI_API_KEY"); + => settings.XaiKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.Xai.ApiKeyEnvName); } \ No newline at end of file diff --git a/src/MaIN.Services/Services/Models/ChatResult.cs b/src/MaIN.Services/Services/Models/ChatResult.cs index fb5ccf12..19bdc6f1 100644 --- a/src/MaIN.Services/Services/Models/ChatResult.cs +++ b/src/MaIN.Services/Services/Models/ChatResult.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; using MaIN.Domain.Entities; -using MaIN.Services.Dtos; namespace MaIN.Services.Services.Models; diff --git a/src/MaIN.Services/Services/Models/Commands/AnswerCommand.cs b/src/MaIN.Services/Services/Models/Commands/AnswerCommand.cs index 27b63915..d6fc8ae0 100644 --- a/src/MaIN.Services/Services/Models/Commands/AnswerCommand.cs +++ b/src/MaIN.Services/Services/Models/Commands/AnswerCommand.cs @@ -4,7 +4,7 @@ using MaIN.Domain.Models; using MaIN.Services.Constants; using MaIN.Services.Services.Models.Commands.Base; -using MaIN.Services.Services.Steps.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; namespace MaIN.Services.Services.Models.Commands; diff --git a/src/MaIN.Services/Services/Models/Commands/FetchCommand.cs b/src/MaIN.Services/Services/Models/Commands/FetchCommand.cs index 8dcfcbcf..a565b881 100644 --- a/src/MaIN.Services/Services/Models/Commands/FetchCommand.cs +++ b/src/MaIN.Services/Services/Models/Commands/FetchCommand.cs @@ -1,7 +1,7 @@ using MaIN.Domain.Entities; using MaIN.Domain.Entities.Agents; using MaIN.Services.Services.Models.Commands.Base; -using MaIN.Services.Services.Steps.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; namespace MaIN.Services.Services.Models.Commands; diff --git a/src/MaIN.Services/Services/Models/Commands/McpCommand.cs b/src/MaIN.Services/Services/Models/Commands/McpCommand.cs index 858f74d8..2b776ece 100644 --- a/src/MaIN.Services/Services/Models/Commands/McpCommand.cs +++ b/src/MaIN.Services/Services/Models/Commands/McpCommand.cs @@ -1,6 +1,6 @@ using MaIN.Domain.Entities; using MaIN.Services.Services.Models.Commands.Base; -using MaIN.Services.Services.Steps.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; namespace MaIN.Services.Services.Models.Commands; diff --git a/src/MaIN.Services/Services/Models/Commands/RedirectCommand.cs b/src/MaIN.Services/Services/Models/Commands/RedirectCommand.cs index d2ea940f..35868a21 100644 --- a/src/MaIN.Services/Services/Models/Commands/RedirectCommand.cs +++ b/src/MaIN.Services/Services/Models/Commands/RedirectCommand.cs @@ -1,6 +1,6 @@ using MaIN.Domain.Entities; using MaIN.Services.Services.Models.Commands.Base; -using MaIN.Services.Services.Steps.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; namespace MaIN.Services.Services.Models.Commands; diff --git a/src/MaIN.Services/Services/Models/Commands/StartCommand.cs b/src/MaIN.Services/Services/Models/Commands/StartCommand.cs index e5e53e26..dd37364e 100644 --- a/src/MaIN.Services/Services/Models/Commands/StartCommand.cs +++ b/src/MaIN.Services/Services/Models/Commands/StartCommand.cs @@ -1,6 +1,6 @@ using MaIN.Domain.Entities; using MaIN.Services.Services.Models.Commands.Base; -using MaIN.Services.Services.Steps.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; namespace MaIN.Services.Services.Models.Commands; diff --git a/src/MaIN.Services/Services/Models/McpResult.cs b/src/MaIN.Services/Services/Models/McpResult.cs index 5eebc03d..3351b2b2 100644 --- a/src/MaIN.Services/Services/Models/McpResult.cs +++ b/src/MaIN.Services/Services/Models/McpResult.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; using MaIN.Domain.Entities; -using MaIN.Services.Dtos; namespace MaIN.Services.Services.Models; diff --git a/src/MaIN.Services/Services/Steps/AnswerStepHandler.cs b/src/MaIN.Services/Services/Steps/AnswerStepHandler.cs index 6a65bd16..04328029 100644 --- a/src/MaIN.Services/Services/Steps/AnswerStepHandler.cs +++ b/src/MaIN.Services/Services/Steps/AnswerStepHandler.cs @@ -1,11 +1,10 @@ using System.Text.RegularExpressions; -using DocumentFormat.OpenXml.Wordprocessing; -using MaIN.Domain.Entities; using MaIN.Services.Constants; +using MaIN.Domain.Exceptions; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; using MaIN.Services.Services.Models.Commands; -using MaIN.Services.Services.Steps.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; namespace MaIN.Services.Services.Steps; @@ -35,9 +34,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/Commands/Abstract/ICommand.cs b/src/MaIN.Services/Services/Steps/Commands/Abstract/ICommand.cs index 0f79c84e..ca09739b 100644 --- a/src/MaIN.Services/Services/Steps/Commands/Abstract/ICommand.cs +++ b/src/MaIN.Services/Services/Steps/Commands/Abstract/ICommand.cs @@ -1,4 +1,4 @@ -namespace MaIN.Services.Services.Steps.Commands; +namespace MaIN.Services.Services.Steps.Commands.Abstract; public interface ICommand { diff --git a/src/MaIN.Services/Services/Steps/Commands/Abstract/ICommandDispatcher.cs b/src/MaIN.Services/Services/Steps/Commands/Abstract/ICommandDispatcher.cs index 322a20e0..fe3bc35b 100644 --- a/src/MaIN.Services/Services/Steps/Commands/Abstract/ICommandDispatcher.cs +++ b/src/MaIN.Services/Services/Steps/Commands/Abstract/ICommandDispatcher.cs @@ -1,4 +1,4 @@ -namespace MaIN.Services.Services.Steps.Commands; +namespace MaIN.Services.Services.Steps.Commands.Abstract; public interface ICommandDispatcher { diff --git a/src/MaIN.Services/Services/Steps/Commands/Abstract/ICommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/Abstract/ICommandHandler.cs index 88cc3fb7..bd13f0c1 100644 --- a/src/MaIN.Services/Services/Steps/Commands/Abstract/ICommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/Abstract/ICommandHandler.cs @@ -1,4 +1,4 @@ -namespace MaIN.Services.Services.Steps.Commands; +namespace MaIN.Services.Services.Steps.Commands.Abstract; public interface ICommandHandler where TCommand : ICommand { diff --git a/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs index 9dc68097..6913a8bd 100644 --- a/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs @@ -9,6 +9,7 @@ using MaIN.Services.Services.LLMService.Factory; using MaIN.Services.Services.Models; using MaIN.Services.Services.Models.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; using MaIN.Services.Utils; namespace MaIN.Services.Services.Steps.Commands; diff --git a/src/MaIN.Services/Services/Steps/Commands/CommandDispatcher.cs b/src/MaIN.Services/Services/Steps/Commands/CommandDispatcher.cs index 6dd33cb8..f3d29339 100644 --- a/src/MaIN.Services/Services/Steps/Commands/CommandDispatcher.cs +++ b/src/MaIN.Services/Services/Steps/Commands/CommandDispatcher.cs @@ -1,4 +1,6 @@ +using MaIN.Services.Services.Steps.Commands.Abstract; + namespace MaIN.Services.Services.Steps.Commands; diff --git a/src/MaIN.Services/Services/Steps/Commands/FetchCommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/FetchCommandHandler.cs index 5818614c..6ec74959 100644 --- a/src/MaIN.Services/Services/Steps/Commands/FetchCommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/FetchCommandHandler.cs @@ -6,6 +6,7 @@ using MaIN.Domain.Configuration; using MaIN.Services.Services.LLMService; using MaIN.Services.Services.LLMService.Factory; +using MaIN.Services.Services.Steps.Commands.Abstract; using MaIN.Services.Utils; namespace MaIN.Services.Services.Steps.Commands; diff --git a/src/MaIN.Services/Services/Steps/Commands/McpCommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/McpCommandHandler.cs index 1b17bfbe..8c6b96be 100644 --- a/src/MaIN.Services/Services/Steps/Commands/McpCommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/McpCommandHandler.cs @@ -1,12 +1,7 @@ -using MaIN.Domain.Configuration; using MaIN.Domain.Entities; -using MaIN.Services.Dtos; -using MaIN.Services.Mappers; using MaIN.Services.Services.Abstract; -using MaIN.Services.Services.LLMService; -using MaIN.Services.Services.LLMService.Factory; -using MaIN.Services.Services.Models; using MaIN.Services.Services.Models.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; namespace MaIN.Services.Services.Steps.Commands; diff --git a/src/MaIN.Services/Services/Steps/Commands/RedirectCommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/RedirectCommandHandler.cs index 64d2b11b..6d9f6086 100644 --- a/src/MaIN.Services/Services/Steps/Commands/RedirectCommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/RedirectCommandHandler.cs @@ -1,8 +1,8 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; -using MaIN.Services.Constants; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; namespace MaIN.Services.Services.Steps.Commands; diff --git a/src/MaIN.Services/Services/Steps/Commands/StartCommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/StartCommandHandler.cs index 64a4e7a5..d74b89d4 100644 --- a/src/MaIN.Services/Services/Steps/Commands/StartCommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/StartCommandHandler.cs @@ -1,6 +1,7 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Services.Services.Models.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; namespace MaIN.Services.Services.Steps.Commands; diff --git a/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs b/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs index 6c70818e..b82e48c7 100644 --- a/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs +++ b/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs @@ -1,10 +1,11 @@ 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; using MaIN.Services.Services.Models.Commands; -using MaIN.Services.Services.Steps.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; namespace MaIN.Services.Services.Steps; @@ -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 3b3d4e95..2fad4126 100644 --- a/src/MaIN.Services/Services/Steps/McpStepHandler.cs +++ b/src/MaIN.Services/Services/Steps/McpStepHandler.cs @@ -1,10 +1,10 @@ using System.Text.RegularExpressions; -using DocumentFormat.OpenXml.Wordprocessing; -using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions; +using MaIN.Domain.Exceptions.MPC; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; using MaIN.Services.Services.Models.Commands; -using MaIN.Services.Services.Steps.Commands; +using MaIN.Services.Services.Steps.Commands.Abstract; namespace MaIN.Services.Services.Steps; @@ -16,7 +16,7 @@ public async Task Handle(StepContext context) { if (context.McpConfig == null) { - throw new MissingFieldException("MCP config is missing"); + throw new MPCConfigNotFoundException(); } await context.NotifyProgress("true", context.Agent.Id, null, context.Agent.CurrentBehaviour, StepName); @@ -29,7 +29,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); diff --git a/src/MaIN.Services/Services/Steps/RedirectStepHandler.cs b/src/MaIN.Services/Services/Steps/RedirectStepHandler.cs index ab5e5007..b976d4d3 100644 --- a/src/MaIN.Services/Services/Steps/RedirectStepHandler.cs +++ b/src/MaIN.Services/Services/Steps/RedirectStepHandler.cs @@ -1,11 +1,8 @@ using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; using MaIN.Services.Services.Models.Commands; -using MaIN.Services.Services.Steps.Commands; -using System; -using System.Linq; -using System.Threading.Tasks; using MaIN.Domain.Entities; +using MaIN.Services.Services.Steps.Commands.Abstract; namespace MaIN.Services.Services.Steps; diff --git a/src/MaIN.Services/Services/TTSService/TextToSpeechService.cs b/src/MaIN.Services/Services/TTSService/TextToSpeechService.cs index bc53f149..3bc93a38 100644 --- a/src/MaIN.Services/Services/TTSService/TextToSpeechService.cs +++ b/src/MaIN.Services/Services/TTSService/TextToSpeechService.cs @@ -1,5 +1,6 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions.Models; using MaIN.Domain.Models; using NAudio.Wave; @@ -98,11 +99,8 @@ private async Task PlaybackAudio(byte[] audioData) private string GetModelsPath() { var path = options.ModelsPath ?? Environment.GetEnvironmentVariable("MaIN_ModelsPath"); - if (string.IsNullOrEmpty(path)) - { - throw new InvalidOperationException("Models path not found in configuration or environment variables"); - } - - return path; + return string.IsNullOrEmpty(path) + ? throw new ModelsPathNotFoundException() + : path; } } \ No newline at end of file diff --git a/src/MaIN.Services/Utils/JsonCleaner.cs b/src/MaIN.Services/Utils/JsonCleaner.cs index bb2c2408..5404a3bf 100644 --- a/src/MaIN.Services/Utils/JsonCleaner.cs +++ b/src/MaIN.Services/Utils/JsonCleaner.cs @@ -1,8 +1,8 @@ -using MaIN.Domain.Models; -using System; -using System.Text.Encodings.Web; +using System.Text.Encodings.Web; using System.Text.Json; +namespace MaIN.Services.Utils; + public static class JsonCleaner { public static string? CleanAndUnescape(string json, int maxDepth = 5)