diff --git a/Examples/Examples.SimpleConsole/Program.cs b/Examples/Examples.SimpleConsole/Program.cs index 8662cdc..9338cbf 100644 --- a/Examples/Examples.SimpleConsole/Program.cs +++ b/Examples/Examples.SimpleConsole/Program.cs @@ -1,12 +1,19 @@ using MaIN.Core; using MaIN.Core.Hub; +using MaIN.Domain.Models.Abstract; +using MaIN.Domain.Models.Concrete; MaINBootstrapper.Initialize(); -var model = AIHub.Model(); +var modelContext = AIHub.Model(); -var m = model.GetModel("gemma3:4b"); -var x = model.GetModel("llama3.2:3b"); -await model.DownloadAsync(x.Name); +// Get models using ModelRegistry +var gemma = modelContext.GetModel("gemma3-4b"); +var llama = modelContext.GetModel("llama3.2-3b"); +// Or use strongly-typed models directly +var gemma2b = new Gemma2_2b(); +Console.WriteLine($"Model: {gemma2b.Name}, File: {gemma2b.FileName}"); +// Download a model +await modelContext.DownloadAsync(gemma2b.Id); diff --git a/Examples/Examples/Chat/ChatCustomGrammarExample.cs b/Examples/Examples/Chat/ChatCustomGrammarExample.cs index ec58c25..7863fd7 100644 --- a/Examples/Examples/Chat/ChatCustomGrammarExample.cs +++ b/Examples/Examples/Chat/ChatCustomGrammarExample.cs @@ -1,6 +1,7 @@ using MaIN.Core.Hub; using MaIN.Domain.Entities; using MaIN.Domain.Models; +using MaIN.Domain.Models.Concrete; using Grammar = MaIN.Domain.Models.Grammar; namespace Examples.Chat; @@ -21,7 +22,7 @@ public async Task Start() """, GrammarFormat.GBNF); await AIHub.Chat() - .WithModel("gemma2:2b") + .WithModel() .WithMessage("Generate random person") .WithInferenceParams(new InferenceParams { diff --git a/Examples/Examples/Chat/ChatExample.cs b/Examples/Examples/Chat/ChatExample.cs index 7a7db1d..3d97ccf 100644 --- a/Examples/Examples/Chat/ChatExample.cs +++ b/Examples/Examples/Chat/ChatExample.cs @@ -1,4 +1,5 @@ using MaIN.Core.Hub; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -8,8 +9,9 @@ public async Task Start() { Console.WriteLine("ChatExample is running!"); + // Using strongly-typed model await AIHub.Chat() - .WithModel("gemma2:2b") + .WithModel() .WithMessage("Where do hedgehogs goes at night?") .CompleteAsync(interactive: true); } diff --git a/Examples/Examples/Chat/ChatExampleAnthropic.cs b/Examples/Examples/Chat/ChatExampleAnthropic.cs index b24baf5..851580f 100644 --- a/Examples/Examples/Chat/ChatExampleAnthropic.cs +++ b/Examples/Examples/Chat/ChatExampleAnthropic.cs @@ -1,5 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -11,7 +12,7 @@ public async Task Start() Console.WriteLine("(Anthropic) ChatExample is running!"); await AIHub.Chat() - .WithModel("claude-sonnet-4-20250514") + .WithModel() .WithMessage("Write a haiku about programming on Monday morning.") .CompleteAsync(interactive: true); } diff --git a/Examples/Examples/Chat/ChatExampleGemini.cs b/Examples/Examples/Chat/ChatExampleGemini.cs index a578db6..deb9b65 100644 --- a/Examples/Examples/Chat/ChatExampleGemini.cs +++ b/Examples/Examples/Chat/ChatExampleGemini.cs @@ -1,5 +1,8 @@ using Examples.Utils; using MaIN.Core.Hub; +using MaIN.Domain.Configuration; +using MaIN.Domain.Models.Abstract; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -10,8 +13,17 @@ public async Task Start() GeminiExample.Setup(); //We need to provide Gemini API key Console.WriteLine("(Gemini) ChatExample is running!"); + // Get built-in Gemini 2.5 Flash model + var model = AIHub.Model().GetModel(new Gemini2_5Flash().Id); + + // Or create the model manually if not available in the hub + var customModel = new GenericCloudModel( + "gemini-2.5-flash", + BackendType.Gemini + ); + await AIHub.Chat() - .WithModel("gemini-2.5-flash") + .WithModel(customModel) .WithMessage("Is the killer whale the smartest animal?") .CompleteAsync(interactive: true); } diff --git a/Examples/Examples/Chat/ChatExampleGroqCloud.cs b/Examples/Examples/Chat/ChatExampleGroqCloud.cs index 7bfc444..69df1dd 100644 --- a/Examples/Examples/Chat/ChatExampleGroqCloud.cs +++ b/Examples/Examples/Chat/ChatExampleGroqCloud.cs @@ -1,5 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -11,7 +12,7 @@ public async Task Start() Console.WriteLine("(GroqCloud) ChatExample is running!"); await AIHub.Chat() - .WithModel("llama-3.1-8b-instant") + .WithModel() .WithMessage("Which color do people like the most?") .CompleteAsync(interactive: true); } diff --git a/Examples/Examples/Chat/ChatExampleOllama.cs b/Examples/Examples/Chat/ChatExampleOllama.cs index 5a1685d..08dd1ba 100644 --- a/Examples/Examples/Chat/ChatExampleOllama.cs +++ b/Examples/Examples/Chat/ChatExampleOllama.cs @@ -1,5 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -11,7 +12,7 @@ public async Task Start() Console.WriteLine("(Ollama) ChatExample is running!"); await AIHub.Chat() - .WithModel("gemma3:4b") + .WithModel() .WithMessage("Write a short poem about the color green.") .CompleteAsync(interactive: true); } diff --git a/Examples/Examples/Chat/ChatExampleOpenAi.cs b/Examples/Examples/Chat/ChatExampleOpenAi.cs index 80ef4d2..da03637 100644 --- a/Examples/Examples/Chat/ChatExampleOpenAi.cs +++ b/Examples/Examples/Chat/ChatExampleOpenAi.cs @@ -1,5 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -12,7 +13,7 @@ public async Task Start() Console.WriteLine("(OpenAi) ChatExample is running!"); await AIHub.Chat() - .WithModel("gpt-5-nano") + .WithModel() .WithMessage("What do you consider to be the greatest invention in history?") .CompleteAsync(interactive: true); } diff --git a/Examples/Examples/Chat/ChatExampleToolsSimple.cs b/Examples/Examples/Chat/ChatExampleToolsSimple.cs index 9681976..f4fa821 100644 --- a/Examples/Examples/Chat/ChatExampleToolsSimple.cs +++ b/Examples/Examples/Chat/ChatExampleToolsSimple.cs @@ -1,6 +1,7 @@ using Examples.Utils; using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -13,7 +14,7 @@ public async Task Start() Console.WriteLine("(OpenAi) ChatExample with tools is running!"); await AIHub.Chat() - .WithModel("gpt-5-nano") + .WithModel() .WithMessage("What time is it right now?") .WithTools(new ToolsConfigurationBuilder() .AddTool( diff --git a/Examples/Examples/Chat/ChatExampleToolsSimpleLocalLLM.cs b/Examples/Examples/Chat/ChatExampleToolsSimpleLocalLLM.cs index 9694c1a..479cc1e 100644 --- a/Examples/Examples/Chat/ChatExampleToolsSimpleLocalLLM.cs +++ b/Examples/Examples/Chat/ChatExampleToolsSimpleLocalLLM.cs @@ -1,6 +1,7 @@ using Examples.Utils; using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -11,7 +12,7 @@ public async Task Start() Console.WriteLine("Local LLM ChatExample with tools is running!"); await AIHub.Chat() - .WithModel("gemma3:4b") + .WithModel() .WithMessage("What time is it right now?") .WithTools(new ToolsConfigurationBuilder() .AddTool( diff --git a/Examples/Examples/Chat/ChatExampleXai.cs b/Examples/Examples/Chat/ChatExampleXai.cs index 2f8df21..be7d474 100644 --- a/Examples/Examples/Chat/ChatExampleXai.cs +++ b/Examples/Examples/Chat/ChatExampleXai.cs @@ -1,5 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -11,7 +12,7 @@ public async Task Start() Console.WriteLine("(xAI) ChatExample is running!"); await AIHub.Chat() - .WithModel("grok-3-beta") + .WithModel() .WithMessage("Is the killer whale cute?") .CompleteAsync(interactive: true); } diff --git a/Examples/Examples/Chat/ChatFromExistingExample.cs b/Examples/Examples/Chat/ChatFromExistingExample.cs index adeba5a..6a14961 100644 --- a/Examples/Examples/Chat/ChatFromExistingExample.cs +++ b/Examples/Examples/Chat/ChatFromExistingExample.cs @@ -1,6 +1,7 @@ using System.Text.Json; using MaIN.Core.Hub; using MaIN.Domain.Exceptions.Chats; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -11,7 +12,7 @@ public async Task Start() Console.WriteLine("ChatExample with files is running!"); var result = AIHub.Chat() - .WithModel("qwen2.5:0.5b"); + .WithModel(); await result.WithMessage("What do you think about math theories?") .CompleteAsync(); diff --git a/Examples/Examples/Chat/ChatGrammarExampleGemini.cs b/Examples/Examples/Chat/ChatGrammarExampleGemini.cs index 43ee0eb..eea49de 100644 --- a/Examples/Examples/Chat/ChatGrammarExampleGemini.cs +++ b/Examples/Examples/Chat/ChatGrammarExampleGemini.cs @@ -2,6 +2,7 @@ using MaIN.Core.Hub; using MaIN.Domain.Entities; using MaIN.Domain.Models; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -38,7 +39,7 @@ public async Task Start() """; await AIHub.Chat() - .WithModel("gemini-2.5-flash") + .WithModel() .WithMessage("Generate random person") .WithInferenceParams(new InferenceParams { diff --git a/Examples/Examples/Chat/ChatWithFilesExample.cs b/Examples/Examples/Chat/ChatWithFilesExample.cs index 75b2337..9be491d 100644 --- a/Examples/Examples/Chat/ChatWithFilesExample.cs +++ b/Examples/Examples/Chat/ChatWithFilesExample.cs @@ -1,4 +1,5 @@ using MaIN.Core.Hub; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -14,7 +15,7 @@ public async Task Start() ]; var result = await AIHub.Chat() - .WithModel("gemma3:4b") + .WithModel() .WithMessage("You have 2 documents in memory. Whats the difference of work between Galileo and Copernicus?. Give answer based on the documents.") .WithFiles(files) .DisableCache() diff --git a/Examples/Examples/Chat/ChatWithFilesExampleGemini.cs b/Examples/Examples/Chat/ChatWithFilesExampleGemini.cs index b4ab6ef..c359918 100644 --- a/Examples/Examples/Chat/ChatWithFilesExampleGemini.cs +++ b/Examples/Examples/Chat/ChatWithFilesExampleGemini.cs @@ -1,5 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -13,7 +14,7 @@ public async Task Start() List files = ["./Files/Nicolaus_Copernicus.pdf", "./Files/Galileo_Galilei.pdf"]; var result = await AIHub.Chat() - .WithModel("gemini-2.0-flash") + .WithModel() .WithMessage("You have 2 documents in memory. Whats the difference of work between Galileo and Copernicus?. Give answer based on the documents.") .WithFiles(files) .CompleteAsync(interactive: true); diff --git a/Examples/Examples/Chat/ChatWithFilesFromStreamExample.cs b/Examples/Examples/Chat/ChatWithFilesFromStreamExample.cs index 08e0cb8..1643cf6 100644 --- a/Examples/Examples/Chat/ChatWithFilesFromStreamExample.cs +++ b/Examples/Examples/Chat/ChatWithFilesFromStreamExample.cs @@ -1,4 +1,5 @@ using MaIN.Core.Hub; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -37,7 +38,7 @@ public async Task Start() } var result = await AIHub.Chat() - .WithModel("qwen2.5:0.5b") + .WithModel() .WithMessage("You have 2 documents in memory. Whats the difference of work between Galileo and Copernicus?. Give answer based on the documents.") .WithFiles(fileStreams) .CompleteAsync(); diff --git a/Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs b/Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs index 3f3529c..d7460c6 100644 --- a/Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs +++ b/Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs @@ -1,5 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -11,7 +12,7 @@ public async Task Start() OpenAiExample.Setup(); // We need to provide OpenAi API key var result = await AIHub.Chat() - .WithModel("dall-e-3") + .WithModel() .EnableVisual() .WithMessage("Generate rock style cow playing guitar") .CompleteAsync(); diff --git a/Examples/Examples/Chat/ChatWithReasoningDeepSeekExample.cs b/Examples/Examples/Chat/ChatWithReasoningDeepSeekExample.cs index 5d77192..7fc4b1f 100644 --- a/Examples/Examples/Chat/ChatWithReasoningDeepSeekExample.cs +++ b/Examples/Examples/Chat/ChatWithReasoningDeepSeekExample.cs @@ -1,5 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -11,7 +12,7 @@ public async Task Start() Console.WriteLine("(DeepSeek) ChatExample with reasoning is running!"); await AIHub.Chat() - .WithModel("deepseek-reasoner") // a model that supports reasoning + .WithModel() // a model that supports reasoning .WithMessage("What chill pc game do you recommend?") .CompleteAsync(interactive: true); } diff --git a/Examples/Examples/Chat/ChatWithReasoningExample.cs b/Examples/Examples/Chat/ChatWithReasoningExample.cs index e6a249d..d233461 100644 --- a/Examples/Examples/Chat/ChatWithReasoningExample.cs +++ b/Examples/Examples/Chat/ChatWithReasoningExample.cs @@ -1,4 +1,5 @@ using MaIN.Core.Hub; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -9,7 +10,7 @@ public async Task Start() Console.WriteLine("ChatWithReasoningExample is running!"); await AIHub.Chat() - .WithModel("deepseekR1:1.5b") + .WithModel() .WithMessage("Think about greatest poet of all time") .CompleteAsync(interactive: true); } diff --git a/Examples/Examples/Chat/ChatWithTextToSpeechExample.cs b/Examples/Examples/Chat/ChatWithTextToSpeechExample.cs index 9b3640a..6dd0212 100644 --- a/Examples/Examples/Chat/ChatWithTextToSpeechExample.cs +++ b/Examples/Examples/Chat/ChatWithTextToSpeechExample.cs @@ -1,5 +1,6 @@ using MaIN.Core.Hub; using MaIN.Domain.Entities; +using MaIN.Domain.Models.Concrete; using MaIN.Services.Services.TTSService; #pragma warning disable CS0618 // Type or member is obsolete @@ -18,7 +19,8 @@ public async Task Start() var voice = VoiceService.GetVoice("af_heart") .MixWith(VoiceService.GetVoice("bf_emma")); - await AIHub.Chat().WithModel("gemma2:2b") + await AIHub.Chat() + .WithModel() .WithMessage("Generate a 4 sentence poem.") .Speak(new TextToSpeechParams("kokoro:82m", voice, true)) .CompleteAsync(interactive: true); diff --git a/Examples/Examples/Chat/ChatWithVisionExample.cs b/Examples/Examples/Chat/ChatWithVisionExample.cs index 40a9808..5259bd8 100644 --- a/Examples/Examples/Chat/ChatWithVisionExample.cs +++ b/Examples/Examples/Chat/ChatWithVisionExample.cs @@ -1,4 +1,5 @@ using MaIN.Core.Hub; +using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -11,11 +12,9 @@ public async Task Start() var image = await File.ReadAllBytesAsync( Path.Combine(AppContext.BaseDirectory, "Files", "gamex.jpg")); - + await AIHub.Chat() - .WithCustomModel("Llava1.6-Mistral", - path: ".gguf", - mmProject: ".gguf") + .WithModel() .WithMessage("What can you see on the image?", image) .CompleteAsync(interactive: true); } diff --git a/MaIN.Core.IntegrationTests/ChatTests.cs b/MaIN.Core.IntegrationTests/ChatTests.cs index f0b7896..e43a5f0 100644 --- a/MaIN.Core.IntegrationTests/ChatTests.cs +++ b/MaIN.Core.IntegrationTests/ChatTests.cs @@ -1,5 +1,6 @@ using MaIN.Core.Hub; using MaIN.Domain.Entities; +using MaIN.Domain.Models.Concrete; namespace MaIN.Core.IntegrationTests; @@ -12,7 +13,7 @@ public ChatTests() : base() [Fact] public async Task Should_AnswerQuestion_BasicChat() { - var context = AIHub.Chat().WithModel("gemma2:2b"); + var context = AIHub.Chat().WithModel(); var result = await context .WithMessage("Where the hedgehog goes at night?") @@ -29,7 +30,7 @@ public async Task Should_AnswerDifferences_BetweenDocuments_ChatWithFiles() List files = ["./Files/Nicolaus_Copernicus.pdf", "./Files/Galileo_Galilei.pdf"]; var result = await AIHub.Chat() - .WithModel("gemma2:2b") + .WithModel() .WithMessage("You have 2 documents in memory. Whats the difference of work between Galileo and Copernicus?. Give answer based on the documents.") .WithFiles(files) .CompleteAsync(); @@ -43,11 +44,12 @@ public async Task Should_AnswerDifferences_BetweenDocuments_ChatWithFiles() public async Task Should_AnswerQuestion_FromExistingChat() { var result = AIHub.Chat() - .WithModel("qwen2.5:0.5b"); + .WithModel(); await result.WithMessage("What do you think about math theories?") .CompleteAsync(); + await result.WithMessage("And about physics?") .CompleteAsync(); @@ -63,9 +65,8 @@ public async Task Should_AnswerGameFromImage_ChatWithVision() List images = ["./Files/gamex.jpg"]; var result = await AIHub.Chat() - .WithModel("llama3.2:3b") + .WithModel() .WithMessage("What is the title of game?") - .WithMemoryParams(new MemoryParams { AnswerTokens = 1000 @@ -120,7 +121,7 @@ public async Task Should_AnswerDifferences_BetweenDocuments_ChatWithFiles_UsingS } var result = await AIHub.Chat() - .WithModel("gemma2:2b") + .WithModel() .WithMessage("You have 2 documents in memory. Whats the difference of work between Galileo and Copernicus?. Give answer based on the documents.") .WithFiles(fileStreams) .CompleteAsync(); diff --git a/MaIN.sln b/MaIN.sln index cfd8fda..4417e84 100644 --- a/MaIN.sln +++ b/MaIN.sln @@ -1,6 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Main.Infrastructure", "src\MaIN.Infrastructure\MaIN.Infrastructure.csproj", "{84C09EF9-B9E3-4ACB-9BC3-DEFFC29D528F}" +# Visual Studio Version 18 +VisualStudioVersion = 18.1.11312.151 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaIN.Infrastructure", "src\MaIN.Infrastructure\MaIN.Infrastructure.csproj", "{84C09EF9-B9E3-4ACB-9BC3-DEFFC29D528F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaIN.Services", "src\MaIN.Services\MaIN.Services.csproj", "{35DBE637-EC89-40A7-B5F9-D005D554E8ED}" EndProject @@ -65,10 +68,16 @@ Global {2C15062A-E9F6-47FC-A4CD-1190A49E3FE3}.Release|Any CPU.ActiveCfg = Release|Any CPU {2C15062A-E9F6-47FC-A4CD-1190A49E3FE3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection GlobalSection(NestedProjects) = preSolution {781BDD20-65BA-4C5D-815B-D8A15931570A} = {28851935-517F-438D-BF7C-02FEB1A37A68} {46E6416B-1736-478C-B697-B37BB8E6A23E} = {53D24B04-279D-4D18-8829-EA0F57AE69F3} {75DEBB8A-75CD-44BA-9369-3916950428EF} = {28851935-517F-438D-BF7C-02FEB1A37A68} {2C15062A-E9F6-47FC-A4CD-1190A49E3FE3} = {53D24B04-279D-4D18-8829-EA0F57AE69F3} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {82494AB7-60FC-49E3-B37C-B9785FB0DE7A} + EndGlobalSection EndGlobal diff --git a/src/MaIN.Core.UnitTests/AgentContextTests.cs b/src/MaIN.Core.UnitTests/AgentContextTests.cs index 69ea8e3..93db54f 100644 --- a/src/MaIN.Core.UnitTests/AgentContextTests.cs +++ b/src/MaIN.Core.UnitTests/AgentContextTests.cs @@ -159,7 +159,7 @@ public async Task ProcessAsync_WithStringMessage_ShouldReturnChatResult() { // Arrange var message = "Hello, agent!"; - var chat = new Chat { Id = _agentContext.GetAgentId(), Messages = new List(), Name = "test", Model = "default"}; + var chat = new Chat { Id = _agentContext.GetAgentId(), Messages = new List(), Name = "test", ModelId = "default"}; var chatResult = new ChatResult { Done = true, Model = "test-model", Message = new Message { Role = "Assistant", @@ -175,7 +175,7 @@ public async Task ProcessAsync_WithStringMessage_ShouldReturnChatResult() _mockAgentService .Setup(s => s.Process(It.IsAny(), _agentContext.GetAgentId(), It.IsAny(), It.IsAny(), null, null)) .ReturnsAsync(new Chat { - Model = "test-model", + ModelId = "test-model", Name = "test", Messages = new List { new Message { Content = "Response", Role = "Assistant", Type = MessageType.LocalLLM} diff --git a/src/MaIN.Core.UnitTests/ChatContextTests.cs b/src/MaIN.Core.UnitTests/ChatContextTests.cs index 6da951c..739e18d 100644 --- a/src/MaIN.Core.UnitTests/ChatContextTests.cs +++ b/src/MaIN.Core.UnitTests/ChatContextTests.cs @@ -101,7 +101,7 @@ public async Task CompleteAsync_ShouldCallChatService() public async Task GetCurrentChat_ShouldCallChatService() { // Arrange - var chat = new Chat { Id = _chatContext.GetChatId(), Model = "default", Name = "test"}; + var chat = new Chat { Id = _chatContext.GetChatId(), ModelId = "default", Name = "test"}; _mockChatService.Setup(s => s.GetById(chat.Id)).ReturnsAsync(chat); // Act diff --git a/src/MaIN.Core.UnitTests/FlowContextTests.cs b/src/MaIN.Core.UnitTests/FlowContextTests.cs index 7db0803..70245b1 100644 --- a/src/MaIN.Core.UnitTests/FlowContextTests.cs +++ b/src/MaIN.Core.UnitTests/FlowContextTests.cs @@ -92,7 +92,7 @@ public async Task ProcessAsync_WithStringMessage_ShouldReturnChatResult() _flowContext.AddAgent(firstAgent); var message = "Hello, flow!"; - var chat = new Chat { Id = firstAgent.Id, Messages = new List(), Model = "default", Name = "test"}; + var chat = new Chat { Id = firstAgent.Id, Messages = new List(), ModelId = "default", Name = "test"}; _mockAgentService .Setup(s => s.GetChatByAgent(firstAgent.Id)) @@ -101,7 +101,7 @@ public async Task ProcessAsync_WithStringMessage_ShouldReturnChatResult() _mockAgentService .Setup(s => s.Process(It.IsAny(), firstAgent.Id, It.IsAny(), It.IsAny(), null, null)) .ReturnsAsync(new Chat { - Model = "test-model", + ModelId = "test-model", Name = "test", Messages = new List { new() { Content = "Response", Role = "Assistant", Type = MessageType.LocalLLM} diff --git a/src/MaIN.Core/Hub/Contexts/AgentContext.cs b/src/MaIN.Core/Hub/Contexts/AgentContext.cs index aa31362..b9fdd09 100644 --- a/src/MaIN.Core/Hub/Contexts/AgentContext.cs +++ b/src/MaIN.Core/Hub/Contexts/AgentContext.cs @@ -242,7 +242,7 @@ public async Task ProcessAsync(Chat chat, bool translate = false) return new ChatResult() { Done = true, - Model = result.Model, + Model = result.ModelId, Message = message, CreatedAt = DateTime.Now }; @@ -271,7 +271,7 @@ public async Task ProcessAsync( return new ChatResult() { Done = true, - Model = result.Model, + Model = result.ModelId, Message = messageResult, CreatedAt = DateTime.Now }; @@ -293,7 +293,7 @@ public async Task ProcessAsync(Message message, return new ChatResult() { Done = true, - Model = result.Model, + Model = result.ModelId, Message = messageResult, CreatedAt = DateTime.Now }; @@ -322,7 +322,7 @@ public async Task ProcessAsync( return new ChatResult() { Done = true, - Model = result.Model, + Model = result.ModelId, Message = messageResult, CreatedAt = DateTime.Now }; diff --git a/src/MaIN.Core/Hub/Contexts/ChatContext.cs b/src/MaIN.Core/Hub/Contexts/ChatContext.cs index 8f58c83..c962edc 100644 --- a/src/MaIN.Core/Hub/Contexts/ChatContext.cs +++ b/src/MaIN.Core/Hub/Contexts/ChatContext.cs @@ -4,6 +4,7 @@ using MaIN.Domain.Entities.Tools; using MaIN.Domain.Exceptions.Chats; using MaIN.Domain.Models; +using MaIN.Domain.Models.Abstract; using MaIN.Services; using MaIN.Services.Constants; using MaIN.Services.Services.Abstract; @@ -16,8 +17,8 @@ public sealed class ChatContext : IChatBuilderEntryPoint, IChatMessageBuilder, I { private readonly IChatService _chatService; private bool _preProcess; - private Chat _chat { get; set; } - private List _files { get; set; } = []; + private readonly Chat _chat; + private List _files = []; internal ChatContext(IChatService chatService) { @@ -26,10 +27,9 @@ internal ChatContext(IChatService chatService) { Name = "New Chat", Id = Guid.NewGuid().ToString(), - Messages = new List(), - Model = string.Empty + Messages = [], + ModelId = string.Empty }; - _files = []; } internal ChatContext(IChatService chatService, Chat existingChat) @@ -38,20 +38,41 @@ internal ChatContext(IChatService chatService, Chat existingChat) _chat = existingChat; } + public IChatMessageBuilder WithModel(AIModel model) + { + SetModel(model); + return this; + } - public IChatMessageBuilder WithModel(string model) + public IChatMessageBuilder WithModel() where TModel : AIModel, new() { - _chat.Model = model; + var model = new TModel(); + return WithModel(model); + } + + [Obsolete("Use WithModel(AIModel model) or WithModel() instead.")] + public IChatMessageBuilder WithModel(string modelId) + { + var model = ModelRegistry.GetById(modelId); + SetModel(model); return this; } + [Obsolete("Use WithModel() instead.")] public IChatMessageBuilder WithCustomModel(string model, string path, string? mmProject = null) { KnownModels.AddModel(model, path, mmProject); - _chat.Model = model; + _chat.ModelId = model; return this; } + private void SetModel(AIModel model) + { + _chat.ModelId = model.Id; + _chat.ModelInstance = model; + _chat.Backend = model.Backend; + } + public IChatMessageBuilder EnableVisual() { _chat.Visual = true; @@ -132,8 +153,7 @@ public IChatConfigurationBuilder WithMessages(IEnumerable messages) public IChatConfigurationBuilder WithFiles(List file, bool preProcess = false) { - _files = file.Select(f => new FileInfo { Name = Path.GetFileName(f.Name), StreamContent = f, Extension = Path.GetExtension(f.Name) }) - .ToList(); + _files = [.. file.Select(f => new FileInfo { Name = Path.GetFileName(f.Name), StreamContent = f, Extension = Path.GetExtension(f.Name) })]; _preProcess = preProcess; return this; } @@ -147,15 +167,14 @@ public IChatConfigurationBuilder WithFiles(List file, bool preProcess public IChatConfigurationBuilder WithFiles(List file, bool preProcess = false) { - _files = file + _files = [.. file .Select(path => new FileInfo { Name = Path.GetFileName(path), Path = path, Extension = Path.GetExtension(path) - }) - .ToList(); + })]; _preProcess = preProcess; return this; } @@ -167,10 +186,14 @@ public IChatConfigurationBuilder DisableCache() } public async Task CompleteAsync( - bool translate = false, - bool interactive = false, + bool translate = false, // Move to WithTranslate + bool interactive = false, // Move to WithInteractive Func? changeOfValue = null) { + if (_chat.ModelInstance is null) + { + throw new ChatNotInitializedException(); + } if (_chat.Messages.Count == 0) { throw new EmptyChatException(_chat.Id); @@ -227,11 +250,8 @@ public async Task GetCurrentChat() return await _chatService.GetById(_chat.Id); } - public async Task> GetAllChats() - { - return await _chatService.GetAll(); - } - + public async Task> GetAllChats() => await _chatService.GetAll(); + public async Task DeleteChat() { if (_chat.Id == null) @@ -244,11 +264,11 @@ public async Task DeleteChat() public List GetChatHistory() { - return _chat.Messages.Select(x => new MessageShort() + return [.. _chat.Messages.Select(x => new MessageShort() { Content = x.Content, Role = x.Role, Time = x.Time - }).ToList(); + })]; } } \ No newline at end of file diff --git a/src/MaIN.Core/Hub/Contexts/FlowContext.cs b/src/MaIN.Core/Hub/Contexts/FlowContext.cs index c6ea64c..39d6cb7 100644 --- a/src/MaIN.Core/Hub/Contexts/FlowContext.cs +++ b/src/MaIN.Core/Hub/Contexts/FlowContext.cs @@ -149,7 +149,7 @@ public async Task ProcessAsync(Chat chat, bool translate = false) return new ChatResult() { Done = true, - Model = result.Model, + Model = result.ModelId, Message = message, CreatedAt = DateTime.Now }; @@ -170,7 +170,7 @@ public async Task ProcessAsync(string message, bool translate = fals return new ChatResult() { Done = true, - Model = result.Model, + Model = result.ModelId, Message = messageResult, CreatedAt = DateTime.Now }; @@ -185,7 +185,7 @@ public async Task ProcessAsync(Message message, bool translate = fal return new ChatResult() { Done = true, - Model = result.Model, + Model = result.ModelId, Message = messageResult, CreatedAt = DateTime.Now }; diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/ModelContext/IModelContext.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/ModelContext/IModelContext.cs index 3b7b216..8e2255f 100644 --- a/src/MaIN.Core/Hub/Contexts/Interfaces/ModelContext/IModelContext.cs +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/ModelContext/IModelContext.cs @@ -1,48 +1,55 @@ -using MaIN.Domain.Models; +using MaIN.Domain.Models.Abstract; 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 + /// Retrieves an enumerable collection 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(); + /// A list of containing all available models in the system + IEnumerable GetAll(); /// - /// Retrieves information about a specific model by its name. This method allows you to get detailed information about a particular model, + /// Retrieves all local models available in the data store. + /// + /// An enumerable collection of instances representing all local models. The collection is + /// empty if no local models are found. + IEnumerable GetAllLocal(); + + /// + /// Retrieves information about a specific model by its id. 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); + /// The id of the model to retrieve. + /// A object containing the model's information and configuration. + AIModel GetModel(string modelId); /// /// Retrieves the designated embedding model used for generating vector representations of text. This is typically used /// for semantic search, similarity calculations, and other NLP tasks that require text embeddings. /// - /// A object representing the embedding model. - Model GetEmbeddingModel(); + /// A object representing the embedding model. + AIModel GetEmbeddingModel(); /// /// Checks whether a specific model exists locally on the filesystem. This method verifies if the model file is present /// and accessible before attempting to use it. /// - /// The name of the model to check for existence. + /// The id of the model to check for existence. /// A boolean value indicating whether the model file exists locally. - bool Exists(string modelName); + bool Exists(string modelId); /// /// Asynchronously downloads a known model from its configured download URL. This method handles the complete download process /// with progress tracking and error handling. /// - /// The name of the model to download. + /// The id of the model to download. /// Optional cancellation token to abort the download operation. /// A task that represents the asynchronous download operation that completes when the download finishes, /// returning the context instance implementing for method chaining. - Task DownloadAsync(string modelName, CancellationToken cancellationToken = default); + Task DownloadAsync(string modelId, CancellationToken cancellationToken = default); /// /// Asynchronously downloads a custom model from a specified URL. This method allows downloading models that are not part @@ -52,7 +59,7 @@ public interface IModelContext /// The URL from which to download the model. /// A task that represents the asynchronous download operation that completes when the download finishes, /// returning the context instance implementing for method chaining. - Task DownloadAsync(string model, string url); + Task DownloadAsync(string model, string url, CancellationToken cancellationToken = default); /// /// Synchronously downloads a known model from its configured download URL. This is the blocking version of the download operation @@ -79,14 +86,14 @@ public interface IModelContext /// /// The Model object to load into a cache. /// The context instance implementing for method chaining. - IModelContext LoadToCache(Model model); + IModelContext LoadToCache(LocalModel model); /// /// Asynchronously loads a model into the memory cache for faster access during inference operations. This is the non-blocking /// version of cache loading that allows other operations to continue while the model loads. /// - /// The object to load into a cache. + /// The object to load into a cache. /// A task that represents the asynchronous operation that completes when the model is loaded into cache, /// returning the context instance implementing for method chaining. - Task LoadToCacheAsync(Model model); -} \ No newline at end of file + Task LoadToCacheAsync(LocalModel model); +} diff --git a/src/MaIN.Core/Hub/Contexts/ModelContext.cs b/src/MaIN.Core/Hub/Contexts/ModelContext.cs index 5bfa66c..1883827 100644 --- a/src/MaIN.Core/Hub/Contexts/ModelContext.cs +++ b/src/MaIN.Core/Hub/Contexts/ModelContext.cs @@ -3,7 +3,9 @@ using MaIN.Core.Hub.Contexts.Interfaces.ModelContext; using MaIN.Domain.Configuration; using MaIN.Domain.Exceptions.Models; -using MaIN.Domain.Models; +using MaIN.Domain.Exceptions.Models.LocalModels; +using MaIN.Domain.Models.Abstract; +using MaIN.Domain.Models.Concrete; using MaIN.Services.Constants; using MaIN.Services.Services.LLMService.Utils; @@ -25,91 +27,133 @@ internal ModelContext(MaINSettings settings, IHttpClientFactory httpClientFactor _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); } - public List GetAll() => KnownModels.All(); + public IEnumerable GetAll() => ModelRegistry.GetAll(); - public Model GetModel(string model) => KnownModels.GetModel(model); + public IEnumerable GetAllLocal() => ModelRegistry.GetAllLocal(); - public Model GetEmbeddingModel() => KnownModels.GetEmbeddingModel(); + public AIModel GetModel(string modelId) => ModelRegistry.GetById(modelId); - public bool Exists(string modelName) + public AIModel GetEmbeddingModel() => new Nomic_Embedding(); + + public bool Exists(string modelId) { - if (string.IsNullOrWhiteSpace(modelName)) + if (string.IsNullOrWhiteSpace(modelId)) { - throw new ArgumentException(nameof(modelName)); + throw new MissingModelIdException(nameof(modelId)); } - var model = KnownModels.GetModel(modelName); - var modelPath = GetModelFilePath(model.FileName); + var model = ModelRegistry.GetById(modelId); + if (model is not LocalModel localModel) + { + return false; // Cloud models don't have local files + } + + var modelPath = GetModelFilePath(localModel.FileName); return File.Exists(modelPath); } - public async Task DownloadAsync(string modelName, CancellationToken cancellationToken = default) + public async Task DownloadAsync(string modelId, CancellationToken cancellationToken = default) { - if (string.IsNullOrWhiteSpace(modelName)) + if (string.IsNullOrWhiteSpace(modelId)) { - throw new MissingModelNameException(nameof(modelName)); + throw new MissingModelIdException(nameof(modelId)); } - var model = KnownModels.GetModel(modelName); - await DownloadModelAsync(model.DownloadUrl!, model.FileName, cancellationToken); + var model = ModelRegistry.GetById(modelId) ?? throw new ModelNotSupportedException(modelId); + + if (model is not LocalModel localModel) + { + throw new InvalidModelTypeException(nameof(LocalModel)); + } + + if (localModel.DownloadUrl is null) + { + throw new DownloadUrlNullOrEmptyException(); + } + + await DownloadModelAsync(localModel.DownloadUrl.ToString(), localModel.FileName, cancellationToken); return this; } - public async Task DownloadAsync(string model, string url) + public async Task DownloadAsync(string modelId, string url, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(model)) + if (string.IsNullOrWhiteSpace(modelId)) { - throw new MissingModelNameException(nameof(model)); + throw new MissingModelIdException(nameof(modelId)); } if (string.IsNullOrWhiteSpace(url)) { - throw new ArgumentException("URL cannot be null or empty", nameof(url)); + throw new DownloadUrlNullOrEmptyException(); } - var fileName = $"{model}.gguf"; - await DownloadModelAsync(url, fileName, CancellationToken.None); + var fileName = $"{modelId}.gguf"; + await DownloadModelAsync(url, fileName, cancellationToken); + + // Register the newly downloaded model + var newModel = new GenericLocalModel( + FileName: fileName, + Name: modelId, + Id: modelId, + DownloadUrl: new Uri(url) + ); + ModelRegistry.RegisterOrReplace(newModel); - var filePath = GetModelFilePath(fileName); - KnownModels.AddModel(model, filePath); return this; } [Obsolete("Use async method instead")] - public IModelContext Download(string modelName) + public IModelContext Download(string modelId) { - if (string.IsNullOrWhiteSpace(modelName)) + if (string.IsNullOrWhiteSpace(modelId)) + { + throw new MissingModelIdException(nameof(modelId)); + } + + var model = ModelRegistry.GetById(modelId) ?? throw new ModelNotSupportedException(modelId); + if (model is not LocalModel localModel) { - throw new MissingModelNameException(nameof(modelName)); + throw new MissingModelIdException(nameof(LocalModel)); } - var model = KnownModels.GetModel(modelName); - DownloadModelSync(model.DownloadUrl!, model.FileName); + if (localModel.DownloadUrl is null) + { + throw new DownloadUrlNullOrEmptyException(); + } + + DownloadModelSync(localModel.DownloadUrl.ToString(), localModel.FileName); return this; } - [Obsolete("Obsolete async method instead")] - public IModelContext Download(string model, string url) + [Obsolete("Use async method instead")] + public IModelContext Download(string modelId, string url) { - if (string.IsNullOrWhiteSpace(model)) + if (string.IsNullOrWhiteSpace(modelId)) { - throw new MissingModelNameException(nameof(model)); + throw new MissingModelIdException(nameof(modelId)); } if (string.IsNullOrWhiteSpace(url)) { - throw new ArgumentException("URL cannot be null or empty", nameof(url)); + throw new DownloadUrlNullOrEmptyException(); } - var fileName = $"{model}.gguf"; + var fileName = $"{modelId}.gguf"; DownloadModelSync(url, fileName); - - var filePath = GetModelFilePath(fileName); - KnownModels.AddModel(model, filePath); + + // Register the newly downloaded model + var newModel = new GenericLocalModel( + FileName: fileName, + Name: modelId, + Id: modelId, + DownloadUrl: new Uri(url) + ); + ModelRegistry.RegisterOrReplace(newModel); + return this; } - public IModelContext LoadToCache(Model model) + public IModelContext LoadToCache(LocalModel model) { ArgumentNullException.ThrowIfNull(model); @@ -118,7 +162,7 @@ public IModelContext LoadToCache(Model model) return this; } - public async Task LoadToCacheAsync(Model model) + public async Task LoadToCacheAsync(LocalModel model) { ArgumentNullException.ThrowIfNull(model); @@ -153,7 +197,7 @@ private async Task DownloadModelAsync(string url, string fileName, CancellationT } } - private async Task DownloadWithProgressAsync(HttpResponseMessage response, string filePath, string fileName, CancellationToken cancellationToken) + private static async Task DownloadWithProgressAsync(HttpResponseMessage response, string filePath, string fileName, CancellationToken cancellationToken) { var totalBytes = response.Content.Headers.ContentLength; var totalBytesRead = 0L; @@ -171,10 +215,13 @@ private async Task DownloadWithProgressAsync(HttpResponseMessage response, strin while (true) { - var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); - if (bytesRead == 0) break; + var bytesRead = await contentStream.ReadAsync(buffer, cancellationToken); + if (bytesRead == 0) + { + break; + } - await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken); + await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); totalBytesRead += bytesRead; if (ShouldUpdateProgress(progressStopwatch)) @@ -273,7 +320,7 @@ private static void ShowProgress(long totalBytesRead, long? totalBytes, Stopwatc $"Speed: {FormatBytes((long)speed)}/s ETA: {eta:hh\\:mm\\:ss}"); var (leftAfter, topAfter) = Console.GetCursorPosition(); - int lengthDifference = leftBefore - leftAfter + (topBefore - topAfter) * Console.WindowWidth; + int lengthDifference = leftBefore - leftAfter + ((topBefore - topAfter) * Console.WindowWidth); while (lengthDifference > 0) { Console.Write(' '); @@ -301,7 +348,10 @@ private static void ShowFinalProgress(long totalBytesRead, Stopwatch totalStopwa private static string FormatBytes(long bytes) { - if (bytes == 0) return "0 Bytes"; + if (bytes == 0) + { + return "0 Bytes"; + } const int scale = 1024; string[] orders = ["GB", "MB", "KB", "Bytes"]; @@ -310,14 +360,17 @@ private static string FormatBytes(long bytes) foreach (var order in orders) { if (bytes >= max) + { return $"{decimal.Divide(bytes, max):##.##} {order}"; + } + max /= scale; } return "0 Bytes"; } - private string ResolvePath(string? settingsModelsPath) => + private static string ResolvePath(string? settingsModelsPath) => settingsModelsPath ?? Environment.GetEnvironmentVariable("MaIN_ModelsPath") ?? throw new ModelsPathNotFoundException(); diff --git a/src/MaIN.Domain/Entities/Chat.cs b/src/MaIN.Domain/Entities/Chat.cs index d88b8d1..b658831 100644 --- a/src/MaIN.Domain/Entities/Chat.cs +++ b/src/MaIN.Domain/Entities/Chat.cs @@ -1,6 +1,7 @@ using LLama.Batched; using MaIN.Domain.Configuration; using MaIN.Domain.Entities.Tools; +using MaIN.Domain.Models.Abstract; namespace MaIN.Domain.Entities; @@ -8,7 +9,13 @@ public class Chat { public string Id { get; init; } = string.Empty; public required string Name { get; init; } - public required string Model { get; set; } + public required string ModelId { get; set; } + private AIModel? _modelInstance; + public AIModel? ModelInstance + { + get => _modelInstance ??= ModelRegistry.GetById(ModelId); + set => (_modelInstance, ModelId) = (value, value?.Id ?? ModelId); + } public List Messages { get; set; } = []; public ChatType Type { get; set; } = ChatType.Conversation; public bool Visual { get; set; } @@ -18,10 +25,9 @@ public class Chat public TextToSpeechParams? TextToSpeechParams { get; set; } public Dictionary Properties { get; init; } = []; public List Memory { get; } = []; - public BackendType? Backend { get; set; } + public BackendType? Backend { get; set; } // TODO: remove because of ModelInstance public Conversation.State? ConversationState { get; set; } public bool Interactive = false; public bool Translate = false; - } \ No newline at end of file diff --git a/src/MaIN.Domain/Exceptions/ModelException.cs b/src/MaIN.Domain/Exceptions/ModelException.cs new file mode 100644 index 0000000..8bce057 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/ModelException.cs @@ -0,0 +1,5 @@ +namespace MaIN.Domain.Exceptions; + +public class ModelException(string message) : Exception(message) +{ +} diff --git a/src/MaIN.Domain/Exceptions/Models/InvalidModelType.cs b/src/MaIN.Domain/Exceptions/Models/InvalidModelType.cs new file mode 100644 index 0000000..e87f188 --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Models/InvalidModelType.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Models; + +public class InvalidModelTypeException(string expectedType) + : MaINCustomException($"Expected {expectedType}") +{ + public override string PublicErrorMessage => "Invalid model type."; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.BadRequest; +} diff --git a/src/MaIN.Domain/Exceptions/Models/LocalModels/DownloadUrlNullOrEmptyException.cs b/src/MaIN.Domain/Exceptions/Models/LocalModels/DownloadUrlNullOrEmptyException.cs new file mode 100644 index 0000000..56096cf --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Models/LocalModels/DownloadUrlNullOrEmptyException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Models.LocalModels; + +public class DownloadUrlNullOrEmptyException() + : MaINCustomException("Download url cannot be null or empty.") +{ + public override string PublicErrorMessage => "Download url 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/MissingModelNameException.cs b/src/MaIN.Domain/Exceptions/Models/MissingModelIdException.cs similarity index 59% rename from src/MaIN.Domain/Exceptions/Models/MissingModelNameException.cs rename to src/MaIN.Domain/Exceptions/Models/MissingModelIdException.cs index ea5e299..d0cdd96 100644 --- a/src/MaIN.Domain/Exceptions/Models/MissingModelNameException.cs +++ b/src/MaIN.Domain/Exceptions/Models/MissingModelIdException.cs @@ -2,9 +2,9 @@ namespace MaIN.Domain.Exceptions.Models; -public class MissingModelNameException(string modelNameParameter) - : MaINCustomException($"Model name cannot be null or empty, {modelNameParameter}.") +public class MissingModelIdException(string modelIdParameter) + : MaINCustomException($"Model id cannot be null or empty, {modelIdParameter}.") { public override string PublicErrorMessage => "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/MissingModelInstanceException.cs b/src/MaIN.Domain/Exceptions/Models/MissingModelInstanceException.cs new file mode 100644 index 0000000..79e342b --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Models/MissingModelInstanceException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Models; + +public class MissingModelInstanceException() + : MaINCustomException("Model instance cannot be null.") +{ + public override string PublicErrorMessage => "Model instance cannot be null."; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.BadRequest; +} diff --git a/src/MaIN.Domain/Models/Abstract/AIModel.cs b/src/MaIN.Domain/Models/Abstract/AIModel.cs new file mode 100644 index 0000000..00e55d1 --- /dev/null +++ b/src/MaIN.Domain/Models/Abstract/AIModel.cs @@ -0,0 +1,236 @@ +using MaIN.Domain.Configuration; + +namespace MaIN.Domain.Models.Abstract; + +public abstract record AIModel( + string Id, + BackendType Backend, + string? Name = null, + uint MaxContextWindowSize = 128000, + string? Description = null, + string? SystemMessage = null) +{ + /// Internal Id. For cloud models it is the cloud Id. + public virtual string Id { get; } = Id; + + /// Name displayed to users. + public virtual string Name { get; } = Name ?? Id; + + /// Gets the type of backend used by the model eg OpenAI or Self (Local). + public virtual BackendType Backend { get; } = Backend; + + /// System Message added before first prompt. + public virtual string? SystemMessage { get; } = SystemMessage; + + /// Model description eg. capabilities or purpose. + public virtual string? Description { get; } = Description; + + /// Max context window size supported by the model. + public virtual uint MaxContextWindowSize { get; } = MaxContextWindowSize; + + /// Checks if model supports reasoning/thinking mode. + public bool HasReasoning => this is IReasoningModel; + + /// Checks if model supports vision/image input. + public bool HasVision => this is IVisionModel; +} + +/// Base class for local models. +public abstract record LocalModel( + string Id, + string FileName, + Uri? DownloadUrl = null, + string? Name = null, + uint MaxContextWindowSize = 128000, + string? Description = null, + string? SystemMessage = null) : AIModel(Id, BackendType.Self, Name, MaxContextWindowSize, Description, SystemMessage) +{ + /// Name of the model file on the hard drive eg. Gemma2-2b.gguf + public virtual string FileName { get; } = FileName; + + /// Uri to download model eg. https://huggingface.co/Inza124/gemma2_2b/resolve/main/gemma2-2b-maIN.gguf?download=true + public virtual Uri? DownloadUrl { get; } = DownloadUrl; + + public virtual bool IsDownloaded(string basePath) + => File.Exists(Path.Combine(basePath, FileName)); + + public virtual string GetFullPath(string basePath) + => Path.Combine(basePath, FileName); +} + +/// Base class for cloud models. +public abstract record CloudModel( + string Id, + BackendType Backend, + string? Name = null, + uint MaxContextWindowSize = 128000, + string? Description = null, + string? SystemMessage = null) : AIModel(Id, Backend, Name, MaxContextWindowSize, Description, SystemMessage) +{ +} + +/// Generic class for runtime defined cloud models. +public record GenericCloudModel( + string Id, + BackendType Backend, + string? Name = null, + uint MaxContextWindowSize = 128000, + string? Description = null, + string? SystemMessage = null +) : CloudModel(Id, Backend, Name, MaxContextWindowSize, Description, SystemMessage) +{ +} + +/// Generic class for runtime defined cloud models with reasoning capability. +public record GenericCloudReasoningModel( + string Id, + BackendType Backend, + string? Name = null, + uint MaxContextWindowSize = 128000, + string? Description = null, + string? SystemMessage = null, + string? AdditionalPrompt = null +) : CloudModel(Id, Backend, Name, MaxContextWindowSize, Description, SystemMessage), IReasoningModel +{ + // IReasoningModel - null for cloud (handled by provider API) + public Func? ReasonFunction => null; + public string? AdditionalPrompt { get; } = AdditionalPrompt; +} + +/// Generic class for runtime defined cloud models with vision capability. +public record GenericCloudVisionModel( + string Id, + BackendType Backend, + string? Name = null, + uint MaxContextWindowSize = 128000, + string? Description = null, + string? SystemMessage = null +) : CloudModel(Id, Backend, Name, MaxContextWindowSize, Description, SystemMessage), IVisionModel +{ + // IVisionModel - cloud models don't need MMProjectPath + public string? MMProjectPath => null; +} + +/// Generic class for runtime defined cloud models with both vision and reasoning capabilities. +public record GenericCloudVisionReasoningModel( + string Id, + BackendType Backend, + string? Name = null, + uint MaxContextWindowSize = 128000, + string? Description = null, + string? SystemMessage = null, + string? AdditionalPrompt = null +) : CloudModel(Id, Backend, Name, MaxContextWindowSize, Description, SystemMessage), IVisionModel, IReasoningModel +{ + // IVisionModel - null for cloud (handled by provider API) + public string? MMProjectPath => null; + + // IReasoningModel - null for cloud (handled by provider API) + public Func? ReasonFunction => null; + public string? AdditionalPrompt { get; } = AdditionalPrompt; +} + +/// Generic class for runtime defined local models. +public record GenericLocalModel( + string FileName, + string? Name = null, + string? Id = null, + Uri? DownloadUrl = null, + uint MaxContextWindowSize = 4096, + string? CustomPath = null, + string? Description = null, + string? SystemMessage = null +) : LocalModel(Id ?? FileName, FileName, DownloadUrl, Name ?? FileName, MaxContextWindowSize, Description, SystemMessage) +{ + /// Custom path override for the model file (only for dynamically loaded models). + public string? CustomPath { get; set; } = CustomPath; + + public override bool IsDownloaded(string basePath) + => File.Exists(Path.Combine(CustomPath ?? basePath, FileName)); + + public override string GetFullPath(string basePath) + => Path.Combine(CustomPath ?? basePath, FileName); +} + +/// Generic class for runtime defined local models with reasoning capability. +public record GenericReasoningModel( + string FileName, + Func ReasonFunction, + string? Name = null, + string? Id = null, + Uri? DownloadUrl = null, + uint MaxContextWindowSize = 4096, + string? CustomPath = null, + string? AdditionalPrompt = null, + string? Description = null, + string? SystemMessage = null +) : LocalModel(Id ?? FileName, FileName, DownloadUrl, Name ?? FileName, MaxContextWindowSize, Description, SystemMessage), IReasoningModel +{ + public string? CustomPath { get; set; } = CustomPath; + + // IReasoningModel implementation + public Func ReasonFunction { get; } = ReasonFunction; + public string? AdditionalPrompt { get; } = AdditionalPrompt; + + public override bool IsDownloaded(string basePath) + => File.Exists(Path.Combine(CustomPath ?? basePath, FileName)); + + public override string GetFullPath(string basePath) + => Path.Combine(CustomPath ?? basePath, FileName); +} + +/// Generic class for runtime defined local models with vision capability. +public record GenericVisionModel( + string FileName, + string MMProjectPath, + string? Name = null, + string? Id = null, + Uri? DownloadUrl = null, + uint MaxContextWindowSize = 4096, + string? CustomPath = null, + string? Description = null, + string? SystemMessage = null +) : LocalModel(Id ?? FileName, FileName, DownloadUrl, Name ?? FileName, MaxContextWindowSize, Description, SystemMessage), IVisionModel +{ + public string? CustomPath { get; set; } = CustomPath; + + // IVisionModel implementation + public string MMProjectPath { get; } = MMProjectPath; + + public override bool IsDownloaded(string basePath) + => File.Exists(Path.Combine(CustomPath ?? basePath, FileName)); + + public override string GetFullPath(string basePath) + => Path.Combine(CustomPath ?? basePath, FileName); +} + +/// Generic class for runtime defined local models with both vision and reasoning capabilities. +public record GenericVisionReasoningModel( + string FileName, + string MMProjectPath, + Func ReasonFunction, + string? Name = null, + string? Id = null, + Uri? DownloadUrl = null, + uint MaxContextWindowSize = 4096, + string? CustomPath = null, + string? AdditionalPrompt = null, + string? Description = null, + string? SystemMessage = null +) : LocalModel(Id ?? FileName, FileName, DownloadUrl, Name ?? FileName, MaxContextWindowSize, Description, SystemMessage), IVisionModel, IReasoningModel +{ + public string? CustomPath { get; set; } = CustomPath; + + // IVisionModel implementation + public string MMProjectPath { get; } = MMProjectPath; + + // IReasoningModel implementation + public Func ReasonFunction { get; } = ReasonFunction; + public string? AdditionalPrompt { get; } = AdditionalPrompt; + + public override bool IsDownloaded(string basePath) + => File.Exists(Path.Combine(CustomPath ?? basePath, FileName)); + + public override string GetFullPath(string basePath) + => Path.Combine(CustomPath ?? basePath, FileName); +} diff --git a/src/MaIN.Domain/Models/Abstract/IModelCapabilities.cs b/src/MaIN.Domain/Models/Abstract/IModelCapabilities.cs new file mode 100644 index 0000000..63dd55b --- /dev/null +++ b/src/MaIN.Domain/Models/Abstract/IModelCapabilities.cs @@ -0,0 +1,56 @@ +using MaIN.Domain.Models; + +namespace MaIN.Domain.Models.Abstract; + +/// +/// Interface for models that support vision/image input capabilities. +/// +public interface IVisionModel +{ + /// + /// Path to multimodal projector file required for vision processing. + /// Null for cloud models (handled by provider API). + /// + string? MMProjectPath { get; } +} + +/// +/// Interface for models that support reasoning/thinking capabilities. +/// +public interface IReasoningModel +{ + /// + /// Function to process reasoning tokens. + /// Null for cloud models (reasoning handled by provider API). + /// + Func? ReasonFunction { get; } + + /// + /// Additional prompt added to enable reasoning mode. + /// + string? AdditionalPrompt { get; } +} + +// TODO: use it with existing embedding model +/// +/// Interface for models that support embeddings generation. +/// +public interface IEmbeddingModel +{ + /// + /// Dimension of the embedding vector. + /// + int EmbeddingDimension { get; } +} + +// TODO: use it with existing TTS model +/// +/// Interface for models that support text-to-speech. +/// +public interface ITTSModel +{ + /// + /// Available voices for the TTS model. + /// + IReadOnlyList AvailableVoices { get; } +} diff --git a/src/MaIN.Domain/Models/Abstract/ModelRegistry.cs b/src/MaIN.Domain/Models/Abstract/ModelRegistry.cs new file mode 100644 index 0000000..f3182f7 --- /dev/null +++ b/src/MaIN.Domain/Models/Abstract/ModelRegistry.cs @@ -0,0 +1,183 @@ +using MaIN.Domain.Exceptions; +using System.Collections.Concurrent; +using System.Reflection; + +namespace MaIN.Domain.Models.Abstract; + +public static class ModelRegistry +{ + private static readonly ConcurrentDictionary _models = new(StringComparer.OrdinalIgnoreCase); + private static bool _initialized = false; + private static readonly object _lock = new(); + + static ModelRegistry() + { + Initialize(); + } + + private static void Initialize() + { + if (_initialized) + { + return; + } + + lock (_lock) + { + if (_initialized) + { + return; + } + + // Reflection but only at startup to register all available models + // Skip abstract, generic types, and Generic* classes (they're for runtime registration) + var modelTypes = Assembly.GetExecutingAssembly() + .GetTypes() + .Where(t => t.IsSubclassOf(typeof(AIModel)) + && !t.IsAbstract + && !t.IsGenericType + && !t.Name.StartsWith("Generic")); + + foreach (var type in modelTypes) + { + try + { + if (Activator.CreateInstance(type) is AIModel instance) + { + Register(instance); + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Could not register model type {type.Name}: {ex.Message}"); + } + } + + _initialized = true; + } + } + + /// + /// Registers a custom model at runtime (e.g., dynamically loaded GGUF file). + /// + public static void Register(AIModel model) + { + ArgumentNullException.ThrowIfNull(model); + + if (string.IsNullOrWhiteSpace(model.Id)) + { + throw new ArgumentException("Model ID cannot be null or empty.", nameof(model)); + } + + var normalizedId = NormalizeId(model.Id); + + if (!_models.TryAdd(normalizedId, model)) + { + throw new InvalidOperationException($"Model with ID '{model.Id}' is already registered."); + } + } + + /// + /// Registers a custom model at runtime, replacing existing if present. + /// + public static void RegisterOrReplace(AIModel model) + { + ArgumentNullException.ThrowIfNull(model); + + if (string.IsNullOrWhiteSpace(model.Id)) + { + throw new ArgumentException("Model ID cannot be null or empty.", nameof(model)); + } + + var normalizedId = NormalizeId(model.Id); + _models[normalizedId] = model; + } + + /// + /// Gets a model by its ID. + /// + public static AIModel GetById(string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentException("Model ID cannot be null or empty.", nameof(id)); + } + + var normalizedId = NormalizeId(id); + + if (_models.TryGetValue(normalizedId, out var model)) + { + return model; + } + + var availableIds = string.Join(", ", _models.Keys.Take(10)); + throw new KeyNotFoundException($"Model with ID '{id}' not found. Available models (first 10): {availableIds}"); + } + + /// + /// Tries to get a model by its ID. + /// + public static bool TryGetById(string id, out AIModel? model) + { + model = null; + + if (string.IsNullOrWhiteSpace(id)) + { + return false; + } + + var normalizedId = NormalizeId(id); + return _models.TryGetValue(normalizedId, out model); + } + + /// + /// Gets a local model by its filename. + /// + public static LocalModel? GetByFileName(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + return null; + } + + return _models.Values + .OfType() + .FirstOrDefault(m => m.FileName.Equals(fileName, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Gets all registered models. + /// + public static IEnumerable GetAll() => _models.Values; + + /// + /// Gets all local models. + /// + public static IEnumerable GetAllLocal() => _models.Values.OfType(); + + /// + /// Gets all cloud models. + /// + public static IEnumerable GetAllCloud() => _models.Values.OfType(); + + /// + /// Checks if a model with the given ID exists. + /// + public static bool Exists(string id) => + !string.IsNullOrWhiteSpace(id) && _models.ContainsKey(NormalizeId(id)); + + /// + /// Removes a model from the registry (useful for dynamically loaded models). + /// + public static bool Unregister(string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + return false; + } + + return _models.TryRemove(NormalizeId(id), out _); + } + + private static string NormalizeId(string id) => id.Trim(); +} diff --git a/src/MaIN.Domain/Models/Concrete/CloudModels.cs b/src/MaIN.Domain/Models/Concrete/CloudModels.cs new file mode 100644 index 0000000..4f370de --- /dev/null +++ b/src/MaIN.Domain/Models/Concrete/CloudModels.cs @@ -0,0 +1,137 @@ +using MaIN.Domain.Configuration; +using MaIN.Domain.Models.Abstract; + +namespace MaIN.Domain.Models.Concrete; + +// ===== OpenAI Models ===== + +public sealed record Gpt4oMini() : CloudModel( + "gpt-4o-mini", + BackendType.OpenAi, + "GPT-4o Mini", + 128000, + "Fast and affordable OpenAI model for everyday tasks") +{ +} + +public sealed record Gpt4_1Mini() : CloudModel( + "gpt-4.1-mini", + BackendType.OpenAi, + "GPT-4.1 Mini", + 128000, + "Updated mini model with improved capabilities") +{ +} + +public sealed record Gpt5Nano() : CloudModel( + "gpt-5-nano", + BackendType.OpenAi, + "GPT-5 Nano", + 128000, + "Next generation OpenAI nano model") +{ +} + +public sealed record DallE3() : CloudModel( + "dall-e-3", + BackendType.OpenAi, + "DALL-E 3", + 4000, + "Advanced image generation model from OpenAI") +{ +} + +// ===== Anthropic Models ===== + +public sealed record ClaudeSonnet4() : CloudModel( + "claude-sonnet-4-20250514", + BackendType.Anthropic, + "Claude Sonnet 4", + 200000, + "Latest Claude model with enhanced reasoning capabilities") +{ +} + +public sealed record ClaudeSonnet4_5() : CloudModel( + "claude-sonnet-4-5-20250929", + BackendType.Anthropic, + "Claude Sonnet 4.5", + 200000, + "Advanced Claude model with superior performance and extended context") +{ +} + +// ===== Gemini Models ===== + +public sealed record Gemini2_5Flash() : CloudModel( + "gemini-2.5-flash", + BackendType.Gemini, + "Gemini 2.5 Flash", + 1000000, + "Fast and efficient Google Gemini model for quick responses") +{ +} + +public sealed record Gemini2_0Flash() : CloudModel( + "gemini-2.0-flash", + BackendType.Gemini, + "Gemini 2.0 Flash", + 1000000, + "Google Gemini 2.0 flash model optimized for speed and efficiency") +{ +} + +// ===== xAI Models ===== + +public sealed record Grok3Beta() : CloudModel( + "grok-3-beta", + BackendType.Xai, + "Grok 3 Beta", + 128000, + "xAI latest Grok model in beta testing phase") +{ +} + +// ===== GroqCloud Models ===== + +public sealed record Llama3_1_8bInstant() : CloudModel( + "llama-3.1-8b-instant", + BackendType.GroqCloud, + "Llama 3.1 8B Instant", + 8192, + "Meta Llama 3.1 8B model optimized for fast inference on Groq hardware") +{ +} + +public sealed record GptOss20b() : CloudModel( + "openai/gpt-oss-20b", + BackendType.GroqCloud, + "GPT OSS 20B", + 8192, + "Open-source 20B parameter GPT model running on Groq infrastructure") +{ +} + +// ===== DeepSeek Models ===== + +public sealed record DeepSeekReasoner() : CloudModel( + "deepseek-reasoner", + BackendType.DeepSeek, + "DeepSeek Reasoner", + 64000, + "DeepSeek reasoning-focused model for complex problem solving"), IReasoningModel +{ + public Func? ReasonFunction => null; + public string? AdditionalPrompt => null; +} + +// ===== Ollama Models ===== + +public sealed record OllamaGemma3_4b() : CloudModel( + "gemma3:4b", + BackendType.Ollama, + "Gemma3 4B (Ollama)", + 8192, + "Balanced 4B model running on Ollama for writing, analysis, and mathematical reasoning") +{ +} diff --git a/src/MaIN.Domain/Models/Concrete/LocalModels.cs b/src/MaIN.Domain/Models/Concrete/LocalModels.cs new file mode 100644 index 0000000..e762280 --- /dev/null +++ b/src/MaIN.Domain/Models/Concrete/LocalModels.cs @@ -0,0 +1,355 @@ +using MaIN.Domain.Models.Abstract; + +namespace MaIN.Domain.Models.Concrete; + +// ===== Gemma Family ===== + +public sealed record Gemma2_2b() : LocalModel( + "gemma2-2b", + "Gemma2-2b.gguf", + new Uri("https://huggingface.co/Inza124/gemma2_2b/resolve/main/gemma2-2b-maIN.gguf?download=true"), + "Gemma 2B", + 8192, + "Lightweight 2B model for general-purpose text generation and understanding") +{ +} + +public sealed record Gemma3_4b() : LocalModel( + "gemma3-4b", + "Gemma3-4b.gguf", + new Uri("https://huggingface.co/Inza124/Gemma3-4b/resolve/main/gemma3-4b.gguf?download=true"), + "Gemma3 4B", + 8192, + "Balanced 4B model for writing, analysis, and mathematical reasoning") +{ +} + +public sealed record Gemma3_12b() : LocalModel( + "gemma3-12b", + "Gemma3-12b.gguf", + new Uri("https://huggingface.co/Inza124/Gemma3-12b/resolve/main/gemma3-12b.gguf?download=true"), + "Gemma3 12B", + 8192, + "Large 12B model for complex analysis, research, and creative writing") +{ +} + +public sealed record Gemma3n_e4b() : LocalModel( + "gemma3n-e4b", + "Gemma3n-e4b.gguf", + new Uri("https://huggingface.co/Inza124/Gemma-3n-e4b/resolve/main/gemma-3n-e4b.gguf?download=true"), + "Gemma3n E4B", + 8192, + "Compact 4B model optimized for efficient reasoning and general-purpose tasks") +{ +} + +// ===== Llama Family ===== + +public sealed record Llama3_2_3b() : LocalModel( + "llama3.2-3b", + "Llama3.2-3b.gguf", + new Uri("https://huggingface.co/Inza124/Llama3.2_3b/resolve/main/Llama3.2-maIN.gguf?download=true"), + "Llama 3.2 3B", + 8192, + "Lightweight 3B model for chatbots, content creation, and basic coding") +{ +} + +public sealed record Llama3_1_8b() : LocalModel( + "llama3.1-8b", + "Llama3.1-8b.gguf", + new Uri("https://huggingface.co/Inza124/Llama3.1_8b/resolve/main/Llama3.1-maIN.gguf?download=true"), + "Llama 3.1 8B", + 8192, + "Versatile 8B model for writing, coding, math, and general assistance") +{ +} + +public sealed record Llava_7b() : LocalModel( + "llava-7b", + "Llava.gguf", + new Uri("https://huggingface.co/Inza124/Llava/resolve/main/Llava-maIN.gguf?download=true"), + "LLaVA 7B", + 4096, + "Vision-language model for image analysis, OCR, and visual Q&A"), IVisionModel +{ + public string MMProjectPath => "mmproj-model-f16.gguf"; +} + +public sealed record Llava16Mistral_7b() : LocalModel( + "llava-1.6-mistral-7b", + "llava-1.6-mistral-7b.gguf", + new Uri("https://huggingface.co/cjpais/llava-1.6-mistral-7b-gguf/resolve/main/llava-v1.6-mistral-7b.Q3_K_XS.gguf?download=true"), + "LLaVA 1.6 Mistral 7B", + 4096, + "Vision-language model for image analysis, OCR, and visual Q&A"), IVisionModel +{ + public string MMProjectPath => "mmproj-model-f16.gguf"; +} + +// ===== Hermes Family ===== + +public sealed record Hermes3_3b() : LocalModel( + "hermes3-3b", + "Hermes3-3b.gguf", + new Uri("https://huggingface.co/Inza124/Hermes3-3b/resolve/main/hermes3-3b.gguf?download=true"), + "Hermes 3 3B", + 8192, + "Efficient 3B model for dialogue, roleplay, and conversational AI") +{ +} + +public sealed record Hermes3_8b() : LocalModel( + "hermes3-8b", + "Hermes3-8b.gguf", + new Uri("https://huggingface.co/Inza124/Hermes3_8b/resolve/main/hermes3-8b.gguf?download=true"), + "Hermes 3 8B", + 8192, + "Enhanced 8B model for complex dialogue, storytelling, and advice") +{ +} + +// ===== Qwen Family ===== + +public sealed record Qwen2_5_0_5b() : LocalModel( + "qwen2.5-0.5b", + "Qwen2.5-0.5b.gguf", + new Uri("https://huggingface.co/Inza124/Qwen2.5/resolve/main/Qwen2.5-maIN.gguf?download=true"), + "Qwen 2.5 0.5B", + 4096, + "Ultra-lightweight 0.5B model for simple text completion and basic tasks") +{ +} + +public sealed record Qwen2_5_Coder_3b() : LocalModel( + "qwen2.5-coder-3b", + "Qwen2.5-coder-3b.gguf", + new Uri("https://huggingface.co/Inza124/Qwen2.5-Coder-3b/resolve/main/Qwen2.5-coder-3b.gguf?download=true"), + "Qwen 2.5 Coder 3B", + 8192, + "Compact 3B model for Python, JavaScript, bug fixing, and code review") +{ +} + +public sealed record Qwen2_5_Coder_7b() : LocalModel( + "qwen2.5-coder-7b", + "Qwen2.5-coder-7b.gguf", + new Uri("https://huggingface.co/Inza124/Qwen2.5-Coder-7b/resolve/main/Qwen2.5-coder-7b.gguf?download=true"), + "Qwen 2.5 Coder 7B", + 8192, + "Advanced 7B model for full-stack development, API design, and testing") +{ +} + +public sealed record Qwen2_5_Coder_14b() : LocalModel( + "qwen2.5-coder-14b", + "Qwen2.5-coder-14b.gguf", + new Uri("https://huggingface.co/Inza124/Qwen2.5-Coder-14b/resolve/main/Qwen2.5-coder-14b.gguf?download=true"), + "Qwen 2.5 Coder 14B", + 8192, + "Professional 14B model for system design, architecture, and code refactoring") +{ +} + +public sealed record Qwen3_8b() : LocalModel( + "qwen3-8b", + "Qwen3-8b.gguf", + new Uri("https://huggingface.co/Inza124/Qwen3-8b/resolve/main/Qwen3-8b.gguf?download=true"), + "Qwen 3 8B", + 8192, + "Fast 8B model for multilingual tasks, translation, and logical reasoning" + ), IReasoningModel +{ + // IReasoningModel implementation + public Func? ReasonFunction => ReasoningFunctions.ProcessDeepSeekToken; + public string? AdditionalPrompt => null; +} + +public sealed record Qwen3_14b() : LocalModel( + "qwen3-14b", + "Qwen3-14b.gguf", + new Uri("https://huggingface.co/Inza124/Qwen3-14b/resolve/main/Qwen3-14b.gguf?download=true"), + "Qwen 3 14B", + 8192, + "Advanced 14B model for complex reasoning, research, and document analysis"), IReasoningModel +{ + public Func? ReasonFunction => ReasoningFunctions.ProcessDeepSeekToken; + public string? AdditionalPrompt => null; +} + +public sealed record QwQ_7b() : LocalModel( + "qwq-7b", + "QwQ-7b.gguf", + new Uri("https://huggingface.co/Inza124/QwQ-7b/resolve/main/qwq-7b.gguf?download=true"), + "QwQ 7B", + 8192, + "Reasoning-focused 7B model for step-by-step problem solving and analysis"), IReasoningModel +{ + public Func? ReasonFunction => ReasoningFunctions.ProcessQwQ_QwenModToken; + public string? AdditionalPrompt => "- Output nothing before , enclose all step-by-step reasoning (excluding the final answer) within ..., and place the final answer immediately after the closing "; +} + +// ===== DeepSeek Family ===== + +public sealed record DeepSeek_R1_8b() : LocalModel( + "deepseekr1-8b", + "DeepSeekR1-8b.gguf", + new Uri("https://huggingface.co/Inza124/DeepseekR1-8b/resolve/main/DeepSeekR1-8b-maIN.gguf?download=true"), + "DeepSeek R1 8B", + 8192, + "Advanced 8B model for math proofs, scientific reasoning, and logical puzzles"), IReasoningModel +{ + public Func? ReasonFunction => ReasoningFunctions.ProcessDeepSeekToken; + public string? AdditionalPrompt => null; +} + +public sealed record DeepSeek_R1_1_5b() : LocalModel( + "deepseekr1-1.5b", + "DeepSeekR1-1.5b.gguf", + new Uri("https://huggingface.co/Inza124/DeepseekR1-1.5b/resolve/main/DeepSeekR1-1.5b.gguf?download=true"), + "DeepSeek R1 1.5B", + 4096, + "Compact 1.5B model for basic logic, simple math, and chain-of-thought tasks"), IReasoningModel +{ + public Func? ReasonFunction => ReasoningFunctions.ProcessDeepSeekToken; + public string? AdditionalPrompt => null; +} + +// ===== Phi Family ===== + +public sealed record Phi3_5_3b() : LocalModel( + "phi3.5-3b", + "phi3.5-3b.gguf", + new Uri("https://huggingface.co/Inza124/phi3.5-3b/resolve/main/phi3.5-3b.gguf?download=true"), + "Phi 3.5 3B", + 4096, + "Efficient 3B model for mobile apps, IoT devices, and edge computing") +{ +} + +public sealed record Phi4_4b() : LocalModel( + "phi4-4b", + "phi4-4b.gguf", + new Uri("https://huggingface.co/Inza124/Phi4-4b/resolve/main/phi4-4b.gguf?download=true"), + "Phi 4 4B", + 4096, + "Latest 4B model for factual Q&A, safety-focused applications, and education") +{ +} + +// ===== Other Models ===== + +public sealed record LFM2_1_2b() : LocalModel( + "lfm2-1.2b", + "lfm2-1.2b.gguf", + new Uri("https://huggingface.co/Inza124/Lfm2-1.2b/resolve/main/lfm2-1.2b.gguf?download=true"), + "LFM2 1.2B", + 4096, + "Lightweight modern 1.2B model for fast inference and resource-constrained environments") +{ +} + +public sealed record Minicpm4_8b() : LocalModel( + "minicpm4-8b", + "Minicpm4-8b.gguf", + new Uri("https://huggingface.co/Inza124/Minicpm4-8b/resolve/main/MiniCPM4-8b.gguf?download=true"), + "MiniCPM4 8B", + 8192, + "Mid-size 8B model balancing performance and efficiency for diverse applications") +{ +} + +public sealed record Mistral_3_2_24b() : LocalModel( + "mistral-3.2-24b", + "Mistral3.2-24b.gguf", + new Uri("https://huggingface.co/Inza124/Mistral3.2-24b/resolve/main/Mistral3.2-24b.gguf?download=true"), + "Mistral 3.2 24B", + 8192, + "Large 24B model offering advanced reasoning and comprehensive knowledge for complex tasks") +{ +} + +public sealed record Webgen_4b() : LocalModel( + "webgen-4b", + "webgen-4b.gguf", + new Uri("https://huggingface.co/Inza124/webgen-4b/resolve/main/Webgen-4b.gguf?download=true"), + "Webgen 4B", + 8192, + "Specialized 4B model optimized for web development and code generation tasks") +{ +} + +public sealed record Bielik_2_5_11b() : LocalModel( + "bielik-2.5-11b", + "Bielik2.5-11b.gguf", + new Uri("https://huggingface.co/Inza124/Bielik2.5-11b/resolve/main/Bielik2.5-11b.gguf?download=true"), + "Bielik 2.5 11B", + 8192, + "Large 11B Polish language model with strong multilingual capabilities and reasoning") +{ +} + +public sealed record OlympicCoder_7b() : LocalModel( + "olympiccoder-7b", + "Olympiccoder-7b.gguf", + new Uri("https://huggingface.co/Inza124/OlympicCoder-7b/resolve/main/OlympicCoder-7b.gguf?download=true"), + "OlympicCoder 7B", + 8192, + "Specialized 7B model for algorithms, data structures, and contest programming") +{ +} + +public sealed record Yi_6b() : LocalModel( + "yi-6b", + "Yi-6b.gguf", + new Uri("https://huggingface.co/Inza124/yi-6b/resolve/main/yi-6b.gguf?download=true"), + "Yi 6B", + 4096, + "Bilingual 6B model for Chinese-English translation and cultural content") +{ +} + +public sealed record Smollm2_0_1b() : LocalModel( + "smollm2-0.1b", + "Smollm2-0.1b.gguf", + new Uri("https://huggingface.co/Inza124/Smollm2-0.1b/resolve/main/smollm2-0.1b.gguf?download=true"), + "SmolLM2 0.1B", + 2048, + "Tiny 0.1B model for keyword extraction, simple classification, and demos") +{ +} + +public sealed record Olmo2_7b() : LocalModel( + "olmo2-7b", + "Olmo2-7b.gguf", + new Uri("https://huggingface.co/Inza124/Olmo2-7b/resolve/main/olmo2-7b.gguf?download=true"), + "OLMo2 7B", + 8192, + "Open-source 7B model for research, benchmarking, and academic studies") +{ +} + +// ===== Embedding Model ===== + +public sealed record Nomic_Embedding() : LocalModel( + "nomic-embedding", + "nomicv2.gguf", + new Uri("https://huggingface.co/Inza124/Nomic/resolve/main/nomicv2.gguf?download=true"), + "Nomic Embedding", + 8192, + "Model used to generate embeddings") +{ +} + +// ===== TTS Model ===== + +public sealed record Kokoro_82m() : LocalModel( + "kokoro-82m", + "kokoro.onnx", + new Uri("https://github.com/taylorchu/kokoro-onnx/releases/download/v0.2.0/kokoro.onnx"), + "Kokoro 82M", + 4096, + "Frontier TTS model for its size of 82 million parameters (text in/audio out)") +{ +} diff --git a/src/MaIN.Domain/Models/SupportedModels.cs b/src/MaIN.Domain/Models/SupportedModels.cs index d2fd387..76e936e 100644 --- a/src/MaIN.Domain/Models/SupportedModels.cs +++ b/src/MaIN.Domain/Models/SupportedModels.cs @@ -1,7 +1,9 @@ +using MaIN.Domain.Models.Abstract; using MaIN.Domain.Exceptions.Models; namespace MaIN.Domain.Models; +[Obsolete("Use AIModel and ModelRegistry instead. This class will be removed in future versions.")] public class Model { public required string Name { get; init; } @@ -15,6 +17,7 @@ public class Model public bool HasReasoning() => ReasonFunction is not null; } +[Obsolete("Use ModelRegistry instead. This class will be removed in future versions.")] public static class KnownModels { private static List Models { get; } = diff --git a/src/MaIN.InferPage/Components/Pages/Home.razor b/src/MaIN.InferPage/Components/Pages/Home.razor index 0c9c272..4e4ad60 100644 --- a/src/MaIN.InferPage/Components/Pages/Home.razor +++ b/src/MaIN.InferPage/Components/Pages/Home.razor @@ -7,6 +7,7 @@ @using MaIN.Domain.Entities @using MaIN.Domain.Exceptions @using MaIN.Domain.Models +@using MaIN.Domain.Models.Abstract @using Markdig @using Microsoft.FluentUI.AspNetCore.Components.Icons.Regular @using Message = MaIN.Domain.Entities.Message @@ -192,8 +193,7 @@ private string? _incomingReasoning = null; private readonly string? _displayName = Utils.Model; private IChatMessageBuilder? ctxBuilder; - // private ChatContext? ctx; - private Chat Chat { get; } = new() { Name = "MaIN Infer", Model = Utils.Model! }; + private Chat Chat { get; } = new() { Name = "MaIN Infer", ModelId = Utils.Model! }; private List Messages { get; set; } = new(); private ElementReference? _bottomElement; private int _inputKey = 0; @@ -221,7 +221,8 @@ } else if (!Utils.OpenAi) { - _reasoning = !Utils.Visual && KnownModels.GetModel(Utils.Model!).HasReasoning(); + var model = ModelRegistry.TryGetById(Utils.Model!, out var foundModel) ? foundModel : null; + _reasoning = !Utils.Visual && model?.HasReasoning == true; Utils.Reason = _reasoning; } @@ -248,7 +249,7 @@ { Message = newMsg }); - Chat.Model = Utils.Model!; + Chat.ModelId = Utils.Model!; _isLoading = true; Chat.Visual = Utils.Visual; _inputKey++; diff --git a/src/MaIN.Services/Dtos/ChatDto.cs b/src/MaIN.Services/Dtos/ChatDto.cs index 6c991cd..e2bee19 100644 --- a/src/MaIN.Services/Dtos/ChatDto.cs +++ b/src/MaIN.Services/Dtos/ChatDto.cs @@ -2,7 +2,7 @@ namespace MaIN.Services.Dtos; -public class ChatDto +public record ChatDto { [JsonPropertyName("id")] public string? Id { get; set; } diff --git a/src/MaIN.Services/Mappers/ChatMapper.cs b/src/MaIN.Services/Mappers/ChatMapper.cs index 4ccff91..ff9775e 100644 --- a/src/MaIN.Services/Mappers/ChatMapper.cs +++ b/src/MaIN.Services/Mappers/ChatMapper.cs @@ -15,7 +15,7 @@ public static ChatDto ToDto(this Chat chat) { Id = chat.Id, Name = chat.Name, - Model = chat.Model, + Model = chat.ModelId, Messages = chat.Messages.Select(m => m.ToDto()).ToList(), Visual = chat.Visual, Type = Enum.Parse(chat.Type.ToString()), @@ -44,7 +44,7 @@ public static Chat ToDomain(this ChatDto chat) { Id = chat.Id!, Name = chat.Name!, - Model = chat.Model!, + ModelId = chat.Model!, Messages = chat.Messages?.Select(m => m.ToDomain()).ToList()!, Visual = chat.Model == ImageGenService.LocalImageModels.FLUX, Type = Enum.Parse(chat.Type.ToString()), @@ -87,7 +87,7 @@ public static ChatDocument ToDocument(this Chat chat) { Id = chat.Id, Name = chat.Name, - Model = chat.Model, + Model = chat.ModelId, Messages = chat.Messages.Select(m => m.ToDocument()).ToList(), Visual = chat.Visual, Backend = chat.Backend, @@ -106,7 +106,7 @@ public static Chat ToDomain(this ChatDocument chat) { Id = chat.Id, Name = chat.Name, - Model = chat.Model, + ModelId = chat.Model, Messages = chat.Messages.Select(m => m.ToDomain()).ToList(), Visual = chat.Visual, Backend = chat.Backend, diff --git a/src/MaIN.Services/Services/AgentService.cs b/src/MaIN.Services/Services/AgentService.cs index c43c1db..5e93283 100644 --- a/src/MaIN.Services/Services/AgentService.cs +++ b/src/MaIN.Services/Services/AgentService.cs @@ -99,7 +99,7 @@ public async Task CreateAgent(Agent agent, bool flow = false, bool intera var chat = new Chat { Id = Guid.NewGuid().ToString(), - Model = agent.Model, + ModelId = agent.Model, Name = agent.Name, Visual = agent.Model == ImageGenService.LocalImageModels.FLUX, ToolsConfiguration = agent.ToolsConfiguration, diff --git a/src/MaIN.Services/Services/ChatService.cs b/src/MaIN.Services/Services/ChatService.cs index 193d880..0792911 100644 --- a/src/MaIN.Services/Services/ChatService.cs +++ b/src/MaIN.Services/Services/ChatService.cs @@ -26,68 +26,68 @@ public async Task Create(Chat chat) await chatProvider.AddChat(chat.ToDocument()); } - public async Task Completions( - Chat chat, - bool translate = false, - bool interactiveUpdates = false, - Func? changeOfValue = null) -{ - if (chat.Model == ImageGenService.LocalImageModels.FLUX) + public async Task Completions( + Chat chat, + bool translate = false, + bool interactiveUpdates = false, + Func? changeOfValue = null) { - chat.Visual = true; - } - chat.Backend ??= settings.BackendType; - - chat.Messages.Where(x => x.Type == MessageType.NotSet).ToList() - .ForEach(x => x.Type = chat.Backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM); - - translate = translate || chat.Translate; - interactiveUpdates = interactiveUpdates || chat.Interactive; - var newMsg = chat.Messages.Last(); - newMsg.Time = DateTime.Now; + if (chat.ModelId == ImageGenService.LocalImageModels.FLUX) + { + chat.Visual = true; // TODO: add IImageGenModel interface and check for that instead + } + chat.Backend ??= settings.BackendType; + + chat.Messages.Where(x => x.Type == MessageType.NotSet).ToList() + .ForEach(x => x.Type = chat.Backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM); - var lng = translate ? await translatorService.DetectLanguage(newMsg.Content) : null; - var originalMessages = chat.Messages; + translate = translate || chat.Translate; + interactiveUpdates = interactiveUpdates || chat.Interactive; + var newMsg = chat.Messages.Last(); + newMsg.Time = DateTime.Now; + + var lng = translate ? await translatorService.DetectLanguage(newMsg.Content) : null; + var originalMessages = chat.Messages; - if (translate) - { - chat.Messages = (await Task.WhenAll(chat.Messages.Select(async m => new Message() + if (translate) { - Role = m.Role, - Content = await translatorService.Translate(m.Content, "en"), - Type = m.Type - }))).ToList(); - } + chat.Messages = [.. await Task.WhenAll(chat.Messages.Select(async m => new Message() + { + Role = m.Role, + Content = await translatorService.Translate(m.Content, "en"), + Type = m.Type + }))]; + } - var result = chat.Visual - ? await imageGenServiceFactory.CreateService(chat.Backend.Value)!.Send(chat) - : await llmServiceFactory.CreateService(chat.Backend.Value).Send(chat, new ChatRequestOptions() - { - InteractiveUpdates = interactiveUpdates, - TokenCallback = changeOfValue - }); + var result = chat.Visual + ? await imageGenServiceFactory.CreateService(chat.Backend.Value)!.Send(chat) + : await llmServiceFactory.CreateService(chat.Backend.Value).Send(chat, new ChatRequestOptions() + { + InteractiveUpdates = interactiveUpdates, + TokenCallback = changeOfValue + }); - if (translate) - { - result!.Message.Content = await translatorService.Translate(result.Message.Content, lng!); - result.Message.Time = DateTime.Now; - } + if (translate) + { + result!.Message.Content = await translatorService.Translate(result.Message.Content, lng!); + result.Message.Time = DateTime.Now; + } - if (!chat.Visual && chat.TextToSpeechParams != null) - { - var speechBytes = await ttsServiceFactory - .CreateService(chat.Backend.Value).Send(result!.Message, chat.TextToSpeechParams.Model, - chat.TextToSpeechParams.Voice, chat.TextToSpeechParams.Playback); + if (!chat.Visual && chat.TextToSpeechParams != null) + { + var speechBytes = await ttsServiceFactory + .CreateService(chat.Backend.Value).Send(result!.Message, chat.TextToSpeechParams.Model, + chat.TextToSpeechParams.Voice, chat.TextToSpeechParams.Playback); - result.Message.Speech = speechBytes; - } + result.Message.Speech = speechBytes; + } - originalMessages.Add(result!.Message); - chat.Messages = originalMessages; + originalMessages.Add(result!.Message); + chat.Messages = originalMessages; - await chatProvider.UpdateChat(chat.Id!, chat.ToDocument()); - return result; -} + await chatProvider.UpdateChat(chat.Id!, chat.ToDocument()); + return result; + } public async Task Delete(string id) { @@ -100,15 +100,9 @@ public async Task Delete(string id) public async Task GetById(string id) { var chatDocument = await chatProvider.GetChatById(id); - if (chatDocument == null) - { - throw new ChatNotFoundException(id); - } - - return chatDocument.ToDomain(); + return chatDocument is null ? throw new ChatNotFoundException(id) : chatDocument.ToDomain(); } public async Task> GetAll() - => (await chatProvider.GetAllChats()) - .Select(x => x.ToDomain()).ToList(); + => [.. (await chatProvider.GetAllChats()).Select(x => x.ToDomain())]; } \ No newline at end of file diff --git a/src/MaIN.Services/Services/ImageGenServices/GeminiImageGenService.cs b/src/MaIN.Services/Services/ImageGenServices/GeminiImageGenService.cs index 8faffa0..dd223e6 100644 --- a/src/MaIN.Services/Services/ImageGenServices/GeminiImageGenService.cs +++ b/src/MaIN.Services/Services/ImageGenServices/GeminiImageGenService.cs @@ -22,14 +22,14 @@ internal class GeminiImageGenService(IHttpClientFactory httpClientFactory, MaINS string apiKey = _settings.GeminiKey ?? Environment.GetEnvironmentVariable(LLMApiRegistry.Gemini.ApiKeyEnvName) ?? throw new APIKeyNotConfiguredException(LLMApiRegistry.Gemini.ApiName); - if (string.IsNullOrEmpty(chat.Model)) + if (string.IsNullOrEmpty(chat.ModelId)) { - chat.Model = Models.IMAGEN_GENERATE; + chat.ModelId = Models.IMAGEN_GENERATE; } client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); var requestBody = new { - model = chat.Model, + model = chat.ModelId, prompt = BuildPromptFromChat(chat), response_format = "b64_json", // necessary for gemini api size = ServiceConstants.Defaults.ImageSize, diff --git a/src/MaIN.Services/Services/ImageGenServices/ImageGenDalleService.cs b/src/MaIN.Services/Services/ImageGenServices/ImageGenDalleService.cs index 7a0e254..066bdaf 100644 --- a/src/MaIN.Services/Services/ImageGenServices/ImageGenDalleService.cs +++ b/src/MaIN.Services/Services/ImageGenServices/ImageGenDalleService.cs @@ -27,7 +27,7 @@ public class OpenAiImageGenService( client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); var requestBody = new { - model = chat.Model, + model = chat.ModelId, prompt = BuildPromptFromChat(chat), size = ServiceConstants.Defaults.ImageSize }; diff --git a/src/MaIN.Services/Services/ImageGenServices/XaiImageGenService.cs b/src/MaIN.Services/Services/ImageGenServices/XaiImageGenService.cs index ac3ce8c..3b931b4 100644 --- a/src/MaIN.Services/Services/ImageGenServices/XaiImageGenService.cs +++ b/src/MaIN.Services/Services/ImageGenServices/XaiImageGenService.cs @@ -28,7 +28,7 @@ public class XaiImageGenService( client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); var requestBody = new { - model = string.IsNullOrWhiteSpace(chat.Model) ? Models.GROK_IMAGE : chat.Model, + model = string.IsNullOrWhiteSpace(chat.ModelId) ? Models.GROK_IMAGE : chat.ModelId, prompt = BuildPromptFromChat(chat), n = 1, response_format = "b64_json" //or "url" diff --git a/src/MaIN.Services/Services/LLMService/AnthropicService.cs b/src/MaIN.Services/Services/LLMService/AnthropicService.cs index 9685cd5..0372456 100644 --- a/src/MaIN.Services/Services/LLMService/AnthropicService.cs +++ b/src/MaIN.Services/Services/LLMService/AnthropicService.cs @@ -504,7 +504,7 @@ private object BuildAnthropicRequestBody(Chat chat, List conversati { var requestBody = new Dictionary { - ["model"] = chat.Model, + ["model"] = chat.ModelId, ["max_tokens"] = chat.InterferenceParams.MaxTokens < 0 ? 4096 : chat.InterferenceParams.MaxTokens, ["stream"] = stream, ["messages"] = BuildAnthropicMessages(conversation) @@ -645,7 +645,7 @@ private async Task ProcessStreamingChatAsync( var requestBody = new { - model = chat.Model, + model = chat.ModelId, max_tokens = chat.InterferenceParams.MaxTokens < 0 ? 4096 : chat.InterferenceParams.MaxTokens, stream = true, system = chat.InterferenceParams.Grammar is not null @@ -733,7 +733,7 @@ private async Task ProcessNonStreamingChatAsync( var requestBody = new { - model = chat.Model, + model = chat.ModelId, max_tokens = chat.InterferenceParams.MaxTokens < 0 ? 4096 : chat.InterferenceParams.MaxTokens, stream = false, system = chat.InterferenceParams.Grammar is not null @@ -770,7 +770,7 @@ private static ChatResult CreateChatResult(Chat chat, string content, List GetCurrentModels() { var models = Directory.GetFiles(modelsPath, "*.gguf", SearchOption.AllDirectories) .Select(Path.GetFileName) - .Where(fileName => KnownModels.GetModelByFileName(modelsPath, fileName!) != null) - .Select(fileName => KnownModels.GetModelByFileName(modelsPath, fileName!)!.Name) + .Where(fileName => ModelRegistry.GetByFileName(fileName!) != null) + .Select(fileName => ModelRegistry.GetByFileName(fileName!)!.Name) .ToArray(); return Task.FromResult(models); @@ -100,15 +101,15 @@ public Task CleanSessionCache(string? id) return Task.CompletedTask; } + public async Task AskMemory( Chat chat, ChatMemoryOptions memoryOptions, ChatRequestOptions requestOptions, CancellationToken cancellationToken = default) { - var model = KnownModels.GetModel(chat.Model); - var finalModelPath = model.Path ?? modelsPath; - var parameters = new ModelParams(Path.Combine(finalModelPath, model.FileName)) + var model = GetLocalModel(chat.ModelId); + var parameters = new ModelParams(Path.Combine(modelsPath, model.FileName)) { GpuLayerCount = chat.MemoryParams.GpuLayerCount, ContextSize = (uint)chat.MemoryParams.ContextSize, @@ -116,15 +117,15 @@ public Task CleanSessionCache(string? id) var disableCache = chat.Properties.CheckProperty(ServiceConstants.Properties.DisableCacheProperty); var llmModel = disableCache ? await LLamaWeights.LoadFromFileAsync(parameters, cancellationToken) - : await ModelLoader.GetOrLoadModelAsync(finalModelPath, model.FileName); + : await ModelLoader.GetOrLoadModelAsync(modelsPath, model.FileName); - var memory = memoryFactory.CreateMemoryWithModel( - finalModelPath, + var (km, generator, textGenerator) = memoryFactory.CreateMemoryWithModel( + modelsPath, llmModel, model.FileName, chat.MemoryParams); - await memoryService.ImportDataToMemory((memory.km, memory.generator), memoryOptions, cancellationToken); + await memoryService.ImportDataToMemory((km, generator), memoryOptions, cancellationToken); var userMessage = chat.Messages.Last(); MemoryAnswer result; @@ -138,7 +139,7 @@ public Task CleanSessionCache(string? id) Stream = true }; - await foreach (var chunk in memory.km.AskStreamingAsync( + await foreach (var chunk in km.AskStreamingAsync( userMessage.Content, options: searchOptions, cancellationToken: cancellationToken)) @@ -178,29 +179,29 @@ await notificationService.DispatchNotification( Stream = false }; - result = await memory.km.AskAsync( + result = await km.AskAsync( userMessage.Content, options: searchOptions, cancellationToken: cancellationToken); } - await memory.km.DeleteIndexAsync(cancellationToken: cancellationToken); + await km.DeleteIndexAsync(cancellationToken: cancellationToken); if (disableCache) { llmModel.Dispose(); ModelLoader.RemoveModel(model.FileName); - memory.textGenerator.Dispose(); + textGenerator.Dispose(); } - memory.generator._embedder.Dispose(); - memory.generator._embedder._weights.Dispose(); - memory.generator.Dispose(); + generator._embedder.Dispose(); + generator._embedder._weights.Dispose(); + generator.Dispose(); return new ChatResult { Done = true, CreatedAt = DateTime.Now, - Model = chat.Model, + Model = chat.ModelId, Message = new Message { Content = memoryService.CleanResponseText(result.Result), @@ -212,7 +213,7 @@ await notificationService.DispatchNotification( private async Task> ProcessChatRequest( Chat chat, - Model model, + LocalModel model, Message lastMsg, ChatRequestOptions requestOptions, CancellationToken cancellationToken) @@ -221,20 +222,20 @@ private async Task> ProcessChatRequest( var thinkingState = new ThinkingState(); var tokens = new List(); - var parameters = CreateModelParameters(chat, modelKey, model.Path); + var parameters = CreateModelParameters(chat, modelKey, null); var disableCache = chat.Properties.CheckProperty(ServiceConstants.Properties.DisableCacheProperty); var llmModel = disableCache ? await LLamaWeights.LoadFromFileAsync(parameters, cancellationToken) - : await ModelLoader.GetOrLoadModelAsync( - model.Path ?? modelsPath, modelKey); + : await ModelLoader.GetOrLoadModelAsync(modelsPath, modelKey); - var llavaWeights = model.MMProject != null - ? await LLavaWeights.LoadFromFileAsync(model.MMProject, cancellationToken) + var visionModel = model as IVisionModel; + var llavaWeights = visionModel?.MMProjectPath is not null + ? await LLavaWeights.LoadFromFileAsync(Path.Combine(modelsPath, visionModel.MMProjectPath), cancellationToken) : null; using var executor = new BatchedExecutor(llmModel, parameters); - var (conversation, isComplete, hasFailed) = await InitializeConversation( + var (conversation, isComplete, hasFailed) = await LLMService.InitializeConversation( chat, lastMsg, model, llmModel, llavaWeights, executor, cancellationToken); if (!isComplete) @@ -278,9 +279,10 @@ private ModelParams CreateModelParameters(Chat chat, string modelKey, string? cu }; } - private async Task<(Conversation Conversation, bool IsComplete, bool HasFailed)> InitializeConversation(Chat chat, + + private static async Task<(Conversation Conversation, bool IsComplete, bool HasFailed)> InitializeConversation(Chat chat, Message lastMsg, - Model model, + LocalModel model, LLamaWeights llmModel, LLavaWeights? llavaWeights, BatchedExecutor executor, @@ -325,7 +327,7 @@ private static async Task ProcessImageMessage(Conversation conversation, private static void ProcessTextMessage(Conversation conversation, Chat chat, Message lastMsg, - Model model, + LocalModel model, LLamaWeights llmModel, BatchedExecutor executor, bool isNewConversation) @@ -333,7 +335,7 @@ private static void ProcessTextMessage(Conversation conversation, var template = new LLamaTemplate(llmModel); var finalPrompt = ChatHelper.GetFinalPrompt(lastMsg, model, isNewConversation); - var hasTools = chat.ToolsConfiguration?.Tools != null && chat.ToolsConfiguration.Tools.Any(); + var hasTools = chat.ToolsConfiguration?.Tools != null && chat.ToolsConfiguration.Tools.Count != 0; if (isNewConversation) { @@ -396,7 +398,7 @@ private static string FormatToolsForPrompt(ToolsConfiguration toolsConfig) private async Task<(List Tokens, bool IsComplete, bool HasFailed)> ProcessTokens( Chat chat, Conversation conversation, - Model model, + LocalModel model, LLamaWeights llmModel, BatchedExecutor executor, ThinkingState thinkingState, @@ -407,11 +409,12 @@ private static string FormatToolsForPrompt(ToolsConfiguration toolsConfig) var isComplete = false; var hasFailed = false; - using var sampler = CreateSampler(chat.InterferenceParams); + using var sampler = LLMService.CreateSampler(chat.InterferenceParams); var decoder = new StreamingTokenDecoder(executor.Context); var inferenceParams = ChatHelper.CreateInferenceParams(chat, llmModel); var maxTokens = inferenceParams.MaxTokens == -1 ? int.MaxValue : inferenceParams.MaxTokens; + var reasoningModel = model as IReasoningModel; for (var i = 0; i < maxTokens && !isComplete; i++) { @@ -426,10 +429,14 @@ private static string FormatToolsForPrompt(ToolsConfiguration toolsConfig) } if (decodeResult == DecodeResult.DecodeFailed) + { throw new Exception("Unknown error occurred while inferring."); + } if (!conversation.RequiresSampling) + { continue; + } var token = conversation.Sample(sampler); var vocab = executor.Context.NativeHandle.ModelHandle.Vocab; @@ -444,8 +451,8 @@ private static string FormatToolsForPrompt(ToolsConfiguration toolsConfig) var tokenTxt = decoder.Read(); conversation.Prompt(token); - var tokenValue = model.ReasonFunction != null - ? model.ReasonFunction(tokenTxt, thinkingState) + var tokenValue = reasoningModel?.ReasonFunction != null + ? reasoningModel.ReasonFunction(tokenTxt, thinkingState) : new LLMTokenValue() { Text = tokenTxt, @@ -463,11 +470,12 @@ private static string FormatToolsForPrompt(ToolsConfiguration toolsConfig) } } + return (tokens, isComplete, hasFailed); } - private BaseSamplingPipeline CreateSampler(InferenceParams interferenceParams) + private static BaseSamplingPipeline CreateSampler(InferenceParams interferenceParams) { if (interferenceParams.Temperature == 0) { @@ -486,6 +494,16 @@ private BaseSamplingPipeline CreateSampler(InferenceParams interferenceParams) }; } + private static LocalModel GetLocalModel(string modelId) + { + var model = ModelRegistry.GetById(modelId); + if (model is LocalModel localModel) + { + return localModel; + } + + throw new InvalidOperationException($"Model '{modelId}' is not a local model. LLMService only supports local models."); + } private string GetModelsPath() { @@ -513,7 +531,7 @@ private async Task CreateChatResult(Chat chat, List t { Done = true, CreatedAt = DateTime.Now, - Model = chat.Model, + Model = chat.ModelId, Message = new Message { Content = responseText, @@ -536,7 +554,12 @@ private async Task ProcessWithToolsAsync( ChatRequestOptions requestOptions, CancellationToken cancellationToken) { - var model = KnownModels.GetModel(chat.Model); + var model = chat.ModelInstance ?? throw new MissingModelInstanceException(); + if (model is not LocalModel localModel) + { + throw new InvalidModelTypeException(nameof(LocalModel)); + } + var iterations = 0; var lastResponseTokens = new List(); var lastResponse = string.Empty; @@ -547,7 +570,7 @@ private async Task ProcessWithToolsAsync( var tokenCallbackOrg = requestOptions.TokenCallback; requestOptions.InteractiveUpdates = false; requestOptions.TokenCallback = null; - lastResponseTokens = await ProcessChatRequest(chat, model, lastMsg, requestOptions, cancellationToken); + lastResponseTokens = await ProcessChatRequest(chat, localModel, lastMsg, requestOptions, cancellationToken); lastMsg.MarkProcessed(); lastResponse = string.Concat(lastResponseTokens.Select(x => x.Text)); var responseMessage = new Message @@ -690,7 +713,7 @@ await requestOptions.ToolCallback.Invoke(new ToolInvocation { Done = true, CreatedAt = DateTime.Now, - Model = chat.Model, + Model = chat.ModelId, Message = chat.Messages.Last() }; } diff --git a/src/MaIN.Services/Services/LLMService/OpenAiCompatibleService.cs b/src/MaIN.Services/Services/LLMService/OpenAiCompatibleService.cs index b0ae2d1..740e498 100644 --- a/src/MaIN.Services/Services/LLMService/OpenAiCompatibleService.cs +++ b/src/MaIN.Services/Services/LLMService/OpenAiCompatibleService.cs @@ -760,7 +760,7 @@ private object BuildRequestBody(Chat chat, List conversation, bool { var requestBody = new Dictionary { - ["model"] = chat.Model, + ["model"] = chat.ModelId, ["messages"] = BuildMessagesArray(conversation, chat, ImageType.AsUrl).Result, ["stream"] = stream }; @@ -841,7 +841,7 @@ protected static ChatResult CreateChatResult(Chat chat, string content, List /// Generates final prompt including additional prompt if needed /// - public static string GetFinalPrompt(Message message, Model model, bool startSession) + public static string GetFinalPrompt(Message message, AIModel model, bool startSession) { - return startSession && model.AdditionalPrompt != null - ? $"{message.Content}{model.AdditionalPrompt}" + var additionalPrompt = (model as IReasoningModel)?.AdditionalPrompt; + return startSession && additionalPrompt != null + ? $"{message.Content}{additionalPrompt}" : message.Content; } diff --git a/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs index 6913a8b..036941f 100644 --- a/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs @@ -131,7 +131,7 @@ Find tags that fits user query based on available knowledge (provided to you abo await notificationService.DispatchNotification(NotificationMessageBuilder.CreateActorKnowledgeStepProgress( agentId, knowledgeItems.Select(x => $" {x.Name}|{x.Type} ").ToList(), - mcpConfig?.Model ?? chat.Model), "ReceiveAgentUpdate"); + mcpConfig?.Model ?? chat.ModelId), "ReceiveAgentUpdate"); if (mcpConfig != null) { diff --git a/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs b/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs index b82e48c..28af385 100644 --- a/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs +++ b/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs @@ -65,7 +65,7 @@ private static Chat CreateMemoryChat(StepContext context, string? filterVal) Role = "User" } }, - Model = context.Chat.Model, + ModelId = context.Chat.ModelId, Properties = context.Chat.Properties, MemoryParams = context.Chat.MemoryParams, InterferenceParams = context.Chat.InterferenceParams, diff --git a/src/MaIN.Services/Utils/AgentStateManager.cs b/src/MaIN.Services/Utils/AgentStateManager.cs index d3fb461..f863daa 100644 --- a/src/MaIN.Services/Utils/AgentStateManager.cs +++ b/src/MaIN.Services/Utils/AgentStateManager.cs @@ -11,7 +11,7 @@ public static void ClearState(AgentDocument agent, Chat chat) agent.CurrentBehaviour = "Default"; chat.Properties.Clear(); - if (chat.Model == ImageGenService.LocalImageModels.FLUX) + if (chat.ModelId == ImageGenService.LocalImageModels.FLUX) { chat.Messages = []; }