From 13aa0633649a1ce16a9edfcc32bf670f93adda57 Mon Sep 17 00:00:00 2001 From: srebrek Date: Wed, 4 Mar 2026 15:23:43 +0100 Subject: [PATCH 1/9] Separate AIModel and Chat - remove Backend field from Chat as it is contained in the AIModel --- src/MaIN.Core.UnitTests/ChatContextTests.cs | 66 ++++++---- src/MaIN.Core/Hub/Contexts/ChatContext.cs | 80 ++++++------ src/MaIN.Core/Hub/Contexts/FlowContext.cs | 86 ++++++------- .../ChatContext/IChatConfigurationBuilder.cs | 28 ++--- src/MaIN.Domain/Entities/Chat.cs | 32 +---- .../Models/ChatDocument.cs | 4 +- .../Repositories/Sql/SqlChatRepository.cs | 92 ++++++-------- .../Sqlite/SqliteChatRepository.cs | 52 ++++---- src/MaIN.Services/Mappers/ChatMapper.cs | 36 +++--- src/MaIN.Services/Services/AgentService.cs | 75 +++++------ src/MaIN.Services/Services/ChatService.cs | 41 +++--- .../Services/LLMService/LLMService.cs | 117 ++++++++++-------- .../Services/Steps/BecomeStepHandler.cs | 19 +-- .../Steps/Commands/AnswerCommandHandler.cs | 58 +++++---- .../Steps/Commands/FetchCommandHandler.cs | 74 ++++++----- .../Steps/Commands/RedirectCommandHandler.cs | 8 +- .../Steps/Commands/StartCommandHandler.cs | 8 +- .../Services/Steps/FechDataStepHandler.cs | 35 +++--- 18 files changed, 450 insertions(+), 461 deletions(-) diff --git a/src/MaIN.Core.UnitTests/ChatContextTests.cs b/src/MaIN.Core.UnitTests/ChatContextTests.cs index 261c6982..2e38a60b 100644 --- a/src/MaIN.Core.UnitTests/ChatContextTests.cs +++ b/src/MaIN.Core.UnitTests/ChatContextTests.cs @@ -27,7 +27,7 @@ public void Constructor_ShouldInitializeNewChat() { // Act var chatId = _chatContext.GetChatId(); - + // Assert Assert.NotNull(chatId); Assert.NotEmpty(chatId); @@ -39,7 +39,7 @@ public void WithMessage_ShouldAddMessage() // Act _chatContext.WithMessage("Hello, world!"); var messages = _chatContext.GetChatHistory(); - + // Assert Assert.Single(messages); Assert.Equal("Hello, world!", messages[0].Content); @@ -53,7 +53,7 @@ public void WithSystemPrompt_ShouldInsertSystemMessageAtBeginning() _chatContext.WithMessage("User message"); _chatContext.WithSystemPrompt("System prompt"); var messages = _chatContext.GetChatHistory(); - + // Assert Assert.Equal(2, messages.Count); Assert.Equal("System", messages[0].Role); @@ -65,11 +65,14 @@ public void WithFiles_ShouldAttachFilesToLastMessage() { // Arrange _chatContext.WithMessage("User message"); - var files = new List { new() { Name = "file.txt", Path = "/path/file.txt", Extension = "txt"} }; - + var files = new List + { + new() { Name = "file.txt", Path = "/path/file.txt", Extension = "txt" } + }; + // Act _chatContext.WithFiles(files); - + // Assert var lastMessage = _chatContext.GetChatHistory().Last(); Assert.NotNull(lastMessage); @@ -79,26 +82,41 @@ public void WithFiles_ShouldAttachFilesToLastMessage() public async Task CompleteAsync_ShouldCallChatService() { // Arrange - var chatResult = new ChatResult(){ Model = "test-model", Message = new Message + var chatResult = new ChatResult() + { + Model = "test-model", + Message = new Message { Role = "Assistant", Content = "test-message", Type = MessageType.LocalLLM } }; - - _mockChatService.Setup(s => s.Completions(It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())) + _mockChatService + .Setup(s => s.Completions( + It.IsAny(), + It.IsAny(), + It.IsAny(), + null, + It.IsAny())) .ReturnsAsync(chatResult); - + _chatContext.WithMessage("User message"); - _chatContext.WithModel(new GenericLocalModel("test-model")); + _chatContext.WithModel(_testModelId); // Act var result = await _chatContext.CompleteAsync(); - + // Assert - _mockChatService.Verify(s => s.Completions(It.IsAny(), false, false, null, It.IsAny()), Times.Once); + _mockChatService.Verify( + s => s.Completions( + It.IsAny(), + false, + false, + null, + It.IsAny()), + Times.Once); Assert.Equal(chatResult, result); } @@ -106,12 +124,12 @@ public async Task CompleteAsync_ShouldCallChatService() public async Task GetCurrentChat_ShouldCallChatService() { // Arrange - var chat = new Chat { Id = _chatContext.GetChatId(), ModelId = _testModelId , Name = "test"}; + var chat = new Chat { Id = _chatContext.GetChatId(), ModelId = _testModelId, Name = "test" }; _mockChatService.Setup(s => s.GetById(chat.Id)).ReturnsAsync(chat); - + // Act var result = await _chatContext.GetCurrentChat(); - + // Assert Assert.Equal(chat, result); } @@ -119,15 +137,19 @@ public async Task GetCurrentChat_ShouldCallChatService() [Fact] public async Task WithModel_ShouldSetModelIdAndInstance() { - // Arrange - var model = new GenericLocalModel(_testModelId); - // Act - await _chatContext.WithModel(model) + await _chatContext.WithModel(_testModelId) .WithMessage("User message") .CompleteAsync(); - + // Assert - _mockChatService.Verify(s => s.Completions(It.Is(c => c.ModelId == _testModelId && c.ModelInstance == model), It.IsAny(), It.IsAny(), null, It.IsAny()), Times.Once); + _mockChatService.Verify(s => + s.Completions( + It.Is(c => c.ModelId == _testModelId), + It.IsAny(), + It.IsAny(), + null, + It.IsAny()), + Times.Once); } } diff --git a/src/MaIN.Core/Hub/Contexts/ChatContext.cs b/src/MaIN.Core/Hub/Contexts/ChatContext.cs index 3cb0073b..6edba5eb 100644 --- a/src/MaIN.Core/Hub/Contexts/ChatContext.cs +++ b/src/MaIN.Core/Hub/Contexts/ChatContext.cs @@ -1,5 +1,4 @@ using MaIN.Core.Hub.Contexts.Interfaces.ChatContext; -using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Entities.Tools; using MaIN.Domain.Exceptions.Chats; @@ -42,35 +41,34 @@ internal ChatContext(IChatService chatService, Chat existingChat) public IChatMessageBuilder WithModel(AIModel model, bool? imageGen = null) { - SetModel(model); - _chat.ImageGen = imageGen ?? model is IImageGenerationModel; + ModelRegistry.RegisterOrReplace(model); + _chat.ModelId = model.Id; + _chat.ImageGen = imageGen ?? (model is IImageGenerationModel); + _chat.ImageGen = model.HasImageGeneration; return this; } + [Obsolete("Use WithModel(string modelId) or WithModel(AIModel model) instead.")] public IChatMessageBuilder WithModel() where TModel : AIModel, new() { var model = new TModel(); - return WithModel(model); + ModelRegistry.RegisterOrReplace(model); + _chat.ModelId = model.Id; + return this; } - [Obsolete("Use WithModel(AIModel model) or WithModel() instead.")] public IChatMessageBuilder WithModel(string modelId) { - AIModel model; - try + if (!ModelRegistry.Exists(modelId)) { - model = ModelRegistry.GetById(modelId); + throw new ModelNotRegisteredException(modelId); } - catch (Exception ex) - { - throw new Exception($"Model with ID '{modelId}' not found in registry. Use WithModel(AIModel model) or WithModel() instead.", ex); - } - SetModel(model); + _chat.ModelId = modelId; return this; } - [Obsolete("Use WithModel() instead.")] + [Obsolete("Use WithModel(AIModel model) instead.")] public IChatMessageBuilder WithCustomModel(string model, string path, string? mmProject = null) { KnownModels.AddModel(model, path, mmProject); @@ -78,14 +76,6 @@ public IChatMessageBuilder WithCustomModel(string model, string path, string? mm return this; } - private void SetModel(AIModel model) - { - _chat.ModelId = model.Id; - _chat.ModelInstance = model; - _chat.Backend = model.Backend; - _chat.ImageGen = model.HasImageGeneration; - } - public IChatMessageBuilder EnsureModelDownloaded() { _ensureModelDownloaded = true; @@ -117,12 +107,6 @@ public IChatConfigurationBuilder Speak(TextToSpeechParams speechParams) return this; } - public IChatConfigurationBuilder WithBackend(BackendType backendType) - { - _chat.Backend = backendType; - return this; - } - public IChatConfigurationBuilder WithSystemPrompt(string systemPrompt) { var message = new Message @@ -139,7 +123,15 @@ public IChatConfigurationBuilder WithSystemPrompt(string systemPrompt) public IChatConfigurationBuilder WithMessage(string content) { - _chat.Messages.Add(new Message { Role = "User", Content = content, Type = MessageType.LocalLLM, Time = DateTime.Now }); + var message = new Message + { + Role = "User", + Content = content, + Type = MessageType.NotSet, + Time = DateTime.Now + }; + + _chat.Messages.Add(message); return this; } @@ -204,10 +196,11 @@ public async Task CompleteAsync( Func? changeOfValue = null, CancellationToken cancellationToken = default) { - if (_chat.ModelInstance is null) + if (string.IsNullOrEmpty(_chat.ModelId)) { - throw new MissingModelInstanceException(); + throw new MissingModelIdException(nameof(_chat.ModelId)); } + if (_chat.Messages.Count == 0) { throw new EmptyChatException(_chat.Id); @@ -215,7 +208,7 @@ public async Task CompleteAsync( if (_ensureModelDownloaded) { - await AIHub.Model().EnsureDownloadedAsync(_chat.ModelId); + await AIHub.Model().EnsureDownloadedAsync(_chat.ModelId, cancellationToken); } _chat.Messages.Last().Files = _files; @@ -228,7 +221,13 @@ public async Task CompleteAsync( { await _chatService.Create(_chat); } - var result = await _chatService.Completions(_chat, translate, interactive, changeOfValue, cancellationToken); + + var result = await _chatService.Completions( + _chat, + translate, + interactive, + changeOfValue, + cancellationToken); _files = []; return result; } @@ -236,7 +235,7 @@ public async Task CompleteAsync( public async Task FromExisting(string chatId) { var existing = await _chatService.GetById(chatId); - return existing == null + return existing is null ? throw new ChatNotFoundException(chatId) : new ChatContext(_chatService, existing); } @@ -258,19 +257,16 @@ private async Task ChatExists(string id) public async Task GetCurrentChat() { - if (_chat.Id == null) - { - throw new ChatNotInitializedException(); - } - - return await _chatService.GetById(_chat.Id); + return _chat.Id is null + ? throw new ChatNotInitializedException() + : await _chatService.GetById(_chat.Id); } public async Task> GetAllChats() => await _chatService.GetAll(); public async Task DeleteChat() { - if (_chat.Id == null) + if (_chat.Id is null) { throw new ChatNotInitializedException(); } @@ -287,4 +283,4 @@ public List GetChatHistory() Time = x.Time })]; } -} \ 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 39d6cb70..0ba28846 100644 --- a/src/MaIN.Core/Hub/Contexts/FlowContext.cs +++ b/src/MaIN.Core/Hub/Contexts/FlowContext.cs @@ -1,13 +1,14 @@ -using System.IO.Compression; -using System.Text.Json; using MaIN.Core.Hub.Contexts.Interfaces.FlowContext; using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Entities.Agents; using MaIN.Domain.Entities.Agents.AgentSource; using MaIN.Domain.Exceptions.Flows; +using MaIN.Domain.Models.Abstract; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; +using System.IO.Compression; +using System.Text.Json; namespace MaIN.Core.Hub.Contexts; @@ -26,7 +27,7 @@ internal FlowContext(IAgentFlowService flowService, IAgentService agentService) { Id = Guid.NewGuid().ToString(), Name = string.Empty, - Agents = new List(), + Agents = [], }; } @@ -48,13 +49,13 @@ public IFlowContext WithName(string name) _flow.Name = name; return this; } - + public IFlowContext WithDescription(string description) { _flow.Description = description; return this; } - + public IFlowContext Save(string path) { Directory.CreateDirectory(Path.GetDirectoryName(path)!); @@ -73,12 +74,10 @@ public IFlowContext Save(string path) { var agentFileName = $"{agent.Id}.json"; var agentEntry = archive.CreateEntry(agentFileName); - using (var entryStream = agentEntry.Open()) - using (var writer = new StreamWriter(entryStream)) - { - var json = JsonSerializer.Serialize(agent); - writer.Write(json); - } + using var entryStream = agentEntry.Open(); + using var writer = new StreamWriter(entryStream); + var json = JsonSerializer.Serialize(agent); + writer.Write(json); } } @@ -97,26 +96,22 @@ public IFlowContext Load(string path) var descriptionEntry = archive.GetEntry("description.txt"); if (descriptionEntry != null) { - using (var entryStream = descriptionEntry.Open()) - using (var reader = new StreamReader(entryStream)) - { - description = reader.ReadToEnd(); - } + using var entryStream = descriptionEntry.Open(); + using var reader = new StreamReader(entryStream); + description = reader.ReadToEnd(); } foreach (var entry in archive.Entries) { if (entry.FullName.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) { - using (var entryStream = entry.Open()) - using (var reader = new StreamReader(entryStream)) + using var entryStream = entry.Open(); + using var reader = new StreamReader(entryStream); + var json = reader.ReadToEnd(); + var agent = JsonSerializer.Deserialize(json); + if (agent != null) { - var json = reader.ReadToEnd(); - var agent = JsonSerializer.Deserialize(json); - if (agent != null) - { - agents.Add(agent); - } + agents.Add(agent); } } } @@ -139,8 +134,7 @@ public IFlowContext AddAgent(Agent agent) _flow.Agents.Add(agent); return this; } - - + public async Task ProcessAsync(Chat chat, bool translate = false) { //TODO add knowledge support also to flows @@ -154,15 +148,16 @@ public async Task ProcessAsync(Chat chat, bool translate = false) CreatedAt = DateTime.Now }; } - + public async Task ProcessAsync(string message, bool translate = false) { var chat = await _agentService.GetChatByAgent(_firstAgent!.Id); + var backend = ModelRegistry.GetById(chat.ModelId).Backend; chat.Messages.Add(new Message() { Content = message, Role = "User", - Type = chat.Backend != BackendType.Self ? MessageType.LocalLLM : MessageType.CloudLLM, + Type = backend == BackendType.Self ? MessageType.LocalLLM : MessageType.CloudLLM, Time = DateTime.Now }); var result = await _agentService.Process(chat, _firstAgent.Id, null, translate); @@ -175,7 +170,7 @@ public async Task ProcessAsync(string message, bool translate = fals CreatedAt = DateTime.Now }; } - + public async Task ProcessAsync(Message message, bool translate = false) { var chat = await _agentService.GetChatByAgent(_firstAgent!.Id); @@ -190,7 +185,7 @@ public async Task ProcessAsync(Message message, bool translate = fal CreatedAt = DateTime.Now }; } - + public IFlowContext AddAgents(IEnumerable agents) { foreach (var agent in agents) @@ -198,43 +193,38 @@ public IFlowContext AddAgents(IEnumerable agents) agent.Flow = true; _flow.Agents.Add(agent); } - + return this; } - - public async Task CreateAsync() - { - return await _flowService.CreateFlow(_flow); - } + + public async Task CreateAsync() => await _flowService.CreateFlow(_flow); public async Task Delete() { - if (_flow.Id == null) + if (_flow.Id is null) + { throw new FlowNotInitializedException(); - + } + await _flowService.DeleteFlow(_flow.Id); } // Retrieval Methods public async Task GetCurrentFlow() { - if (_flow.Id == null) - throw new FlowNotInitializedException(); - - return await _flowService.GetFlowById(_flow.Id); + return _flow.Id is null + ? throw new FlowNotInitializedException() + : await _flowService.GetFlowById(_flow.Id); } - public async Task> GetAllFlows() - { - return await _flowService.GetAllFlows(); - } + public async Task> GetAllFlows() => await _flowService.GetAllFlows(); // Static factory methods public async Task FromExisting(string flowId) { var existingFlow = await _flowService.GetFlowById(flowId); - return existingFlow == null + return existingFlow is null ? throw new FlowNotFoundException(flowId) : this; } -} \ No newline at end of file +} diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatConfigurationBuilder.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatConfigurationBuilder.cs index 5c3c1788..fcbec9f4 100644 --- a/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatConfigurationBuilder.cs +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatConfigurationBuilder.cs @@ -1,4 +1,3 @@ -using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Entities.Tools; using MaIN.Domain.Models; @@ -18,7 +17,7 @@ public interface IChatConfigurationBuilder : IChatActions /// MaxTokens, TopP, etc. These parameters control the generation behavior of the chat. /// The context instance implementing for method chaining. IChatConfigurationBuilder WithInferenceParams(InferenceParams inferenceParams); - + /// /// Attaches external tools/functions that the model can invoke during the conversation. /// @@ -26,7 +25,7 @@ public interface IChatConfigurationBuilder : IChatActions /// and their execution modes. /// The context instance implementing for method chaining. IChatConfigurationBuilder WithTools(ToolsConfiguration toolsConfiguration); - + /// /// Sets the memory parameters for the chat session, allowing you to customize how the AI accesses and /// uses its memory for generating responses. Memory parameters influence aspects such as context size, memory search depth, @@ -36,21 +35,14 @@ public interface IChatConfigurationBuilder : IChatActions /// MaxMatchesCount, AnswerTokens, etc. These parameters control how the chat uses memory for response generation. /// The context instance implementing for method chaining. IChatConfigurationBuilder WithMemoryParams(MemoryParams memoryParams); - + /// /// Configures the session to use Text-to-Speech for the model's responses. /// /// A - parameters for the voice synthesis. /// The context instance implementing for method chaining. IChatConfigurationBuilder Speak(TextToSpeechParams speechParams); - - /// - /// Defines backend that will be used for model inference - /// - /// The - an enum that defines which AI backend to use. - /// The context instance implementing for method chaining. - IChatConfigurationBuilder WithBackend(BackendType backendType); - + /// /// Inserts a system message at the beginning of the chat. System messages are typically used for setting the context /// or providing instructions to the AI. @@ -58,7 +50,7 @@ public interface IChatConfigurationBuilder : IChatActions /// The system prompt content that provides instructions to the AI. /// The context instance implementing for method chaining. IChatConfigurationBuilder WithSystemPrompt(string systemPrompt); - + /// /// Attaches files to the most recent message in the chat. Files are associated with the last message to provide additional context /// or media for the AI to process. @@ -68,7 +60,7 @@ public interface IChatConfigurationBuilder : IChatActions /// but can also greatly improve the quality of inference /// The context instance implementing for method chaining. IChatConfigurationBuilder WithFiles(List file, bool preProcess = false); - + /// /// Attaches files to the most recent message in the chat. Files are associated with the last message to provide additional context /// or media for the AI to process. @@ -78,7 +70,7 @@ public interface IChatConfigurationBuilder : IChatActions /// but can also greatly improve the quality of inference /// The context instance implementing for method chaining. IChatConfigurationBuilder WithFiles(List file, bool preProcess = false); - + /// /// Attaches a list of files to the most recent message in the chat by specifying their file paths. /// This method is an alternative to using FileInfo. @@ -88,14 +80,14 @@ public interface IChatConfigurationBuilder : IChatActions /// but can also greatly improve the quality of inference /// The context instance implementing for method chaining. IChatConfigurationBuilder WithFiles(List file, bool preProcess = false); - + /// /// Each time we run inference, we need to load the model into memory; this takes time and memory. This method allows us to save some /// more of GPU/RAM resources at the cost of time, because model weights are no longer cached /// /// The context instance implementing for method chaining. IChatConfigurationBuilder DisableCache(); - + /// /// Completes the chat session by generating a response based on the messages so far. This method interacts with the underlying /// chat service to process the chat and generate a result. @@ -105,4 +97,4 @@ public interface IChatConfigurationBuilder : IChatActions /// An optional callback invoked whenever a new token or update is received during streaming. /// A object containing the result of the completed chat session. Task CompleteAsync(bool translate = false, bool interactive = false, Func? changeOfValue = null, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/src/MaIN.Domain/Entities/Chat.cs b/src/MaIN.Domain/Entities/Chat.cs index 2f905775..b55eb9a8 100644 --- a/src/MaIN.Domain/Entities/Chat.cs +++ b/src/MaIN.Domain/Entities/Chat.cs @@ -1,7 +1,5 @@ using LLama.Batched; -using MaIN.Domain.Configuration; using MaIN.Domain.Entities.Tools; -using MaIN.Domain.Models.Abstract; namespace MaIN.Domain.Entities; @@ -9,32 +7,7 @@ public class Chat { public string Id { get; init; } = string.Empty; public required string Name { get; init; } - private string? _modelId; - public required string ModelId - { - get => _modelInstance?.Id ?? _modelId ?? string.Empty; - set - { - _modelId = value; - if (string.IsNullOrEmpty(value)) - { - _modelInstance = null; - return; - } - - ModelRegistry.TryGetById(value, out _modelInstance); - } - } - private AIModel? _modelInstance; - public AIModel? ModelInstance - { - get => _modelInstance; - set - { - _modelInstance = value; - _modelId = value?.Id ?? string.Empty; - } - } + public string ModelId { get; set; } = string.Empty; public List Messages { get; set; } = []; public ChatType Type { get; set; } = ChatType.Conversation; public bool ImageGen { get; set; } @@ -44,9 +17,8 @@ public AIModel? ModelInstance public TextToSpeechParams? TextToSpeechParams { get; set; } public Dictionary Properties { get; init; } = []; public List Memory { get; } = []; - 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.Infrastructure/Models/ChatDocument.cs b/src/MaIN.Infrastructure/Models/ChatDocument.cs index b7847355..f560bf6e 100644 --- a/src/MaIN.Infrastructure/Models/ChatDocument.cs +++ b/src/MaIN.Infrastructure/Models/ChatDocument.cs @@ -1,4 +1,3 @@ -using MaIN.Domain.Configuration; using MaIN.Domain.Entities.Tools; using MongoDB.Bson.Serialization.Attributes; @@ -13,7 +12,6 @@ public class ChatDocument public required List Messages { get; init; } public ChatTypeDocument Type { get; init; } public required Dictionary Properties { get; init; } = []; - public BackendType? Backend { get; set; } public bool ImageGen { get; init; } public bool Interactive { get; init; } public bool Translate { get; init; } @@ -21,4 +19,4 @@ public class ChatDocument public MemoryParamsDocument? MemoryParams { get; init; } public object? ConvState { get; init; } public ToolsConfiguration? ToolsConfiguration { get; set; } -} \ No newline at end of file +} diff --git a/src/MaIN.Infrastructure/Repositories/Sql/SqlChatRepository.cs b/src/MaIN.Infrastructure/Repositories/Sql/SqlChatRepository.cs index 5b9fe47e..027a8089 100644 --- a/src/MaIN.Infrastructure/Repositories/Sql/SqlChatRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/Sql/SqlChatRepository.cs @@ -1,9 +1,8 @@ -using System.Data; -using System.Text.Json; using Dapper; -using MaIN.Domain.Configuration; using MaIN.Infrastructure.Models; using MaIN.Infrastructure.Repositories.Abstract; +using System.Data; +using System.Text.Json; namespace MaIN.Infrastructure.Repositories.Sql; @@ -21,26 +20,25 @@ private ChatDocument MapChatDocument(dynamic row) Id = row.Id, Name = row.Name, Model = row.Model, - Messages = row.Messages != null ? - JsonSerializer.Deserialize>(row.Messages.ToString(), _jsonOptions) : - new List(), - Type = row.Type != null ? - JsonSerializer.Deserialize(row.Type.ToString(), _jsonOptions) : - default, - ConvState = row.ConvState != null ? - JsonSerializer.Deserialize(row.ConvState.ToString(), _jsonOptions) : - default, - InferenceParams = row.InferenceParams != null ? - JsonSerializer.Deserialize(row.InferenceParams.ToString(), _jsonOptions) : - default, - MemoryParams = row.MemoryParams != null ? - JsonSerializer.Deserialize(row.MemoryParams.ToString(), _jsonOptions) : - default, - Properties = row.Properties != null ? - JsonSerializer.Deserialize>(row.Properties.ToString(), _jsonOptions) : - new Dictionary(), + Messages = row.Messages is not null + ? JsonSerializer.Deserialize>(row.Messages.ToString(), _jsonOptions) + : new List(), + Type = row.Type is not null + ? JsonSerializer.Deserialize(row.Type.ToString(), _jsonOptions) + : default, + ConvState = row.ConvState is not null + ? JsonSerializer.Deserialize(row.ConvState.ToString(), _jsonOptions) + : default, + InferenceParams = row.InferenceParams is not null + ? JsonSerializer.Deserialize(row.InferenceParams.ToString(), _jsonOptions) + : default, + MemoryParams = row.MemoryParams is not null + ? JsonSerializer.Deserialize(row.MemoryParams.ToString(), _jsonOptions) + : default, + Properties = row.Properties is not null + ? JsonSerializer.Deserialize>(row.Properties.ToString(), _jsonOptions) + : new Dictionary(), ImageGen = row.Visual, - Backend = (BackendType)row.BackendType, Interactive = row.Interactive }; return chat; @@ -48,24 +46,22 @@ private ChatDocument MapChatDocument(dynamic row) private object MapChatToParameters(ChatDocument chat) { - if (chat == null) - throw new ArgumentNullException(nameof(chat)); - - return new - { - chat.Id, - chat.Name, - chat.Model, - Messages = JsonSerializer.Serialize(chat.Messages, _jsonOptions), - Type = JsonSerializer.Serialize(chat.Type, _jsonOptions), - ConvState = JsonSerializer.Serialize(chat.ConvState, _jsonOptions), - InferenceParams = JsonSerializer.Serialize(chat.InferenceParams, _jsonOptions), - MemoryParams = JsonSerializer.Serialize(chat.MemoryParams, _jsonOptions), - Properties = JsonSerializer.Serialize(chat.Properties, _jsonOptions), - Visual = chat.ImageGen, - BackendType = chat.Backend ?? 0, - chat.Interactive - }; + return chat is null + ? throw new ArgumentNullException(nameof(chat)) + : new + { + chat.Id, + chat.Name, + chat.Model, + Messages = JsonSerializer.Serialize(chat.Messages, _jsonOptions), + Type = JsonSerializer.Serialize(chat.Type, _jsonOptions), + ConvState = JsonSerializer.Serialize(chat.ConvState, _jsonOptions), + InferenceParams = JsonSerializer.Serialize(chat.InferenceParams, _jsonOptions), + MemoryParams = JsonSerializer.Serialize(chat.MemoryParams, _jsonOptions), + Properties = JsonSerializer.Serialize(chat.Properties, _jsonOptions), + Visual = chat.ImageGen, + chat.Interactive + }; } public async Task> GetAllChats() @@ -80,7 +76,7 @@ public async Task> GetAllChats() var row = await connection.QueryFirstOrDefaultAsync(@" SELECT * FROM Chats WHERE Id = @Id", new { Id = id }); - return row != null ? MapChatDocument(row) : null; + return row is not null ? MapChatDocument(row) : null; } public async Task AddChat(ChatDocument chat) @@ -94,7 +90,7 @@ INSERT INTO Chats ( Stream, Visual, ConvState, InferenceParams, MemoryParams, Interactive ) VALUES ( @Id, @Name, @Model, @Messages, @Type, @Properties, - @Visual, @ConvState, @InferenceParams, @MemoryParams, @Interactive)", + @Visual, @ConvState, @InferenceParams, @MemoryParams, @Interactive)", parameters); } @@ -122,14 +118,4 @@ DELETE FROM Chats WHERE Id = @Id", new { Id = id }); - // TODO nice idea but does it even work? nothing using it can be removed as well probably - public async Task> GetChatsByProperty(string key, string value) - { - var rows = await connection.QueryAsync(@" - SELECT * - FROM Chats - WHERE JSON_VALUE(Properties, '$." + key + @"') = @value", - new { value }); - return rows.Select(MapChatDocument); - } -} \ No newline at end of file +} diff --git a/src/MaIN.Infrastructure/Repositories/Sqlite/SqliteChatRepository.cs b/src/MaIN.Infrastructure/Repositories/Sqlite/SqliteChatRepository.cs index a09dd7ec..634b760d 100644 --- a/src/MaIN.Infrastructure/Repositories/Sqlite/SqliteChatRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/Sqlite/SqliteChatRepository.cs @@ -1,9 +1,9 @@ -using System.Data; -using System.Text.Json; using Dapper; using MaIN.Domain.Entities; using MaIN.Infrastructure.Models; using MaIN.Infrastructure.Repositories.Abstract; +using System.Data; +using System.Text.Json; namespace MaIN.Infrastructure.Repositories.Sqlite; @@ -21,34 +21,32 @@ private ChatDocument MapChatDocument(dynamic row) Id = row.Id, Name = row.Name, Model = row.Model, - Messages = row.Messages != null ? - JsonSerializer.Deserialize>(row.Messages, _jsonOptions) : - new List(), - Type = row.Type != null ? - JsonSerializer.Deserialize(row.Type, _jsonOptions) : - default, - ConvState = row.Type != null ? - JsonSerializer.Deserialize(row.ConvState, _jsonOptions) : - default, - InferenceParams = row.Type != null ? - JsonSerializer.Deserialize(row.InferenceParams, _jsonOptions) : - default, - MemoryParams = row.Type != null ? - JsonSerializer.Deserialize(row.MemoryParams, _jsonOptions) : - default, - Properties = row.Properties != null ? - JsonSerializer.Deserialize>(row.Properties, _jsonOptions) : - new Dictionary(), + Messages = row.Messages is not null + ? JsonSerializer.Deserialize>(row.Messages, _jsonOptions) + : new List(), + Type = row.Type is not null + ? JsonSerializer.Deserialize(row.Type, _jsonOptions) + : default, + ConvState = row.Type is not null + ? JsonSerializer.Deserialize(row.ConvState, _jsonOptions) + : default, + InferenceParams = row.Type is not null + ? JsonSerializer.Deserialize(row.InferenceParams, _jsonOptions) + : default, + MemoryParams = row.Type is not null + ? JsonSerializer.Deserialize(row.MemoryParams, _jsonOptions) + : default, + Properties = row.Properties is not null + ? JsonSerializer.Deserialize>(row.Properties, _jsonOptions) + : new Dictionary(), ImageGen = Convert.ToBoolean(row.Visual), - Backend = row.BackendType, Interactive = Convert.ToBoolean(row.Interactive) }; return chat; } - private object MapChatToParameters(ChatDocument chat) - { - return new + private object MapChatToParameters(ChatDocument chat) => + new { chat.Id, chat.Name, @@ -60,10 +58,8 @@ private object MapChatToParameters(ChatDocument chat) Type = JsonSerializer.Serialize(chat.Type, _jsonOptions), Properties = JsonSerializer.Serialize(chat.Properties, _jsonOptions), Visual = chat.ImageGen ? 1 : 0, - BackendType = chat.Backend ?? 0, Interactive = chat.Interactive ? 1 : 0 }; - } public async Task> GetAllChats() { @@ -77,7 +73,7 @@ public async Task> GetAllChats() var row = await connection.QueryFirstOrDefaultAsync( "SELECT * FROM Chats WHERE Id = @Id", new { Id = id }); - return row != null ? MapChatDocument(row) : null; + return row is not null ? MapChatDocument(row) : null; } public async Task AddChat(ChatDocument chat) @@ -107,4 +103,4 @@ public async Task DeleteChat(string id) => await connection.ExecuteAsync( "DELETE FROM Chats WHERE Id = @Id", new { Id = id }); -} \ No newline at end of file +} diff --git a/src/MaIN.Services/Mappers/ChatMapper.cs b/src/MaIN.Services/Mappers/ChatMapper.cs index 1bdf0398..802d34bb 100644 --- a/src/MaIN.Services/Mappers/ChatMapper.cs +++ b/src/MaIN.Services/Mappers/ChatMapper.cs @@ -16,14 +16,14 @@ public static ChatDto ToDto(this Chat chat) Id = chat.Id, Name = chat.Name, Model = chat.ModelId, - Messages = chat.Messages.Select(m => m.ToDto()).ToList(), + Messages = [.. chat.Messages.Select(m => m.ToDto())], ImageGen = chat.ImageGen, Type = Enum.Parse(chat.Type.ToString()), Properties = chat.Properties }; private static MessageDto ToDto(this Message message) - => new MessageDto() + => new() { Content = message.Content, Role = message.Tool ? "System" : message.Role, @@ -69,29 +69,28 @@ private static Message ToDomain(this MessageDto message) }; private static MessageDocument ToDocument(this Message message) - => new MessageDocument() + => new() { Content = message.Content, Role = message.Role, Time = message.Time, MessageType = message.Type.ToString(), Images = message.Image, - Tokens = message.Tokens.Select(x => x.ToDocument()).ToList(), + Tokens = [.. message.Tokens.Select(x => x.ToDocument())], Properties = message.Properties, Tool = message.Tool, Files = (message.Files?.Select(x => x.Content).ToArray() ?? [])! }; public static ChatDocument ToDocument(this Chat chat) - => new ChatDocument() + => new() { Id = chat.Id, Name = chat.Name, Model = chat.ModelId, - Messages = chat.Messages.Select(m => m.ToDocument()).ToList(), + Messages = [.. chat.Messages.Select(m => m.ToDocument())], ImageGen = chat.ImageGen, - Backend = chat.Backend, - ToolsConfiguration = chat.ToolsConfiguration, + ToolsConfiguration = chat.ToolsConfiguration, MemoryParams = chat.MemoryParams.ToDocument(), InferenceParams = chat.InterferenceParams.ToDocument(), ConvState = chat.ConversationState, @@ -107,9 +106,8 @@ public static Chat ToDomain(this ChatDocument chat) Id = chat.Id, Name = chat.Name, ModelId = chat.Model, - Messages = chat.Messages.Select(m => m.ToDomain()).ToList(), + Messages = [.. chat.Messages.Select(m => m.ToDomain())], ImageGen = chat.ImageGen, - Backend = chat.Backend, Properties = chat.Properties, ToolsConfiguration = chat.ToolsConfiguration, ConversationState = chat.ConvState as Conversation.State, @@ -127,7 +125,7 @@ private static Message ToDomain(this MessageDocument message) Tool = message.Tool, Time = message.Time, Type = Enum.Parse(message.MessageType), - Tokens = message.Tokens.Select(x => x.ToDomain()).ToList(), + Tokens = [.. message.Tokens.Select(x => x.ToDomain())], Role = message.Role, Image = message.Images, Properties = message.Properties, @@ -141,14 +139,14 @@ private static LLMTokenValueDocument ToDocument(this LLMTokenValue llmTokenValue }; private static LLMTokenValue ToDomain(this LLMTokenValueDocument llmTokenValue) - => new LLMTokenValue() + => new() { Text = llmTokenValue.Text, Type = llmTokenValue.Type }; private static InferenceParams ToDomain(this InferenceParamsDocument inferenceParams) - => new InferenceParams + => new() { Temperature = inferenceParams.Temperature, ContextSize = inferenceParams.ContextSize, @@ -165,9 +163,9 @@ private static InferenceParams ToDomain(this InferenceParamsDocument inferencePa TopP = inferenceParams.TopP, Grammar = inferenceParams.Grammar }; - + private static MemoryParams ToDomain(this MemoryParamsDocument memoryParams) - => new MemoryParams + => new() { Temperature = memoryParams.Temperature, AnswerTokens = memoryParams.AnswerTokens, @@ -180,7 +178,7 @@ private static MemoryParams ToDomain(this MemoryParamsDocument memoryParams) }; private static InferenceParamsDocument ToDocument(this InferenceParams inferenceParams) - => new InferenceParamsDocument + => new() { Temperature = inferenceParams.Temperature, ContextSize = inferenceParams.ContextSize, @@ -197,9 +195,9 @@ private static InferenceParamsDocument ToDocument(this InferenceParams inference TopP = inferenceParams.TopP, Grammar = inferenceParams.Grammar }; - + private static MemoryParamsDocument ToDocument(this MemoryParams memoryParams) - => new MemoryParamsDocument + => new() { Temperature = memoryParams.Temperature, AnswerTokens = memoryParams.AnswerTokens, @@ -210,4 +208,4 @@ private static MemoryParamsDocument ToDocument(this MemoryParams memoryParams) FrequencyPenalty = memoryParams.FrequencyPenalty, Grammar = memoryParams.Grammar }; -} \ No newline at end of file +} diff --git a/src/MaIN.Services/Services/AgentService.cs b/src/MaIN.Services/Services/AgentService.cs index fc06c719..8d35aa97 100644 --- a/src/MaIN.Services/Services/AgentService.cs +++ b/src/MaIN.Services/Services/AgentService.cs @@ -1,11 +1,11 @@ -using System.Text.RegularExpressions; using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Entities.Agents; using MaIN.Domain.Entities.Agents.Knowledge; using MaIN.Domain.Entities.Tools; -using MaIN.Domain.Models; using MaIN.Domain.Exceptions.Agents; +using MaIN.Domain.Models; +using MaIN.Domain.Models.Abstract; using MaIN.Infrastructure.Repositories.Abstract; using MaIN.Services.Constants; using MaIN.Services.Mappers; @@ -16,6 +16,7 @@ using MaIN.Services.Services.Steps.Commands.Abstract; using MaIN.Services.Utils; using Microsoft.Extensions.Logging; +using System.Text.RegularExpressions; using static System.Text.RegularExpressions.Regex; namespace MaIN.Services.Services; @@ -39,19 +40,19 @@ public async Task Process( Func? callbackToken = null, Func? callbackTool = null) { - var agent = await agentRepository.GetAgentById(agentId); - if (agent == null) - { - throw new AgentNotFoundException(agentId); - } - - if (agent.Context == null) + var agent = await agentRepository.GetAgentById(agentId) ?? throw new AgentNotFoundException(agentId); + + if (agent.Context is null) { throw new AgentContextNotFoundException(agentId); - } + } await notificationService.DispatchNotification( - NotificationMessageBuilder.ProcessingStarted(agentId, agent.CurrentBehaviour, "STARTED"), "ReceiveAgentUpdate"); + NotificationMessageBuilder.ProcessingStarted( + agentId, + agent.CurrentBehaviour, + "STARTED"), + "ReceiveAgentUpdate"); try { @@ -65,7 +66,8 @@ await notificationService.DispatchNotification( async (status, id, progress, behaviour, details) => { await notificationService.DispatchNotification( - NotificationMessageBuilder.CreateActorProgress(id, status, progress, behaviour, details), "ReceiveAgentUpdate"); //TODO prepare static lookup for magic string :) + NotificationMessageBuilder.CreateActorProgress(id, status, progress, behaviour, details), + "ReceiveAgentUpdate"); //TODO prepare static lookup for magic string :) }, async c => await chatRepository.UpdateChat(c.Id, c.ToDocument()), logger @@ -78,11 +80,11 @@ await notificationService.DispatchNotification( //normalize message before returning it to user chat.Messages.Last().Content = Replace( - chat.Messages.Last().Content, - @".*?", - string.Empty, - RegexOptions.Singleline); - + chat.Messages.Last().Content, + @".*?", + string.Empty, + RegexOptions.Singleline); + return chat; } catch (Exception ex) @@ -105,9 +107,8 @@ public async Task CreateAgent(Agent agent, bool flow = false, bool intera ToolsConfiguration = agent.ToolsConfiguration, InterferenceParams = inferenceParams ?? new InferenceParams(), MemoryParams = memoryParams ?? new MemoryParams(), - Messages = new List(), + Messages = [], Interactive = interactiveResponse, - Backend = agent.Backend, Type = flow ? ChatType.Flow : ChatType.Rag, }; @@ -127,7 +128,7 @@ public async Task CreateAgent(Agent agent, bool flow = false, bool intera agent.Started = true; agent.Flow = flow; - agent.Behaviours ??= new Dictionary(); + agent.Behaviours ??= []; agent.Behaviours.Add("Default", agent.Context.Instruction!); agent.CurrentBehaviour = "Default"; @@ -142,24 +143,16 @@ public async Task CreateAgent(Agent agent, bool flow = false, bool intera public async Task GetChatByAgent(string agentId) { - var agent = await agentRepository.GetAgentById(agentId); - if (agent == null) - { - throw new AgentNotFoundException(agentId); - } - + var agent = await agentRepository.GetAgentById(agentId) ?? throw new AgentNotFoundException(agentId); + var chat = await chatRepository.GetChatById(agent.ChatId); return chat!.ToDomain(); } public async Task Restart(string agentId) { - var agent = await agentRepository.GetAgentById(agentId); - if (agent == null) - { - throw new AgentNotFoundException(agentId); - } - + var agent = await agentRepository.GetAgentById(agentId) ?? throw new AgentNotFoundException(agentId); + var chat = (await chatRepository.GetChatById(agent.ChatId))!.ToDomain(); var llmService = llmServiceFactory.CreateService(agent.Backend ?? maInSettings.BackendType); await llmService.CleanSessionCache(chat.Id!); @@ -171,23 +164,21 @@ public async Task Restart(string agentId) return chat; } - public async Task> GetAgents() => - (await agentRepository.GetAllAgents()) - .Select(x => x.ToDomain()) - .ToList(); + public async Task> GetAgents() => [.. (await agentRepository.GetAllAgents()).Select(x => x.ToDomain())]; - public async Task GetAgentById(string id) => - (await agentRepository.GetAgentById(id))?.ToDomain(); + public async Task GetAgentById(string id) => (await agentRepository.GetAgentById(id))?.ToDomain(); public async Task DeleteAgent(string id) { var chat = await GetChatByAgent(id); - var llmService = llmServiceFactory.CreateService(chat.Backend ?? maInSettings.BackendType); + var backend = ModelRegistry.TryGetById(chat.ModelId, out var model) + ? model!.Backend + : maInSettings.BackendType; + var llmService = llmServiceFactory.CreateService(backend); await llmService.CleanSessionCache(chat.Id); await chatRepository.DeleteChat(chat.Id); await agentRepository.DeleteAgent(id); } - public Task AgentExists(string id) => - agentRepository.Exists(id); -} \ No newline at end of file + public Task AgentExists(string id) => agentRepository.Exists(id); +} diff --git a/src/MaIN.Services/Services/ChatService.cs b/src/MaIN.Services/Services/ChatService.cs index d805f4a0..e7371145 100644 --- a/src/MaIN.Services/Services/ChatService.cs +++ b/src/MaIN.Services/Services/ChatService.cs @@ -2,6 +2,7 @@ using MaIN.Domain.Entities; using MaIN.Domain.Exceptions.Chats; using MaIN.Domain.Models; +using MaIN.Domain.Models.Abstract; using MaIN.Infrastructure.Repositories.Abstract; using MaIN.Services.Mappers; using MaIN.Services.Services.Abstract; @@ -33,15 +34,17 @@ public async Task Completions( Func? changeOfValue = null, CancellationToken cancellationToken = default) { - if (chat.ModelId == ImageGenService.LocalImageModels.FLUX) + if (chat.ModelId == ImageGenService.LocalImageModels.FLUX) { chat.ImageGen = true; } - chat.Backend = settings.BackendType; + + var model = ModelRegistry.GetById(chat.ModelId); + var backend = model.Backend; chat.Messages.Where(x => x.Type == MessageType.NotSet).ToList() - .ForEach(x => x.Type = chat.Backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM); - + .ForEach(x => x.Type = backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM); + translate = translate || chat.Translate; interactiveUpdates = interactiveUpdates || chat.Interactive; var newMsg = chat.Messages.Last(); @@ -49,7 +52,7 @@ public async Task Completions( 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() @@ -61,8 +64,8 @@ public async Task Completions( } var result = chat.ImageGen - ? await imageGenServiceFactory.CreateService(chat.Backend.Value)!.Send(chat) - : await llmServiceFactory.CreateService(chat.Backend.Value).Send(chat, new ChatRequestOptions() + ? await imageGenServiceFactory.CreateService(backend)!.Send(chat) + : await llmServiceFactory.CreateService(backend).Send(chat, new ChatRequestOptions() { InteractiveUpdates = interactiveUpdates, TokenCallback = changeOfValue @@ -73,37 +76,39 @@ public async Task Completions( result!.Message.Content = await translatorService.Translate(result.Message.Content, lng!); result.Message.Time = DateTime.Now; } - + if (!chat.ImageGen && chat.TextToSpeechParams is not null) { var speechBytes = await ttsServiceFactory - .CreateService(chat.Backend.Value).Send(result!.Message, chat.TextToSpeechParams.Model, + .CreateService(backend).Send(result!.Message, chat.TextToSpeechParams.Model, chat.TextToSpeechParams.Voice, chat.TextToSpeechParams.Playback); - + result.Message.Speech = speechBytes; } - + originalMessages.Add(result!.Message); chat.Messages = originalMessages; - + await chatProvider.UpdateChat(chat.Id!, chat.ToDocument()); return result; } public async Task Delete(string id) { - var chat = await chatProvider.GetChatById(id); - var llmService = llmServiceFactory.CreateService(chat?.Backend ?? settings.BackendType); + var chatDoc = await chatProvider.GetChatById(id); + var backend = chatDoc is not null && ModelRegistry.TryGetById(chatDoc.Model, out var model) + ? model!.Backend + : settings.BackendType; + var llmService = llmServiceFactory.CreateService(backend); await llmService.CleanSessionCache(id); await chatProvider.DeleteChat(id); } - + public async Task GetById(string id) { var chatDocument = await chatProvider.GetChatById(id); return chatDocument is null ? throw new ChatNotFoundException(id) : chatDocument.ToDomain(); } - public async Task> GetAll() - => [.. (await chatProvider.GetAllChats()).Select(x => x.ToDomain())]; -} \ No newline at end of file + public async Task> GetAll() => [.. (await chatProvider.GetAllChats()).Select(x => x.ToDomain())]; +} diff --git a/src/MaIN.Services/Services/LLMService/LLMService.cs b/src/MaIN.Services/Services/LLMService/LLMService.cs index 2591e8a3..6aa3b36b 100644 --- a/src/MaIN.Services/Services/LLMService/LLMService.cs +++ b/src/MaIN.Services/Services/LLMService/LLMService.cs @@ -1,6 +1,3 @@ -using System.Collections.Concurrent; -using System.Text; -using System.Text.Json; using LLama; using LLama.Batched; using LLama.Common; @@ -8,8 +5,8 @@ using LLama.Sampling; using MaIN.Domain.Configuration; using MaIN.Domain.Entities; -using MaIN.Domain.Exceptions.Models; using MaIN.Domain.Entities.Tools; +using MaIN.Domain.Exceptions.Models; using MaIN.Domain.Models; using MaIN.Domain.Models.Abstract; using MaIN.Services.Constants; @@ -19,6 +16,9 @@ using MaIN.Services.Services.Models; using MaIN.Services.Utils; using Microsoft.KernelMemory; +using System.Collections.Concurrent; +using System.Text; +using System.Text.Json; using Grammar = LLama.Sampling.Grammar; using InferenceParams = MaIN.Domain.Entities.InferenceParams; #pragma warning disable KMEXP00 @@ -68,7 +68,7 @@ public LLMService( return await AskMemory(chat, memoryOptions, requestOptions, cancellationToken); } - if (chat.ToolsConfiguration?.Tools != null && chat.ToolsConfiguration.Tools.Count != 0) + if (chat.ToolsConfiguration?.Tools is not null && chat.ToolsConfiguration.Tools.Count != 0) { return await ProcessWithToolsAsync(chat, requestOptions, cancellationToken); } @@ -83,7 +83,7 @@ public Task GetCurrentModels() { var models = Directory.GetFiles(modelsPath, "*.gguf", SearchOption.AllDirectories) .Select(Path.GetFileName) - .Where(fileName => ModelRegistry.GetByFileName(fileName!) != null) + .Where(fileName => ModelRegistry.GetByFileName(fileName!) is not null) .Select(fileName => ModelRegistry.GetByFileName(fileName!)!.Name) .ToArray(); @@ -101,7 +101,6 @@ public Task CleanSessionCache(string? id) return Task.CompletedTask; } - public async Task AskMemory( Chat chat, ChatMemoryOptions memoryOptions, @@ -127,7 +126,7 @@ public Task CleanSessionCache(string? id) await memoryService.ImportDataToMemory((km, generator), memoryOptions, cancellationToken); var userMessage = chat.Messages.Last(); - + MemoryAnswer result; var tokens = new List(); @@ -165,8 +164,10 @@ await notificationService.DispatchNotification( ServiceConstants.Notifications.ReceiveMessageUpdate); } - if (requestOptions.TokenCallback != null) + if (requestOptions.TokenCallback is not null) + { await requestOptions.TokenCallback(tokenValue); + } } } @@ -198,6 +199,7 @@ await notificationService.DispatchNotification( ModelLoader.RemoveModel(model.FileName); textGenerator.Dispose(); } + generator._embedder.Dispose(); generator._embedder._weights.Dispose(); generator.Dispose(); @@ -236,9 +238,11 @@ private async Task> ProcessChatRequest( var visionModel = model as IVisionModel; var llavaWeights = visionModel?.MMProjectName is not null - ? await LLavaWeights.LoadFromFileAsync(Path.Combine(modelsPath, visionModel.MMProjectName), cancellationToken) + ? await LLavaWeights.LoadFromFileAsync( + Path.Combine(modelsPath, visionModel.MMProjectName), + cancellationToken) : null; - + using var executor = new BatchedExecutor(llmModel, parameters); var (conversation, isComplete, hasFailed) = await LLMService.InitializeConversation( @@ -285,8 +289,8 @@ private ModelParams CreateModelParameters(Chat chat, string modelKey, string? cu }; } - - private static 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, LocalModel model, LLamaWeights llmModel, @@ -294,12 +298,12 @@ private ModelParams CreateModelParameters(Chat chat, string modelKey, string? cu BatchedExecutor executor, CancellationToken cancellationToken) { - var isNewConversation = chat.ConversationState == null; + var isNewConversation = chat.ConversationState is null; var conversation = isNewConversation ? executor.Create() : executor.Load(chat.ConversationState!); - if (lastMsg.Image != null) + if (lastMsg.Image is not null) { await ProcessImageMessage(conversation, lastMsg, llmModel, llavaWeights, executor, cancellationToken); } @@ -341,7 +345,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.Count != 0; + var hasTools = chat.ToolsConfiguration?.Tools is not null && chat.ToolsConfiguration.Tools.Count != 0; if (isNewConversation) { @@ -475,51 +479,51 @@ private static string FormatToolsForPrompt(ToolsConfiguration toolsConfig) requestOptions.TokenCallback?.Invoke(tokenValue); } } - - + + return (tokens, isComplete, hasFailed); } private static BaseSamplingPipeline CreateSampler(InferenceParams interferenceParams) { - if (interferenceParams.Temperature == 0) - { - return new GreedySamplingPipeline() + return interferenceParams.Temperature == 0 + ? new GreedySamplingPipeline() + { + Grammar = interferenceParams.Grammar is not null + ? new Grammar(interferenceParams.Grammar.Value, "root") + : null + } + : new DefaultSamplingPipeline() { - Grammar = interferenceParams.Grammar != null ? new Grammar(interferenceParams.Grammar.Value, "root") : null + Temperature = interferenceParams.Temperature, + TopP = interferenceParams.TopP, + TopK = interferenceParams.TopK, + Grammar = interferenceParams.Grammar is not null + ? new Grammar(interferenceParams.Grammar.Value, "root") + : null }; - } - - return new DefaultSamplingPipeline() - { - Temperature = interferenceParams.Temperature, - TopP = interferenceParams.TopP, - TopK = interferenceParams.TopK, - Grammar = interferenceParams.Grammar != null ? new Grammar(interferenceParams.Grammar.Value, "root") : null - }; } private static LocalModel GetLocalModel(string modelId) { var model = ModelRegistry.GetById(modelId); - if (model is not LocalModel localModel) - { - throw new InvalidModelTypeException(nameof(LocalModel)); - } - - return localModel; + return model is not LocalModel localModel + ? throw new InvalidModelTypeException(nameof(LocalModel)) + : localModel; } private string GetModelsPath() { var path = options.ModelsPath ?? Environment.GetEnvironmentVariable(DEFAULT_MODEL_ENV_PATH); - return string.IsNullOrEmpty(path) - ? throw new ModelsPathNotFoundException() + return string.IsNullOrEmpty(path) + ? throw new ModelsPathNotFoundException() : path; } - private async Task CreateChatResult(Chat chat, List tokens, + private async Task CreateChatResult( + Chat chat, + List tokens, ChatRequestOptions requestOptions) { var responseText = string.Concat(tokens.Select(x => x.Text)); @@ -560,7 +564,7 @@ private async Task ProcessWithToolsAsync( ChatRequestOptions requestOptions, CancellationToken cancellationToken) { - var model = chat.ModelInstance ?? throw new MissingModelInstanceException(); + var model = ModelRegistry.GetById(chat.ModelId); if (model is not LocalModel localModel) { throw new InvalidModelTypeException(nameof(LocalModel)); @@ -610,11 +614,14 @@ private async Task ProcessWithToolsAsync( { requestOptions.InteractiveUpdates = true; requestOptions.TokenCallback = tokenCallbackOrg; - await SendNotification(chat.Id, new LLMTokenValue - { - Type = TokenType.FullAnswer, - Text = lastResponse - }, false); + await SendNotification( + chat.Id, + new LLMTokenValue + { + Type = TokenType.FullAnswer, + Text = lastResponse + }, + false); break; } } @@ -623,7 +630,7 @@ private async Task ProcessWithToolsAsync( responseMessage.Properties[ToolCallsProperty] = JsonSerializer.Serialize(toolCalls); foreach (var toolCall in toolCalls) - { + { if (chat.Properties.CheckProperty(ServiceConstants.Properties.AgentIdProperty)) { await notificationService.DispatchNotification( @@ -636,13 +643,12 @@ await notificationService.DispatchNotification( var executor = chat.ToolsConfiguration?.GetExecutor(toolCall.Function.Name); - if (executor == null) + if (executor is null) { var errorMessage = $"No executor found for tool: {toolCall.Function.Name}"; throw new InvalidOperationException(errorMessage); } - try { if (requestOptions.ToolCallback is not null) @@ -708,11 +714,14 @@ await requestOptions.ToolCallback.Invoke(new ToolInvocation }; chat.Messages.Add(iterationMessage.MarkProcessed()); - await SendNotification(chat.Id, new LLMTokenValue - { - Type = TokenType.FullAnswer, - Text = errorMessage - }, false); + await SendNotification( + chat.Id, + new LLMTokenValue + { + Type = TokenType.FullAnswer, + Text = errorMessage + }, + false); } return new ChatResult diff --git a/src/MaIN.Services/Services/Steps/BecomeStepHandler.cs b/src/MaIN.Services/Services/Steps/BecomeStepHandler.cs index dda8a6f8..39720b64 100644 --- a/src/MaIN.Services/Services/Steps/BecomeStepHandler.cs +++ b/src/MaIN.Services/Services/Steps/BecomeStepHandler.cs @@ -1,5 +1,6 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Models.Abstract; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; @@ -10,16 +11,15 @@ public class BecomeStepHandler : IStepHandler public string StepName => "BECOME"; public string[] SupportedSteps => ["BECOME", "BECOME*"]; - public async Task Handle(StepContext context) { if (context.StepName == "BECOME*" && context.Chat.Properties.ContainsKey("BECOME*")) { return new StepResult { Chat = context.Chat }; } - + var newBehaviour = context.Arguments[0]; - var messageFilter = context.Agent.Behaviours.GetValueOrDefault(newBehaviour) ?? + var messageFilter = context.Agent.Behaviours.GetValueOrDefault(newBehaviour) ?? context.Agent.Context!.Instruction; if (context.Chat.Properties.TryGetValue("data_filter", out var filterQuery)) @@ -31,22 +31,23 @@ public async Task Handle(StepContext context) context.Agent.CurrentBehaviour = newBehaviour; context.Chat.Messages[0].Content = messageFilter ?? context.Agent.Context!.Instruction!; - + await context.NotifyProgress("true", context.Agent.Id, null, context.Agent.CurrentBehaviour, StepName); + var backend = ModelRegistry.GetById(context.Chat.ModelId).Backend; context.Chat.Messages.Add(new() { Role = "System", Content = $"Now - {messageFilter}", - Properties = new() {{"agent_internal", "true"}}, - Type = context.Chat.Backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM + Properties = new() { { "agent_internal", "true" } }, + Type = backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM }); - + if (context.StepName == "BECOME*") { context.Chat.Properties.Add("BECOME*", string.Empty); } - + return new() { Chat = context.Chat }; } -} \ No newline at end of file +} diff --git a/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs index 4178f6d4..362293aa 100644 --- a/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs @@ -1,8 +1,8 @@ -using System.Text.Json; using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Entities.Agents.Knowledge; using MaIN.Domain.Models; +using MaIN.Domain.Models.Abstract; using MaIN.Services.Constants; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.LLMService; @@ -11,6 +11,7 @@ using MaIN.Services.Services.Models.Commands; using MaIN.Services.Services.Steps.Commands.Abstract; using MaIN.Services.Utils; +using System.Text.Json; namespace MaIN.Services.Services.Steps.Commands; @@ -22,7 +23,7 @@ public class AnswerCommandHandler( MaINSettings settings) : ICommandHandler { - private static readonly JsonSerializerOptions JsonOptions = new() + private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true }; @@ -30,8 +31,11 @@ public class AnswerCommandHandler( public async Task HandleAsync(AnswerCommand command) { ChatResult? result; - var llmService = llmServiceFactory.CreateService(command.Chat.Backend ?? settings.BackendType); - var imageGenService = imageGenServiceFactory.CreateService(command.Chat.Backend ?? settings.BackendType); + var backend = ModelRegistry.TryGetById(command.Chat.ModelId, out var resolvedModel) + ? resolvedModel!.Backend + : settings.BackendType; + var llmService = llmServiceFactory.CreateService(backend); + var imageGenService = imageGenServiceFactory.CreateService(backend); switch (command.KnowledgeUsage) { @@ -54,7 +58,12 @@ public class AnswerCommandHandler( result = command.Chat.ImageGen ? await imageGenService!.Send(command.Chat) : await llmService.Send(command.Chat, - new ChatRequestOptions { InteractiveUpdates = command.Chat.Interactive, TokenCallback = command.Callback, ToolCallback = command.ToolCallback }); + new ChatRequestOptions + { + InteractiveUpdates = command.Chat.Interactive, + TokenCallback = command.Callback, + ToolCallback = command.ToolCallback + }); return result!.Message; } @@ -64,7 +73,7 @@ private async Task ShouldUseKnowledge(Knowledge? knowledge, Chat chat) var originalContent = chat.Messages.Last().Content; var indexAsKnowledge = knowledge?.Index.Items.ToDictionary(x => x.Name, x => x.Tags); - var index = JsonSerializer.Serialize(indexAsKnowledge, JsonOptions); + var index = JsonSerializer.Serialize(indexAsKnowledge, _jsonOptions); chat.InterferenceParams.Grammar = new Grammar(ServiceConstants.Grammars.DecisionGrammar, GrammarFormat.GBNF); chat.Messages.Last().Content = @@ -78,25 +87,28 @@ private async Task ShouldUseKnowledge(Knowledge? knowledge, Chat chat) Content of available knowledge has source tags. Prompt: {originalContent} """; - var service = llmServiceFactory.CreateService(chat.Backend ?? settings.BackendType); + var backend = ModelRegistry.TryGetById(chat.ModelId, out var resolvedModel) + ? resolvedModel!.Backend + : settings.BackendType; + var service = llmServiceFactory.CreateService(backend); var result = await service.Send(chat, new ChatRequestOptions() { SaveConv = false }); - var decision = JsonSerializer.Deserialize(result!.Message.Content, JsonOptions); + var decision = JsonSerializer.Deserialize(result!.Message.Content, _jsonOptions); var decisionValue = decision.GetProperty("decision").GetRawText(); chat.InterferenceParams.Grammar = null; var shouldUseKnowledge = bool.Parse(decisionValue.Trim('"')); chat.Messages.Last().Content = originalContent; - return shouldUseKnowledge!; + return shouldUseKnowledge; } private async Task ProcessKnowledgeQuery(Knowledge? knowledge, Chat chat, string agentId) { var originalContent = chat.Messages.Last().Content; var indexAsKnowledge = knowledge?.Index.Items.ToDictionary(x => x.Name, x => x.Tags); - var index = JsonSerializer.Serialize(indexAsKnowledge, JsonOptions); + var index = JsonSerializer.Serialize(indexAsKnowledge, _jsonOptions); chat.InterferenceParams.Grammar = new Grammar(ServiceConstants.Grammars.KnowledgeGrammar, GrammarFormat.GBNF); chat.Messages.Last().Content = @@ -108,18 +120,20 @@ Find tags that fits user query based on available knowledge (provided to you abo Always return at least 1 tag in array, and no more than 4. Prompt: {originalContent} """; - var llmService = llmServiceFactory.CreateService(chat.Backend ?? settings.BackendType); + var backend = ModelRegistry.TryGetById(chat.ModelId, out var resolvedModel) + ? resolvedModel!.Backend + : settings.BackendType; + var llmService = llmServiceFactory.CreateService(backend); var searchResult = await llmService.Send(chat, new ChatRequestOptions() { SaveConv = false }); - var matchedTags = JsonSerializer.Deserialize>(searchResult!.Message.Content, JsonOptions); + var matchedTags = JsonSerializer.Deserialize>(searchResult!.Message.Content, _jsonOptions); var knowledgeItems = knowledge!.Index.Items - .Where(x => x.Tags.Intersect(matchedTags!).Any() || - matchedTags!.Contains(x.Name)) + .Where(x => x.Tags.Intersect(matchedTags!).Any() || matchedTags!.Contains(x.Name)) .ToList(); - + //NOTE: perhaps good idea for future to combine knowledge form MCP and from KM var memoryOptions = new ChatMemoryOptions(); var mcpConfig = BuildMemoryOptionsFromKnowledgeItems(knowledgeItems, memoryOptions); @@ -127,13 +141,13 @@ Find tags that fits user query based on available knowledge (provided to you abo chat.Messages.Last().Content = $"{originalContent} - Use information given you as memory."; chat.MemoryParams.IncludeQuestionSource = true; chat.MemoryParams.Grammar = null; - + await notificationService.DispatchNotification(NotificationMessageBuilder.CreateActorKnowledgeStepProgress( agentId, - knowledgeItems.Select(x => $" {x.Name}|{x.Type} ").ToList(), + [.. knowledgeItems.Select(x => $" {x.Name}|{x.Type} ")], mcpConfig?.Model ?? chat.ModelId), "ReceiveAgentUpdate"); - - if (mcpConfig != null) + + if (mcpConfig is not null) { var result = await mcpService.Prompt(mcpConfig, chat.Messages); return result.Message; @@ -149,9 +163,9 @@ await notificationService.DispatchNotification(NotificationMessageBuilder.Create { //First or default because we cannot combine response from multiple servers in one go at the moment var mcp = knowledgeItems?.FirstOrDefault(x => x.Type == KnowledgeItemType.Mcp); - if (mcp != null) + if (mcp is not null) { - return JsonSerializer.Deserialize(mcp.Value, JsonOptions); + return JsonSerializer.Deserialize(mcp.Value, _jsonOptions); } foreach (var item in knowledgeItems!) @@ -172,4 +186,4 @@ await notificationService.DispatchNotification(NotificationMessageBuilder.Create return null; } -} \ No newline at end of file +} diff --git a/src/MaIN.Services/Services/Steps/Commands/FetchCommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/FetchCommandHandler.cs index 6ec74959..b23f84a5 100644 --- a/src/MaIN.Services/Services/Steps/Commands/FetchCommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/FetchCommandHandler.cs @@ -1,13 +1,14 @@ +using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Entities.Agents.AgentSource; +using MaIN.Domain.Models.Abstract; using MaIN.Services.Services.Abstract; -using MaIN.Services.Services.Models.Commands; -using System.Text.Json; -using MaIN.Domain.Configuration; using MaIN.Services.Services.LLMService; using MaIN.Services.Services.LLMService.Factory; +using MaIN.Services.Services.Models.Commands; using MaIN.Services.Services.Steps.Commands.Abstract; using MaIN.Services.Utils; +using System.Text.Json; namespace MaIN.Services.Services.Steps.Commands; @@ -19,6 +20,10 @@ public class FetchCommandHandler( { public async Task HandleAsync(FetchCommand command) { + var backend = ModelRegistry.TryGetById(command.Chat.ModelId, out var resolvedModel) + ? resolvedModel!.Backend + : settings.BackendType; + var properties = new Dictionary { { "agent_internal", "true" }, @@ -30,16 +35,16 @@ public class FetchCommandHandler( switch (command.Context.Source!.Type) { case AgentSourceType.File: - response = await HandleFileSource(command, properties); + response = await HandleFileSource(command, properties, backend); break; case AgentSourceType.Web: - response = await HandleWebSource(command, properties); + response = await HandleWebSource(command, properties, backend); break; case AgentSourceType.Text: var textData = dataSourceService.FetchTextData(command.Context.Source.Details); - response = CreateMessage(textData, properties, command.Chat.Backend); + response = CreateMessage(textData, properties, backend); break; case AgentSourceType.API: @@ -48,7 +53,7 @@ public class FetchCommandHandler( command.Filter, httpClientFactory, properties); - response = CreateMessage(apiData, properties, command.Chat.Backend); + response = CreateMessage(apiData, properties, backend); break; case AgentSourceType.SQL: @@ -56,7 +61,7 @@ public class FetchCommandHandler( command.Context.Source.Details, command.Filter, properties); - response = CreateMessage(sqlData, properties, command.Chat.Backend); + response = CreateMessage(sqlData, properties, backend); break; case AgentSourceType.NoSQL: @@ -64,7 +69,7 @@ public class FetchCommandHandler( command.Context.Source.Details, command.Filter, properties); - response = CreateMessage(noSqlData, properties, command.Chat.Backend); + response = CreateMessage(noSqlData, properties, backend); break; default: @@ -74,21 +79,24 @@ public class FetchCommandHandler( // Process JSON response if needed if (response.Properties.ContainsValue("JSON")) { - response = await ProcessJsonResponse(response, command); + response = await ProcessJsonResponse(response, command, backend); } return response; } - private async Task HandleFileSource(FetchCommand command, Dictionary properties) + private async Task HandleFileSource( + FetchCommand command, + Dictionary properties, + BackendType backend) { var fileData = JsonSerializer.Deserialize(command.Context.Source!.Details?.ToString()!); - var filesDictionary = fileData!.Files.ToDictionary( path => Path.GetFileName(path), path => path); - + var filesDictionary = fileData!.Files.ToDictionary(path => Path.GetFileName(path), path => path); + if (command.Chat.Messages.Count > 0) { var memoryChat = command.MemoryChat; - var result = await llmServiceFactory.CreateService(command.Chat.Backend ?? settings.BackendType) + var result = await llmServiceFactory.CreateService(backend) .AskMemory( memoryChat!, new ChatMemoryOptions @@ -102,26 +110,29 @@ private async Task HandleFileSource(FetchCommand command, Dictionary HandleWebSource(FetchCommand command, Dictionary properties) + private async Task HandleWebSource( + FetchCommand command, + Dictionary properties, + BackendType backend) { var webData = JsonSerializer.Deserialize(command.Context.Source!.Details?.ToString()!); if (command.Chat.Messages.Count > 0) { var memoryChat = command.MemoryChat; - var result = await llmServiceFactory.CreateService(command.Chat.Backend ?? settings.BackendType) - .AskMemory(memoryChat!, new ChatMemoryOptions { WebUrls = [webData!.Url] }, new ChatRequestOptions()); + var result = await llmServiceFactory.CreateService(backend) + .AskMemory(memoryChat!, new ChatMemoryOptions { WebUrls = [webData!.Url] }, new ChatRequestOptions()); result!.Message.Role = command.ResponseType == FetchResponseType.AS_System ? "System" : "Assistant"; return result!.Message; } - return CreateMessage($"Web data from {webData!.Url}", properties, command.Chat.Backend); + return CreateMessage($"Web data from {webData!.Url}", properties, backend); } - private async Task ProcessJsonResponse(Message response, FetchCommand command) + private async Task ProcessJsonResponse(Message response, FetchCommand command, BackendType backend) { var chunker = new JsonChunker(); var chunksAsList = chunker.ChunkJson(response.Content).ToList(); @@ -129,31 +140,36 @@ private async Task ProcessJsonResponse(Message response, FetchCommand c .Select((chunk, index) => new { Key = $"CHUNK_{index + 1}-{chunksAsList.Count}", Value = chunk }) .ToDictionary(item => item.Key, item => item.Value); - var result = await llmServiceFactory.CreateService(command.Chat.Backend ?? settings.BackendType).AskMemory(command.MemoryChat!, new ChatMemoryOptions - { - TextData = chunks - }, new ChatRequestOptions()); + var result = await llmServiceFactory + .CreateService(backend) + .AskMemory( + command.MemoryChat!, + new ChatMemoryOptions + { + TextData = chunks + }, + new ChatRequestOptions()); result!.Message.Role = command.ResponseType == FetchResponseType.AS_System ? "System" : "Assistant"; var newMessage = result!.Message; newMessage.Properties = new() { { "agent_internal", "true" }, - { Message.UnprocessedMessageProperty, string.Empty} + { Message.UnprocessedMessageProperty, string.Empty } }; return newMessage; } - private static Message CreateMessage(string content, + private static Message CreateMessage(string content, Dictionary properties, - BackendType? chatBackend) + BackendType backend) { return new Message { Content = content, Role = "System", Properties = properties, - Type = chatBackend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM + Type = backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM }; } -} \ No newline at end of file +} diff --git a/src/MaIN.Services/Services/Steps/Commands/RedirectCommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/RedirectCommandHandler.cs index 6d9f6086..23b194f1 100644 --- a/src/MaIN.Services/Services/Steps/Commands/RedirectCommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/RedirectCommandHandler.cs @@ -1,5 +1,6 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Models.Abstract; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models.Commands; using MaIN.Services.Services.Steps.Commands.Abstract; @@ -11,6 +12,7 @@ public class RedirectCommandHandler(IAgentService agentService) : ICommandHandle public async Task HandleAsync(RedirectCommand command) { var chat = await agentService.GetChatByAgent(command.RelatedAgentId); + var backend = ModelRegistry.GetById(chat.ModelId).Backend; chat.Messages.Add(new Message() { Role = "User", @@ -20,7 +22,7 @@ public class RedirectCommandHandler(IAgentService agentService) : ICommandHandle { "agent_internal", "true" }, { Message.UnprocessedMessageProperty, string.Empty} }, - Type = chat.Backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM + Type = backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM }); if (!string.IsNullOrEmpty(command.Filter)) @@ -35,7 +37,7 @@ public class RedirectCommandHandler(IAgentService agentService) : ICommandHandle Content = result.Messages.Last().Content, Image = result.Messages.Last().Image, Role = "System", - Type = chat.Backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM, + Type = backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM, Properties = new Dictionary() { { "agent_internal", "true" }, @@ -43,4 +45,4 @@ public class RedirectCommandHandler(IAgentService agentService) : ICommandHandle } }; } -} \ No newline at end of file +} diff --git a/src/MaIN.Services/Services/Steps/Commands/StartCommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/StartCommandHandler.cs index 436f2931..4293e2f8 100644 --- a/src/MaIN.Services/Services/Steps/Commands/StartCommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/StartCommandHandler.cs @@ -1,5 +1,6 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Models.Abstract; using MaIN.Services.Services.Models.Commands; using MaIN.Services.Services.Steps.Commands.Abstract; @@ -14,14 +15,15 @@ public class StartCommandHandler : ICommandHandler return Task.FromResult(null); } + var backend = ModelRegistry.GetById(command.Chat.ModelId).Backend; var message = new Message() { Content = command.InitialPrompt!, - Type = command.Chat.Backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM, + Type = backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM, Role = "System" }; command.Chat.Messages.Add(message); - + return Task.FromResult(new Message() { Content = "STARTED", @@ -30,4 +32,4 @@ public class StartCommandHandler : ICommandHandler Time = DateTime.UtcNow })!; } -} \ No newline at end of file +} diff --git a/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs b/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs index 28af3856..30eff706 100644 --- a/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs +++ b/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs @@ -1,6 +1,7 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Exceptions; +using MaIN.Domain.Models.Abstract; using MaIN.Services.Mappers; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; @@ -14,7 +15,7 @@ public class FetchDataStepHandler( { public string StepName => "FETCH_DATA"; public string[] SupportedSteps => ["FETCH_DATA", "FETCH_DATA*"]; - + public async Task Handle(StepContext context) { var respondAsSystem = context.Arguments.Contains("AS_SYSTEM"); @@ -33,45 +34,43 @@ public async Task Handle(StepContext context) MemoryChat = CreateMemoryChat(context, filter) }; - var response = await commandDispatcher.DispatchAsync(fetchCommand); - if (response == null) - { - throw new CommandFailedException(fetchCommand.CommandName); - } + var response = await commandDispatcher.DispatchAsync(fetchCommand) + ?? throw new CommandFailedException(fetchCommand.CommandName); if (context.StepName == "FETCH_DATA*") { context.Chat.Properties["FETCH_DATA*"] = string.Empty; } - + context.Chat.Messages.Add(response); - - return new StepResult { - Chat = context.Chat, - RedirectMessage = context.Chat.Messages.Last() + + return new StepResult + { + Chat = context.Chat, + RedirectMessage = context.Chat.Messages.Last() }; } - + private static Chat CreateMemoryChat(StepContext context, string? filterVal) { + var backend = ModelRegistry.GetById(context.Chat.ModelId).Backend; return new Chat { - Messages = new List - { + Messages = + [ new() { Content = context.Agent.Behaviours[context.Agent.CurrentBehaviour].Replace("@filter@", filterVal ?? string.Empty), - Type = context.Chat.Backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM, + Type = backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM, Role = "User" } - }, + ], ModelId = context.Chat.ModelId, Properties = context.Chat.Properties, MemoryParams = context.Chat.MemoryParams, InterferenceParams = context.Chat.InterferenceParams, - Backend = context.Chat.Backend, Name = "Memory Chat", Id = Guid.NewGuid().ToString() }; } -} \ No newline at end of file +} From 36396dfb9d8b5b4d3b463ceb7e1749e481c319c7 Mon Sep 17 00:00:00 2001 From: srebrek Date: Wed, 4 Mar 2026 18:01:11 +0100 Subject: [PATCH 2/9] Remove WithModel(AIModel) and WithModel WithModel(AIModel) and WithModel makes more questions than answers. It gives user one clear workflow. Add this flow with register as an example." --- Examples/Examples/Agents/AgentExample.cs | 7 +- .../Examples/Chat/ChatCustomGrammarExample.cs | 20 ++-- Examples/Examples/Chat/ChatExample.cs | 6 +- .../Examples/Chat/ChatExampleAnthropic.cs | 6 +- Examples/Examples/Chat/ChatExampleGemini.cs | 15 +-- .../Examples/Chat/ChatExampleGroqCloud.cs | 6 +- Examples/Examples/Chat/ChatExampleOllama.cs | 6 +- Examples/Examples/Chat/ChatExampleOpenAi.cs | 12 +-- .../Examples/Chat/ChatExampleToolsSimple.cs | 6 +- .../Chat/ChatExampleToolsSimpleLocalLLM.cs | 6 +- Examples/Examples/Chat/ChatExampleXai.cs | 6 +- .../Examples/Chat/ChatFromExistingExample.cs | 13 ++- .../Examples/Chat/ChatGrammarExampleGemini.cs | 53 +++++---- .../Chat/ChatWithCustomModelIdExample.cs | 29 +++++ .../Examples/Chat/ChatWithFilesExample.cs | 10 +- .../Chat/ChatWithFilesExampleGemini.cs | 6 +- .../Chat/ChatWithFilesFromStreamExample.cs | 20 ++-- .../Examples/Chat/ChatWithImageGenExample.cs | 10 +- .../Chat/ChatWithImageGenGeminiExample.cs | 6 +- .../Chat/ChatWithImageGenOpenAiExample.cs | 10 +- .../Chat/ChatWithReasoningDeepSeekExample.cs | 6 +- .../Examples/Chat/ChatWithReasoningExample.cs | 6 +- .../Chat/ChatWithTextToSpeechExample.cs | 14 +-- .../Examples/Chat/ChatWithVisionExample.cs | 6 +- Examples/Examples/Program.cs | 8 +- MaIN.Core.IntegrationTests/ChatTests.cs | 62 ++++++----- src/MaIN.Core/Hub/Contexts/ChatContext.cs | 27 +---- .../ChatContext/IChatBuilderEntryPoint.cs | 9 -- src/MaIN.Domain/Models/Abstract/AIModel.cs | 39 ++++--- src/MaIN.Domain/Models/Models.cs | 102 ++++++++++++++++++ .../Components/Pages/Home.razor | 5 +- 31 files changed, 320 insertions(+), 217 deletions(-) create mode 100644 Examples/Examples/Chat/ChatWithCustomModelIdExample.cs create mode 100644 src/MaIN.Domain/Models/Models.cs diff --git a/Examples/Examples/Agents/AgentExample.cs b/Examples/Examples/Agents/AgentExample.cs index 3678efad..3bf8efdf 100644 --- a/Examples/Examples/Agents/AgentExample.cs +++ b/Examples/Examples/Agents/AgentExample.cs @@ -1,4 +1,5 @@ using MaIN.Core.Hub; +using MaIN.Domain.Models; namespace Examples.Agents; @@ -24,13 +25,13 @@ has the best possible counsel as she seeks to reclaim the Iron Throne. """; var context = AIHub.Agent() - .WithModel("llama3.2:3b") + .WithModel(Models.Local.Llama3_2_3b) .WithInitialPrompt(systemPrompt) .Create(); - + var result = await context .ProcessAsync("Where is the Iron Throne located? I need this information for Lady Princess"); Console.WriteLine(result.Message.Content); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatCustomGrammarExample.cs b/Examples/Examples/Chat/ChatCustomGrammarExample.cs index 7863fd7c..ca655806 100644 --- a/Examples/Examples/Chat/ChatCustomGrammarExample.cs +++ b/Examples/Examples/Chat/ChatCustomGrammarExample.cs @@ -1,7 +1,6 @@ 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; @@ -13,16 +12,17 @@ public async Task Start() Console.WriteLine("ChatExample with grammar is running!"); var personGrammar = new Grammar(""" - root ::= person - person ::= "{" ws "\"name\":" ws name "," ws "\"age\":" ws age "," ws "\"city\":" ws city ws "}" - name ::= "\"" [A-Za-z ]+ "\"" - age ::= [1-9] | [1-9][0-9] - city ::= "\"" [A-Za-z ]+ "\"" - ws ::= [ \t]* - """, GrammarFormat.GBNF); + root ::= person + person ::= "{" ws "\"name\":" ws name "," ws "\"age\":" ws age "," ws "\"city\":" ws city ws "}" + name ::= "\"" [A-Za-z ]+ "\"" + age ::= [1-9] | [1-9][0-9] + city ::= "\"" [A-Za-z ]+ "\"" + ws ::= [ \t]* + """, + GrammarFormat.GBNF); await AIHub.Chat() - .WithModel() + .WithModel(Models.Local.Gemma2_2b) .WithMessage("Generate random person") .WithInferenceParams(new InferenceParams { @@ -30,4 +30,4 @@ await AIHub.Chat() }) .CompleteAsync(interactive: true); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatExample.cs b/Examples/Examples/Chat/ChatExample.cs index 989cac6f..bec53fe9 100644 --- a/Examples/Examples/Chat/ChatExample.cs +++ b/Examples/Examples/Chat/ChatExample.cs @@ -1,5 +1,5 @@ using MaIN.Core.Hub; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -11,9 +11,9 @@ public async Task Start() // Using strongly-typed model await AIHub.Chat() - .WithModel() + .WithModel(Models.Local.Gemma2_2b) .EnsureModelDownloaded() .WithMessage("Where do hedgehogs goes at night?") .CompleteAsync(interactive: true); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatExampleAnthropic.cs b/Examples/Examples/Chat/ChatExampleAnthropic.cs index 851580f9..ef058e3f 100644 --- a/Examples/Examples/Chat/ChatExampleAnthropic.cs +++ b/Examples/Examples/Chat/ChatExampleAnthropic.cs @@ -1,6 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -12,8 +12,8 @@ public async Task Start() Console.WriteLine("(Anthropic) ChatExample is running!"); await AIHub.Chat() - .WithModel() + .WithModel(Models.Anthropic.ClaudeSonnet4) .WithMessage("Write a haiku about programming on Monday morning.") .CompleteAsync(interactive: true); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatExampleGemini.cs b/Examples/Examples/Chat/ChatExampleGemini.cs index 8d7e9e30..b28cc3ff 100644 --- a/Examples/Examples/Chat/ChatExampleGemini.cs +++ b/Examples/Examples/Chat/ChatExampleGemini.cs @@ -1,8 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; -using MaIN.Domain.Configuration; -using MaIN.Domain.Models.Abstract; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -13,17 +11,8 @@ 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(customModel) + .WithModel(Models.Gemini.Gemini2_5Flash) .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 69df1ddc..60683829 100644 --- a/Examples/Examples/Chat/ChatExampleGroqCloud.cs +++ b/Examples/Examples/Chat/ChatExampleGroqCloud.cs @@ -1,6 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -12,8 +12,8 @@ public async Task Start() Console.WriteLine("(GroqCloud) ChatExample is running!"); await AIHub.Chat() - .WithModel() + .WithModel(Models.Groq.Llama3_1_8bInstant) .WithMessage("Which color do people like the most?") .CompleteAsync(interactive: true); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatExampleOllama.cs b/Examples/Examples/Chat/ChatExampleOllama.cs index 08dd1ba1..7123c96d 100644 --- a/Examples/Examples/Chat/ChatExampleOllama.cs +++ b/Examples/Examples/Chat/ChatExampleOllama.cs @@ -1,6 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -12,8 +12,8 @@ public async Task Start() Console.WriteLine("(Ollama) ChatExample is running!"); await AIHub.Chat() - .WithModel() + .WithModel(Models.Ollama.Gemma3_4b) .WithMessage("Write a short poem about the color green.") .CompleteAsync(interactive: true); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatExampleOpenAi.cs b/Examples/Examples/Chat/ChatExampleOpenAi.cs index da03637a..d241edc1 100644 --- a/Examples/Examples/Chat/ChatExampleOpenAi.cs +++ b/Examples/Examples/Chat/ChatExampleOpenAi.cs @@ -1,6 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -9,12 +9,12 @@ public class ChatExampleOpenAi : IExample public async Task Start() { OpenAiExample.Setup(); //We need to provide OpenAi API key - - Console.WriteLine("(OpenAi) ChatExample is running!"); - + + Console.WriteLine("(OpenAi) ChatExample is running!"); + await AIHub.Chat() - .WithModel() + .WithModel(Models.OpenAi.Gpt5Nano) .WithMessage("What do you consider to be the greatest invention in history?") .CompleteAsync(interactive: true); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatExampleToolsSimple.cs b/Examples/Examples/Chat/ChatExampleToolsSimple.cs index f4fa8215..ddece522 100644 --- a/Examples/Examples/Chat/ChatExampleToolsSimple.cs +++ b/Examples/Examples/Chat/ChatExampleToolsSimple.cs @@ -1,7 +1,7 @@ using Examples.Utils; using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -14,13 +14,13 @@ public async Task Start() Console.WriteLine("(OpenAi) ChatExample with tools is running!"); await AIHub.Chat() - .WithModel() + .WithModel(Models.OpenAi.Gpt5Nano) .WithMessage("What time is it right now?") .WithTools(new ToolsConfigurationBuilder() .AddTool( name: "get_current_time", description: "Get the current date and time", - execute: Tools.GetCurrentTime) + execute: Tools.GetCurrentTime) .WithToolChoice("auto") .Build()) .CompleteAsync(interactive: true); diff --git a/Examples/Examples/Chat/ChatExampleToolsSimpleLocalLLM.cs b/Examples/Examples/Chat/ChatExampleToolsSimpleLocalLLM.cs index 479cc1e8..cccbc8d7 100644 --- a/Examples/Examples/Chat/ChatExampleToolsSimpleLocalLLM.cs +++ b/Examples/Examples/Chat/ChatExampleToolsSimpleLocalLLM.cs @@ -1,7 +1,7 @@ using Examples.Utils; using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -12,7 +12,7 @@ public async Task Start() Console.WriteLine("Local LLM ChatExample with tools is running!"); await AIHub.Chat() - .WithModel() + .WithModel(Models.Local.Gemma3_4b) .WithMessage("What time is it right now?") .WithTools(new ToolsConfigurationBuilder() .AddTool( @@ -23,4 +23,4 @@ await AIHub.Chat() .Build()) .CompleteAsync(interactive: true); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatExampleXai.cs b/Examples/Examples/Chat/ChatExampleXai.cs index be7d474c..f5e607d7 100644 --- a/Examples/Examples/Chat/ChatExampleXai.cs +++ b/Examples/Examples/Chat/ChatExampleXai.cs @@ -1,6 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -12,8 +12,8 @@ public async Task Start() Console.WriteLine("(xAI) ChatExample is running!"); await AIHub.Chat() - .WithModel() + .WithModel(Models.Xai.Grok3Beta) .WithMessage("Is the killer whale cute?") .CompleteAsync(interactive: true); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatFromExistingExample.cs b/Examples/Examples/Chat/ChatFromExistingExample.cs index 6a14961d..cdc0738a 100644 --- a/Examples/Examples/Chat/ChatFromExistingExample.cs +++ b/Examples/Examples/Chat/ChatFromExistingExample.cs @@ -1,7 +1,7 @@ -using System.Text.Json; using MaIN.Core.Hub; using MaIN.Domain.Exceptions.Chats; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; +using System.Text.Json; namespace Examples.Chat; @@ -12,11 +12,11 @@ public async Task Start() Console.WriteLine("ChatExample with files is running!"); var result = AIHub.Chat() - .WithModel(); - + .WithModel(Models.Local.Qwen2_5_0_5b); + await result.WithMessage("What do you think about math theories?") .CompleteAsync(); - + await result.WithMessage("And about physics?") .CompleteAsync(); @@ -30,6 +30,5 @@ await result.WithMessage("And about physics?") { Console.WriteLine(ex.PublicErrorMessage); } - } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatGrammarExampleGemini.cs b/Examples/Examples/Chat/ChatGrammarExampleGemini.cs index eea49de8..ac9234f9 100644 --- a/Examples/Examples/Chat/ChatGrammarExampleGemini.cs +++ b/Examples/Examples/Chat/ChatGrammarExampleGemini.cs @@ -2,7 +2,6 @@ using MaIN.Core.Hub; using MaIN.Domain.Entities; using MaIN.Domain.Models; -using MaIN.Domain.Models.Concrete; namespace Examples.Chat; @@ -14,37 +13,37 @@ public async Task Start() Console.WriteLine("(Gemini) ChatExample is running!"); var grammarValue = """ - { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "User", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Full name of the user." - }, - "age": { - "type": "integer", - "minimum": 0, - "description": "User's age in years." - }, - "email": { - "type": "string", - "format": "email", - "description": "User's email address." - } - }, - "required": ["name", "email"] - } - """; + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "User", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Full name of the user." + }, + "age": { + "type": "integer", + "minimum": 0, + "description": "User's age in years." + }, + "email": { + "type": "string", + "format": "email", + "description": "User's email address." + } + }, + "required": ["name", "email"] + } + """; await AIHub.Chat() - .WithModel() + .WithModel(Models.Gemini.Gemini2_5Flash) .WithMessage("Generate random person") .WithInferenceParams(new InferenceParams { - Grammar = new Grammar(grammarValue, GrammarFormat.JSONSchema) + Grammar = new Grammar(grammarValue, GrammarFormat.JSONSchema) }) .CompleteAsync(interactive: true); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatWithCustomModelIdExample.cs b/Examples/Examples/Chat/ChatWithCustomModelIdExample.cs new file mode 100644 index 00000000..cce7becb --- /dev/null +++ b/Examples/Examples/Chat/ChatWithCustomModelIdExample.cs @@ -0,0 +1,29 @@ +using MaIN.Core.Hub; +using MaIN.Domain.Models.Abstract; + +namespace Examples.Chat; + +public class ChatWithCustomModelIdExample : IExample +{ + public async Task Start() + { + Console.WriteLine("ChatWithCustomModelId is running!"); + + // Register an existing model file under a custom ID with tailored configuration. + // The same Gemma2-2b.gguf file, but exposed as a named role alias. + var writingAssistant = new GenericLocalModel( + "Gemma2-2b.gguf", + "Writing Assistant", + "writing-assistant", + SystemMessage: "You are a creative writing assistant. Always respond with vivid, expressive language." + ); + + ModelRegistry.Register(writingAssistant); + + await AIHub.Chat() + .WithModel("writing-assistant") + .EnsureModelDownloaded() + .WithMessage("Write a one-sentence opening to a mystery story.") + .CompleteAsync(interactive: true); + } +} diff --git a/Examples/Examples/Chat/ChatWithFilesExample.cs b/Examples/Examples/Chat/ChatWithFilesExample.cs index 9be491d6..f113c91a 100644 --- a/Examples/Examples/Chat/ChatWithFilesExample.cs +++ b/Examples/Examples/Chat/ChatWithFilesExample.cs @@ -1,5 +1,5 @@ using MaIN.Core.Hub; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -13,15 +13,15 @@ public async Task Start() Path.Combine(AppContext.BaseDirectory, "Files", "Nicolaus_Copernicus.pdf"), Path.Combine(AppContext.BaseDirectory, "Files", "Galileo_Galilei.pdf"), ]; - + var result = await AIHub.Chat() - .WithModel() + .WithModel(Models.Local.Gemma3_4b) .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() .CompleteAsync(); - + Console.WriteLine(result.Message.Content); Console.ReadKey(); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatWithFilesExampleGemini.cs b/Examples/Examples/Chat/ChatWithFilesExampleGemini.cs index 4e712d41..f7e278c7 100644 --- a/Examples/Examples/Chat/ChatWithFilesExampleGemini.cs +++ b/Examples/Examples/Chat/ChatWithFilesExampleGemini.cs @@ -1,6 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -14,11 +14,11 @@ public async Task Start() List files = ["./Files/Nicolaus_Copernicus.pdf", "./Files/Galileo_Galilei.pdf"]; await AIHub.Chat() - .WithModel() + .WithModel(Models.Gemini.Gemini2_5Flash) .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); Console.ReadKey(); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatWithFilesFromStreamExample.cs b/Examples/Examples/Chat/ChatWithFilesFromStreamExample.cs index 1643cf61..f1c96789 100644 --- a/Examples/Examples/Chat/ChatWithFilesFromStreamExample.cs +++ b/Examples/Examples/Chat/ChatWithFilesFromStreamExample.cs @@ -1,5 +1,5 @@ -using MaIN.Core.Hub; -using MaIN.Domain.Models.Concrete; +using MaIN.Core.Hub; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -14,20 +14,20 @@ public async Task Start() Path.Combine(AppContext.BaseDirectory, "Files", "Nicolaus_Copernicus.pdf"), Path.Combine(AppContext.BaseDirectory, "Files", "Galileo_Galilei.pdf"), ]; - + var fileStreams = new List(); - + foreach (var path in files) { if (File.Exists(path)) { // Open file with read access - FileStream fs = new FileStream( + FileStream fs = new( path, FileMode.Open, FileAccess.Read, FileShare.Read); - + fileStreams.Add(fs); Console.WriteLine($"Loaded: {path}"); } @@ -36,14 +36,14 @@ public async Task Start() Console.WriteLine($"File not found: {path}"); } } - + var result = await AIHub.Chat() - .WithModel() + .WithModel(Models.Local.Qwen2_5_0_5b) .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(); - + Console.WriteLine(result.Message.Content); Console.ReadKey(); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatWithImageGenExample.cs b/Examples/Examples/Chat/ChatWithImageGenExample.cs index d8923d42..20dd3024 100644 --- a/Examples/Examples/Chat/ChatWithImageGenExample.cs +++ b/Examples/Examples/Chat/ChatWithImageGenExample.cs @@ -9,12 +9,14 @@ public class ChatWithImageGenExample : IExample public async Task Start() { Console.WriteLine("ChatExample with image gen is running!"); - + + var fluxModel = new GenericLocalModel("FLUX.1_Shnell"); + ModelRegistry.RegisterOrReplace(fluxModel); var result = await AIHub.Chat() - .WithModel(new GenericLocalModel("FLUX.1_Shnell"), imageGen: true) + .WithModel(fluxModel.Id) .WithMessage("Generate cyberpunk godzilla cat warrior") .CompleteAsync(); - + ImagePreview.ShowImage(result.Message.Image); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatWithImageGenGeminiExample.cs b/Examples/Examples/Chat/ChatWithImageGenGeminiExample.cs index c037d2dc..b00d75e1 100644 --- a/Examples/Examples/Chat/ChatWithImageGenGeminiExample.cs +++ b/Examples/Examples/Chat/ChatWithImageGenGeminiExample.cs @@ -12,11 +12,13 @@ public async Task Start() Console.WriteLine("ChatExample with image gen is running! (Gemini)"); GeminiExample.Setup(); // We need to provide Gemini API key + var imagenModel = new GenericImageGenerationCloudModel("imagen-3", BackendType.Gemini); + ModelRegistry.RegisterOrReplace(imagenModel); var result = await AIHub.Chat() - .WithModel(new GenericCloudModel("imagen-3", BackendType.Gemini), imageGen: true) + .WithModel(imagenModel.Id) .WithMessage("Generate hamster as a astronaut on the moon") .CompleteAsync(); ImagePreview.ShowImage(result.Message.Image); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs b/Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs index c7ecba3e..b651fcd8 100644 --- a/Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs +++ b/Examples/Examples/Chat/ChatWithImageGenOpenAiExample.cs @@ -1,6 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -10,12 +10,12 @@ public async Task Start() { Console.WriteLine("ChatExample with image gen is running! (OpenAi)"); OpenAiExample.Setup(); // We need to provide OpenAi API key - + var result = await AIHub.Chat() - .WithModel() + .WithModel(Models.OpenAi.DallE3) .WithMessage("Generate rock style cow playing guitar") .CompleteAsync(); - + ImagePreview.ShowImage(result.Message.Image); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatWithReasoningDeepSeekExample.cs b/Examples/Examples/Chat/ChatWithReasoningDeepSeekExample.cs index 7fc4b1fa..71836725 100644 --- a/Examples/Examples/Chat/ChatWithReasoningDeepSeekExample.cs +++ b/Examples/Examples/Chat/ChatWithReasoningDeepSeekExample.cs @@ -1,6 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -12,8 +12,8 @@ public async Task Start() Console.WriteLine("(DeepSeek) ChatExample with reasoning is running!"); await AIHub.Chat() - .WithModel() // a model that supports reasoning + .WithModel(Models.DeepSeek.Reasoner) // a model that supports reasoning .WithMessage("What chill pc game do you recommend?") .CompleteAsync(interactive: true); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatWithReasoningExample.cs b/Examples/Examples/Chat/ChatWithReasoningExample.cs index d233461d..de8b21e0 100644 --- a/Examples/Examples/Chat/ChatWithReasoningExample.cs +++ b/Examples/Examples/Chat/ChatWithReasoningExample.cs @@ -1,5 +1,5 @@ using MaIN.Core.Hub; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -10,8 +10,8 @@ public async Task Start() Console.WriteLine("ChatWithReasoningExample is running!"); await AIHub.Chat() - .WithModel() + .WithModel(Models.Local.DeepSeekR1_1_5b) .WithMessage("Think about greatest poet of all time") .CompleteAsync(interactive: true); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatWithTextToSpeechExample.cs b/Examples/Examples/Chat/ChatWithTextToSpeechExample.cs index 2486362b..c5f248bc 100644 --- a/Examples/Examples/Chat/ChatWithTextToSpeechExample.cs +++ b/Examples/Examples/Chat/ChatWithTextToSpeechExample.cs @@ -1,26 +1,26 @@ -using MaIN.Core.Hub; +using MaIN.Core.Hub; using MaIN.Domain.Entities; +using MaIN.Domain.Models; using MaIN.Domain.Models.Concrete; using MaIN.Services.Services.TTSService; -#pragma warning disable CS0618 // Type or member is obsolete namespace Examples.Chat; public class ChatWithTextToSpeechExample : IExample { private const string VoicePath = ""; - + public async Task Start() { Console.WriteLine("ChatWithTextToSpeech is running! Put on your headphones and press any key."); Console.ReadKey(); - + VoiceService.SetVoicesPath(VoicePath); var voice = VoiceService.GetVoice("af_heart") .MixWith(VoiceService.GetVoice("bf_emma")); - + await AIHub.Chat() - .WithModel() + .WithModel(Models.Local.Gemma2_2b) .WithMessage("Generate a 4 sentence poem.") .Speak(new TextToSpeechParams(new Kokoro_82m(), voice, true)) .CompleteAsync(interactive: true); @@ -28,4 +28,4 @@ await AIHub.Chat() Console.WriteLine("Done!"); Console.ReadKey(); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatWithVisionExample.cs b/Examples/Examples/Chat/ChatWithVisionExample.cs index 5259bd82..9cb537fd 100644 --- a/Examples/Examples/Chat/ChatWithVisionExample.cs +++ b/Examples/Examples/Chat/ChatWithVisionExample.cs @@ -1,5 +1,5 @@ using MaIN.Core.Hub; -using MaIN.Domain.Models.Concrete; +using MaIN.Domain.Models; namespace Examples.Chat; @@ -14,8 +14,8 @@ public async Task Start() Path.Combine(AppContext.BaseDirectory, "Files", "gamex.jpg")); await AIHub.Chat() - .WithModel() + .WithModel(Models.Local.Llava16Mistral_7b) .WithMessage("What can you see on the image?", image) .CompleteAsync(interactive: true); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Program.cs b/Examples/Examples/Program.cs index 627261c1..7283210b 100644 --- a/Examples/Examples/Program.cs +++ b/Examples/Examples/Program.cs @@ -1,4 +1,4 @@ -using Examples; +using Examples; using Examples.Agents; using Examples.Agents.Flows; using Examples.Chat; @@ -18,7 +18,6 @@ ╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗ Interactive Example Runner v1.0 "; - Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine(Banner); Console.ResetColor(); @@ -80,6 +79,7 @@ static void RegisterExamples(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } async Task RunSelectedExample(IServiceProvider serviceProvider) @@ -147,7 +147,6 @@ async Task RunSelectedExample(IServiceProvider serviceProvider) } } - namespace Examples { public class ExampleRegistry(IServiceProvider serviceProvider) @@ -195,7 +194,8 @@ public class ExampleRegistry(IServiceProvider serviceProvider) ("\u25a0 McpClient example", serviceProvider.GetRequiredService()), ("\u25a0 McpAgent example", serviceProvider.GetRequiredService()), ("\u25a0 Chat with TTS example", serviceProvider.GetRequiredService()), - ("\u25a0 McpAgent example", serviceProvider.GetRequiredService()) + ("\u25a0 McpAgent example", serviceProvider.GetRequiredService()), + ("\u25a0 Chat with custom model ID", serviceProvider.GetRequiredService()) ]; } }; diff --git a/MaIN.Core.IntegrationTests/ChatTests.cs b/MaIN.Core.IntegrationTests/ChatTests.cs index a98f8ce2..73130600 100644 --- a/MaIN.Core.IntegrationTests/ChatTests.cs +++ b/MaIN.Core.IntegrationTests/ChatTests.cs @@ -1,9 +1,9 @@ -using FuzzySharp; +using FuzzySharp; using MaIN.Core.Hub; using MaIN.Core.IntegrationTests.Helpers; using MaIN.Domain.Entities; +using MaIN.Domain.Models; using MaIN.Domain.Models.Abstract; -using MaIN.Domain.Models.Concrete; namespace MaIN.Core.IntegrationTests; @@ -12,12 +12,12 @@ public class ChatTests : IntegrationTestBase public ChatTests() : base() { } - + [Fact] public async Task Should_AnswerQuestion_BasicChat() { - var context = AIHub.Chat().WithModel(); - + var context = AIHub.Chat().WithModel(Models.Local.Gemma2_2b); + var result = await context .WithMessage("Where the hedgehog goes at night?") .CompleteAsync(interactive: true); @@ -31,13 +31,13 @@ public async Task Should_AnswerQuestion_BasicChat() public async Task Should_AnswerDifferences_BetweenDocuments_ChatWithFiles() { List files = ["./Files/Nicolaus_Copernicus.pdf", "./Files/Galileo_Galilei.pdf"]; - + var result = await AIHub.Chat() - .WithModel() + .WithModel(Models.Local.Gemma2_2b) .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(); - + Assert.True(result.Done); Assert.NotNull(result.Message); Assert.NotEmpty(result.Message.Content); @@ -47,12 +47,12 @@ public async Task Should_AnswerDifferences_BetweenDocuments_ChatWithFiles() public async Task Should_AnswerQuestion_FromExistingChat() { var result = AIHub.Chat() - .WithModel(); - + .WithModel(Models.Local.Qwen2_5_0_5b); + await result.WithMessage("What do you think about math theories?") .CompleteAsync(); - - + + await result.WithMessage("And about physics?") .CompleteAsync(); @@ -66,9 +66,9 @@ await result.WithMessage("And about physics?") public async Task Should_AnswerGameFromImage_ChatWithVision() { List images = ["./Files/gamex.jpg"]; - + var result = await AIHub.Chat() - .WithModel() + .WithModel(Models.Local.Llama3_2_3b) .WithMessage("What is the title of the game? Answer only this question.") .WithMemoryParams(new MemoryParams { @@ -76,7 +76,7 @@ public async Task Should_AnswerGameFromImage_ChatWithVision() }) .WithFiles(images) .CompleteAsync(); - + Assert.True(result.Done); Assert.NotNull(result.Message); Assert.NotEmpty(result.Message.Content); @@ -94,50 +94,56 @@ Fuzzy match failed! public async Task Should_GenerateImage_BasedOnPrompt() { Assert.True(NetworkHelper.PingHost("127.0.0.1", 5003, 5), "Please make sure ImageGen service is running on port 5003"); - + const string extension = "png"; - + + var fluxModel = new GenericLocalModel("FLUX.1_Shnell"); + ModelRegistry.RegisterOrReplace(fluxModel); var result = await AIHub.Chat() - .WithModel(new GenericLocalModel("FLUX.1_Shnell"), imageGen: true) + .WithModel(fluxModel.Id) .WithMessage("Generate cat in Rome. Sightseeing, colloseum, ancient builidngs, Italy.") .CompleteAsync(); if (string.IsNullOrWhiteSpace(extension) || extension.Contains(".")) + { throw new ArgumentException("Invalid file extension"); + } Assert.True(result.Done); Assert.NotNull(result.Message.Image); } - + [Fact] public async Task Should_AnswerDifferences_BetweenDocuments_ChatWithFiles_UsingStreams() { List files = ["./Files/Nicolaus_Copernicus.pdf", "./Files/Galileo_Galilei.pdf"]; - + var fileStreams = new List(); - + foreach (var path in files) { - if (!File.Exists(path)) + if (!File.Exists(path)) + { continue; - + } + var fs = new FileStream( path, FileMode.Open, FileAccess.Read, FileShare.Read); - + fileStreams.Add(fs); } - + var result = await AIHub.Chat() - .WithModel() + .WithModel(Models.Local.Gemma2_2b) .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(); - + Assert.True(result.Done); Assert.NotNull(result.Message); Assert.NotEmpty(result.Message.Content); } -} \ No newline at end of file +} diff --git a/src/MaIN.Core/Hub/Contexts/ChatContext.cs b/src/MaIN.Core/Hub/Contexts/ChatContext.cs index 6edba5eb..4eb67d04 100644 --- a/src/MaIN.Core/Hub/Contexts/ChatContext.cs +++ b/src/MaIN.Core/Hub/Contexts/ChatContext.cs @@ -39,24 +39,6 @@ internal ChatContext(IChatService chatService, Chat existingChat) _chat = existingChat; } - public IChatMessageBuilder WithModel(AIModel model, bool? imageGen = null) - { - ModelRegistry.RegisterOrReplace(model); - _chat.ModelId = model.Id; - _chat.ImageGen = imageGen ?? (model is IImageGenerationModel); - _chat.ImageGen = model.HasImageGeneration; - return this; - } - - [Obsolete("Use WithModel(string modelId) or WithModel(AIModel model) instead.")] - public IChatMessageBuilder WithModel() where TModel : AIModel, new() - { - var model = new TModel(); - ModelRegistry.RegisterOrReplace(model); - _chat.ModelId = model.Id; - return this; - } - public IChatMessageBuilder WithModel(string modelId) { if (!ModelRegistry.Exists(modelId)) @@ -65,14 +47,7 @@ public IChatMessageBuilder WithModel(string modelId) } _chat.ModelId = modelId; - return this; - } - - [Obsolete("Use WithModel(AIModel model) instead.")] - public IChatMessageBuilder WithCustomModel(string model, string path, string? mmProject = null) - { - KnownModels.AddModel(model, path, mmProject); - _chat.ModelId = model; + _chat.ImageGen = ModelRegistry.GetById(modelId).HasImageGeneration; return this; } diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatBuilderEntryPoint.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatBuilderEntryPoint.cs index c3abc5f5..77fc94b5 100644 --- a/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatBuilderEntryPoint.cs +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatBuilderEntryPoint.cs @@ -10,15 +10,6 @@ public interface IChatBuilderEntryPoint : IChatActions /// The context instance implementing for method chaining. IChatMessageBuilder WithModel(string model); - /// - /// Configures a custom model with a specific path and project context. - /// - /// The name of the custom model. - /// The path to the model files. - /// Optional multi-modal project identifier. - /// The context instance implementing for method chaining. - IChatMessageBuilder WithCustomModel(string model, string path, string? mmProject = null); - /// /// Loads an existing chat session from the database using its unique identifier. /// diff --git a/src/MaIN.Domain/Models/Abstract/AIModel.cs b/src/MaIN.Domain/Models/Abstract/AIModel.cs index 1b9c01dc..74f7451a 100644 --- a/src/MaIN.Domain/Models/Abstract/AIModel.cs +++ b/src/MaIN.Domain/Models/Abstract/AIModel.cs @@ -1,4 +1,4 @@ -using MaIN.Domain.Configuration; +using MaIN.Domain.Configuration; using MaIN.Domain.Exceptions.Models.LocalModels; namespace MaIN.Domain.Models.Abstract; @@ -31,7 +31,7 @@ public abstract record AIModel( /// 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; @@ -70,7 +70,7 @@ public bool IsDownloaded(string? basePath) return false; } } - + /// /// Combines the specified base path with the file name to generate a full file path. /// @@ -81,12 +81,9 @@ public bool IsDownloaded(string? basePath) /// Thrown if both CustomPath and basePath are null or empty. public string GetFullPath(string? basePath = null) { - if (string.IsNullOrEmpty(CustomPath) && string.IsNullOrEmpty(basePath)) - { - throw new ModelPathNullOrEmptyException(); - } - - return Path.Combine((CustomPath ?? basePath)!, FileName); + return string.IsNullOrEmpty(CustomPath) && string.IsNullOrEmpty(basePath) + ? throw new ModelPathNullOrEmptyException() + : Path.Combine((CustomPath ?? basePath)!, FileName); } } @@ -109,6 +106,16 @@ public record GenericCloudModel( string? SystemMessage = null ) : CloudModel(Id, Backend, Name, MaxContextWindowSize, Description, SystemMessage); +/// Generic class for runtime defined cloud image generation models. +public record GenericImageGenerationCloudModel( + string Id, + BackendType Backend, + string? Name = null, + uint MaxContextWindowSize = ModelDefaults.DefaultMaxContextWindow, + string? Description = null, + string? SystemMessage = null +) : CloudModel(Id, Backend, Name, MaxContextWindowSize, Description, SystemMessage), IImageGenerationModel; + /// Generic class for runtime defined cloud models with reasoning capability. public record GenericCloudReasoningModel( string Id, @@ -119,7 +126,7 @@ public record GenericCloudReasoningModel( 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; @@ -134,7 +141,7 @@ public record GenericCloudVisionModel( string? Description = null, string? SystemMessage = null ) : CloudModel(Id, Backend, Name, MaxContextWindowSize, Description, SystemMessage), IVisionModel -{ +{ // IVisionModel - cloud models don't need MMProjectPath public string? MMProjectName => null; } @@ -149,10 +156,10 @@ public record GenericCloudVisionReasoningModel( 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? MMProjectName => null; - + // IReasoningModel - null for cloud (handled by provider API) public Func? ReasonFunction => null; public string? AdditionalPrompt { get; } = AdditionalPrompt; @@ -183,7 +190,7 @@ public record GenericLocalReasoningModel( string? Description = null, string? SystemMessage = null ) : LocalModel(Id ?? FileName, FileName, DownloadUrl, Name ?? FileName, MaxContextWindowSize, Description, SystemMessage, CustomPath), IReasoningModel -{ +{ // IReasoningModel implementation public Func ReasonFunction { get; } = ReasonFunction; public string? AdditionalPrompt { get; } = AdditionalPrompt; @@ -201,7 +208,7 @@ public record GenericLocalVisionModel( string? Description = null, string? SystemMessage = null ) : LocalModel(Id ?? FileName, FileName, DownloadUrl, Name ?? FileName, MaxContextWindowSize, Description, SystemMessage, CustomPath), IVisionModel -{ +{ // IVisionModel implementation public string MMProjectName { get; } = MMProjectPath; } @@ -223,7 +230,7 @@ public record GenericLocalVisionReasoningModel( { // IVisionModel implementation public string MMProjectName { get; } = MMProjectPath; - + // IReasoningModel implementation public Func ReasonFunction { get; } = ReasonFunction; public string? AdditionalPrompt { get; } = AdditionalPrompt; diff --git a/src/MaIN.Domain/Models/Models.cs b/src/MaIN.Domain/Models/Models.cs new file mode 100644 index 00000000..f43a4ffc --- /dev/null +++ b/src/MaIN.Domain/Models/Models.cs @@ -0,0 +1,102 @@ +namespace MaIN.Domain.Models; + +/// +/// Compile-time constants for all pre-built model IDs. +/// Use these with WithModel(string modelId) to avoid magic strings. +/// +public static class Models +{ + public static class OpenAi + { + public const string Gpt4oMini = "gpt-4o-mini"; + public const string Gpt4_1Mini = "gpt-4.1-mini"; + public const string Gpt5Nano = "gpt-5-nano"; + public const string DallE3 = "dall-e-3"; + } + + public static class Anthropic + { + public const string ClaudeSonnet4 = "claude-sonnet-4-20250514"; + public const string ClaudeSonnet4_5 = "claude-sonnet-4-5-20250929"; + } + + public static class Gemini + { + public const string Gemini2_5Flash = "gemini-2.5-flash"; + public const string Gemini2_0Flash = "gemini-2.0-flash"; + } + + public static class Xai + { + public const string Grok3Beta = "grok-3-beta"; + } + + public static class Groq + { + public const string Llama3_1_8bInstant = "llama-3.1-8b-instant"; + public const string GptOss20b = "openai/gpt-oss-20b"; + } + + public static class DeepSeek + { + public const string Reasoner = "deepseek-reasoner"; + } + + public static class Ollama + { + public const string Gemma3_4b = "gemma3:4b"; + } + + public static class Local + { + // Gemma + public const string Gemma2_2b = "gemma2-2b"; + public const string Gemma3_4b = "gemma3-4b"; + public const string Gemma3_12b = "gemma3-12b"; + public const string Gemma3n_e4b = "gemma3n-e4b"; + + // Llama + public const string Llama3_2_3b = "llama3.2-3b"; + public const string Llama3_1_8b = "llama3.1-8b"; + public const string Llava_7b = "llava-7b"; + public const string Llava16Mistral_7b = "llava-1.6-mistral-7b"; + + // Hermes + public const string Hermes3_3b = "hermes3-3b"; + public const string Hermes3_8b = "hermes3-8b"; + + // Qwen + public const string Qwen2_5_0_5b = "qwen2.5-0.5b"; + public const string Qwen2_5_Coder_3b = "qwen2.5-coder-3b"; + public const string Qwen2_5_Coder_7b = "qwen2.5-coder-7b"; + public const string Qwen2_5_Coder_14b = "qwen2.5-coder-14b"; + public const string Qwen3_8b = "qwen3-8b"; + public const string Qwen3_14b = "qwen3-14b"; + public const string QwQ_7b = "qwq-7b"; + + // DeepSeek + public const string DeepSeekR1_8b = "deepseekr1-8b"; + public const string DeepSeekR1_1_5b = "deepseekr1-1.5b"; + + // Phi + public const string Phi3_5_3b = "phi3.5-3b"; + public const string Phi4_4b = "phi4-4b"; + + // Other + public const string Lfm2_1_2b = "lfm2-1.2b"; + public const string Minicpm4_8b = "minicpm4-8b"; + public const string Mistral3_2_24b = "mistral-3.2-24b"; + public const string Webgen_4b = "webgen-4b"; + public const string Bielik2_5_11b = "bielik-2.5-11b"; + public const string OlympicCoder_7b = "olympiccoder-7b"; + public const string Yi_6b = "yi-6b"; + public const string Smollm2_0_1b = "smollm2-0.1b"; + public const string Olmo2_7b = "olmo2-7b"; + + // Embedding + public const string NomicEmbedding = "nomic-embedding"; + + // TTS + public const string Kokoro82m = "kokoro-82m"; + } +} diff --git a/src/MaIN.InferPage/Components/Pages/Home.razor b/src/MaIN.InferPage/Components/Pages/Home.razor index dd8bb757..4fb24c4a 100644 --- a/src/MaIN.InferPage/Components/Pages/Home.razor +++ b/src/MaIN.InferPage/Components/Pages/Home.razor @@ -1,4 +1,4 @@ -@page "/" +@page "/" @rendermode @(new InteractiveServerRenderMode(prerender: true)) @inject IJSRuntime JS @implements IDisposable @@ -271,7 +271,8 @@ : new GenericCloudModel(Id: Utils.Model!, Backend: Utils.BackendType); } - ctx = AIHub.Chat().WithModel(model, imageGen: Utils.ImageGen); + ModelRegistry.RegisterOrReplace(model!); + ctx = AIHub.Chat().WithModel(model!.Id); } catch (MaINCustomException ex) { From 03905a7d5e4915e56eefef534776e76dd068cf1f Mon Sep 17 00:00:00 2001 From: srebrek Date: Thu, 5 Mar 2026 10:20:07 +0100 Subject: [PATCH 3/9] Make use of the ChatId property of the Agent --- MaIN.Core.IntegrationTests/ChatTests.cs | 3 +-- src/MaIN.Domain/Entities/Agents/Agent.cs | 4 ++-- src/MaIN.Services/Services/AgentService.cs | 14 ++++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/MaIN.Core.IntegrationTests/ChatTests.cs b/MaIN.Core.IntegrationTests/ChatTests.cs index 73130600..60d67048 100644 --- a/MaIN.Core.IntegrationTests/ChatTests.cs +++ b/MaIN.Core.IntegrationTests/ChatTests.cs @@ -47,12 +47,11 @@ public async Task Should_AnswerDifferences_BetweenDocuments_ChatWithFiles() public async Task Should_AnswerQuestion_FromExistingChat() { var result = AIHub.Chat() - .WithModel(Models.Local.Qwen2_5_0_5b); + .WithModel(Models.Local.Gemma2_2b); await result.WithMessage("What do you think about math theories?") .CompleteAsync(); - await result.WithMessage("And about physics?") .CompleteAsync(); diff --git a/src/MaIN.Domain/Entities/Agents/Agent.cs b/src/MaIN.Domain/Entities/Agents/Agent.cs index b8f908bb..0bf6af30 100644 --- a/src/MaIN.Domain/Entities/Agents/Agent.cs +++ b/src/MaIN.Domain/Entities/Agents/Agent.cs @@ -12,10 +12,10 @@ public class Agent public bool Started { get; set; } public bool Flow { get; set; } public required AgentData Context { get; init; } - public string ChatId => string.Empty; + public string ChatId { get; set; } = string.Empty; public int Order { get; set; } public BackendType? Backend { get; set; } public Dictionary Behaviours { get; set; } = []; public required string CurrentBehaviour { get; set; } public ToolsConfiguration? ToolsConfiguration { get; set; } -} \ No newline at end of file +} diff --git a/src/MaIN.Services/Services/AgentService.cs b/src/MaIN.Services/Services/AgentService.cs index 8d35aa97..333df664 100644 --- a/src/MaIN.Services/Services/AgentService.cs +++ b/src/MaIN.Services/Services/AgentService.cs @@ -76,7 +76,10 @@ await notificationService.DispatchNotification( await agentRepository.UpdateAgent(agent.Id, agent); await notificationService.DispatchNotification( - NotificationMessageBuilder.ProcessingComplete(agentId, agent.CurrentBehaviour, "COMPLETED"), "ReceiveAgentUpdate"); + NotificationMessageBuilder.ProcessingComplete( + agentId, + agent.CurrentBehaviour, + "COMPLETED"), "ReceiveAgentUpdate"); //normalize message before returning it to user chat.Messages.Last().Content = Replace( @@ -90,7 +93,8 @@ await notificationService.DispatchNotification( catch (Exception ex) { await notificationService.DispatchNotification( - NotificationMessageBuilder.ProcessingFailed(agentId, agent.CurrentBehaviour, ex.Message), "ReceiveAgentUpdate"); + NotificationMessageBuilder.ProcessingFailed(agentId, agent.CurrentBehaviour, ex.Message), + "ReceiveAgentUpdate"); throw; } } @@ -128,15 +132,13 @@ public async Task CreateAgent(Agent agent, bool flow = false, bool intera agent.Started = true; agent.Flow = flow; + agent.ChatId = chat.Id; agent.Behaviours ??= []; agent.Behaviours.Add("Default", agent.Context.Instruction!); agent.CurrentBehaviour = "Default"; - var agentDocument = agent.ToDocument(); - agentDocument.ChatId = chat.Id; - await chatRepository.AddChat(chat.ToDocument()); - await agentRepository.AddAgent(agentDocument); + await agentRepository.AddAgent(agent.ToDocument()); return agent; } From 4710d44cae3c255107b9a94410565fd7b1d958ee Mon Sep 17 00:00:00 2001 From: srebrek Date: Thu, 5 Mar 2026 10:33:17 +0100 Subject: [PATCH 4/9] Use const modelIDs instead of magic strings in the predefined models --- .../Models/Concrete/CloudModels.cs | 30 ++++----- .../Models/Concrete/LocalModels.cs | 66 +++++++++---------- src/MaIN.Domain/Models/Models.cs | 2 + 3 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/MaIN.Domain/Models/Concrete/CloudModels.cs b/src/MaIN.Domain/Models/Concrete/CloudModels.cs index 2ea7bb6a..002e8d2f 100644 --- a/src/MaIN.Domain/Models/Concrete/CloudModels.cs +++ b/src/MaIN.Domain/Models/Concrete/CloudModels.cs @@ -6,7 +6,7 @@ namespace MaIN.Domain.Models.Concrete; // ===== OpenAI Models ===== public sealed record Gpt4oMini() : CloudModel( - "gpt-4o-mini", + Models.OpenAi.Gpt4oMini, BackendType.OpenAi, "GPT-4o Mini", ModelDefaults.DefaultMaxContextWindow, @@ -16,7 +16,7 @@ public sealed record Gpt4oMini() : CloudModel( } public sealed record Gpt4_1Mini() : CloudModel( - "gpt-4.1-mini", + Models.OpenAi.Gpt4_1Mini, BackendType.OpenAi, "GPT-4.1 Mini", ModelDefaults.DefaultMaxContextWindow, @@ -26,7 +26,7 @@ public sealed record Gpt4_1Mini() : CloudModel( } public sealed record Gpt5Nano() : CloudModel( - "gpt-5-nano", + Models.OpenAi.Gpt5Nano, BackendType.OpenAi, "GPT-5 Nano", ModelDefaults.DefaultMaxContextWindow, @@ -36,14 +36,14 @@ public sealed record Gpt5Nano() : CloudModel( } public sealed record DallE3() : CloudModel( - "dall-e-3", + Models.OpenAi.DallE3, BackendType.OpenAi, "DALL-E 3", 4000, "Advanced image generation model from OpenAI"), IImageGenerationModel; public sealed record GptImage1() : CloudModel( - "gpt-image-1", + Models.OpenAi.GptImage1, BackendType.OpenAi, "GPT Image 1", 4000, @@ -52,7 +52,7 @@ public sealed record GptImage1() : CloudModel( // ===== Anthropic Models ===== public sealed record ClaudeSonnet4() : CloudModel( - "claude-sonnet-4-20250514", + Models.Anthropic.ClaudeSonnet4, BackendType.Anthropic, "Claude Sonnet 4", 200000, @@ -62,7 +62,7 @@ public sealed record ClaudeSonnet4() : CloudModel( } public sealed record ClaudeSonnet4_5() : CloudModel( - "claude-sonnet-4-5-20250929", + Models.Anthropic.ClaudeSonnet4_5, BackendType.Anthropic, "Claude Sonnet 4.5", 200000, @@ -74,7 +74,7 @@ public sealed record ClaudeSonnet4_5() : CloudModel( // ===== Gemini Models ===== public sealed record Gemini2_5Flash() : CloudModel( - "gemini-2.5-flash", + Models.Gemini.Gemini2_5Flash, BackendType.Gemini, "Gemini 2.5 Flash", 1000000, @@ -84,7 +84,7 @@ public sealed record Gemini2_5Flash() : CloudModel( } public sealed record Gemini2_0Flash() : CloudModel( - "gemini-2.0-flash", + Models.Gemini.Gemini2_0Flash, BackendType.Gemini, "Gemini 2.0 Flash", 1000000, @@ -96,7 +96,7 @@ public sealed record Gemini2_0Flash() : CloudModel( // ===== xAI Models ===== public sealed record Grok3Beta() : CloudModel( - "grok-3-beta", + Models.Xai.Grok3Beta, BackendType.Xai, "Grok 3 Beta", ModelDefaults.DefaultMaxContextWindow, @@ -106,7 +106,7 @@ public sealed record Grok3Beta() : CloudModel( } public sealed record GrokImage() : CloudModel( - "grok-2-image", + Models.Xai.GrokImage, BackendType.Xai, "Grok 2 Image", 4000, @@ -115,14 +115,14 @@ public sealed record GrokImage() : CloudModel( // ===== GroqCloud Models ===== public sealed record Llama3_1_8bInstant() : CloudModel( - "llama-3.1-8b-instant", + Models.Groq.Llama3_1_8bInstant, 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", + Models.Groq.GptOss20b, BackendType.GroqCloud, "GPT OSS 20B", 8192, @@ -131,7 +131,7 @@ public sealed record GptOss20b() : CloudModel( // ===== DeepSeek Models ===== public sealed record DeepSeekReasoner() : CloudModel( - "deepseek-reasoner", + Models.DeepSeek.Reasoner, BackendType.DeepSeek, "DeepSeek Reasoner", 64000, @@ -144,7 +144,7 @@ public sealed record DeepSeekReasoner() : CloudModel( // ===== Ollama Models ===== public sealed record OllamaGemma3_4b() : CloudModel( - "gemma3:4b", + Models.Ollama.Gemma3_4b, BackendType.Ollama, "Gemma3 4B (Ollama)", 8192, diff --git a/src/MaIN.Domain/Models/Concrete/LocalModels.cs b/src/MaIN.Domain/Models/Concrete/LocalModels.cs index 7ac871e7..729639ea 100644 --- a/src/MaIN.Domain/Models/Concrete/LocalModels.cs +++ b/src/MaIN.Domain/Models/Concrete/LocalModels.cs @@ -5,7 +5,7 @@ namespace MaIN.Domain.Models.Concrete; // ===== Gemma Family ===== public sealed record Gemma2_2b() : LocalModel( - "gemma2-2b", + Models.Local.Gemma2_2b, "Gemma2-2b.gguf", new Uri("https://huggingface.co/Inza124/gemma2_2b/resolve/main/gemma2-2b-maIN.gguf?download=true"), "Gemma 2B", @@ -13,7 +13,7 @@ public sealed record Gemma2_2b() : LocalModel( "Lightweight 2B model for general-purpose text generation and understanding"); public sealed record Gemma3_4b() : LocalModel( - "gemma3-4b", + Models.Local.Gemma3_4b, "Gemma3-4b.gguf", new Uri("https://huggingface.co/Inza124/Gemma3-4b/resolve/main/gemma3-4b.gguf?download=true"), "Gemma3 4B", @@ -21,7 +21,7 @@ public sealed record Gemma3_4b() : LocalModel( "Balanced 4B model for writing, analysis, and mathematical reasoning"); public sealed record Gemma3_12b() : LocalModel( - "gemma3-12b", + Models.Local.Gemma3_12b, "Gemma3-12b.gguf", new Uri("https://huggingface.co/Inza124/Gemma3-12b/resolve/main/gemma3-12b.gguf?download=true"), "Gemma3 12B", @@ -29,7 +29,7 @@ public sealed record Gemma3_12b() : LocalModel( "Large 12B model for complex analysis, research, and creative writing"); public sealed record Gemma3n_e4b() : LocalModel( - "gemma3n-e4b", + Models.Local.Gemma3n_e4b, "Gemma3n-e4b.gguf", new Uri("https://huggingface.co/Inza124/Gemma-3n-e4b/resolve/main/gemma-3n-e4b.gguf?download=true"), "Gemma3n E4B", @@ -39,7 +39,7 @@ public sealed record Gemma3n_e4b() : LocalModel( // ===== Llama Family ===== public sealed record Llama3_2_3b() : LocalModel( - "llama3.2-3b", + Models.Local.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", @@ -47,7 +47,7 @@ public sealed record Llama3_2_3b() : LocalModel( "Lightweight 3B model for chatbots, content creation, and basic coding"); public sealed record Llama3_1_8b() : LocalModel( - "llama3.1-8b", + Models.Local.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", @@ -55,7 +55,7 @@ public sealed record Llama3_1_8b() : LocalModel( "Versatile 8B model for writing, coding, math, and general assistance"); public sealed record Llava_7b() : LocalModel( - "llava-7b", + Models.Local.Llava_7b, "Llava.gguf", new Uri("https://huggingface.co/Inza124/Llava/resolve/main/Llava-maIN.gguf?download=true"), "LLaVA 7B", @@ -66,7 +66,7 @@ public sealed record Llava_7b() : LocalModel( } public sealed record Llava16Mistral_7b() : LocalModel( - "llava-1.6-mistral-7b", + Models.Local.Llava16Mistral_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", @@ -79,7 +79,7 @@ public sealed record Llava16Mistral_7b() : LocalModel( // ===== Hermes Family ===== public sealed record Hermes3_3b() : LocalModel( - "hermes3-3b", + Models.Local.Hermes3_3b, "Hermes3-3b.gguf", new Uri("https://huggingface.co/Inza124/Hermes3-3b/resolve/main/hermes3-3b.gguf?download=true"), "Hermes 3 3B", @@ -87,7 +87,7 @@ public sealed record Hermes3_3b() : LocalModel( "Efficient 3B model for dialogue, roleplay, and conversational AI"); public sealed record Hermes3_8b() : LocalModel( - "hermes3-8b", + Models.Local.Hermes3_8b, "Hermes3-8b.gguf", new Uri("https://huggingface.co/Inza124/Hermes3_8b/resolve/main/hermes3-8b.gguf?download=true"), "Hermes 3 8B", @@ -97,7 +97,7 @@ public sealed record Hermes3_8b() : LocalModel( // ===== Qwen Family ===== public sealed record Qwen2_5_0_5b() : LocalModel( - "qwen2.5-0.5b", + Models.Local.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", @@ -105,7 +105,7 @@ public sealed record Qwen2_5_0_5b() : LocalModel( "Ultra-lightweight 0.5B model for simple text completion and basic tasks"); public sealed record Qwen2_5_Coder_3b() : LocalModel( - "qwen2.5-coder-3b", + Models.Local.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", @@ -113,7 +113,7 @@ public sealed record Qwen2_5_Coder_3b() : LocalModel( "Compact 3B model for Python, JavaScript, bug fixing, and code review"); public sealed record Qwen2_5_Coder_7b() : LocalModel( - "qwen2.5-coder-7b", + Models.Local.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", @@ -121,7 +121,7 @@ public sealed record Qwen2_5_Coder_7b() : LocalModel( "Advanced 7B model for full-stack development, API design, and testing"); public sealed record Qwen2_5_Coder_14b() : LocalModel( - "qwen2.5-coder-14b", + Models.Local.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", @@ -129,21 +129,21 @@ public sealed record Qwen2_5_Coder_14b() : LocalModel( "Professional 14B model for system design, architecture, and code refactoring"); public sealed record Qwen3_8b() : LocalModel( - "qwen3-8b", + Models.Local.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", + Models.Local.Qwen3_14b, "Qwen3-14b.gguf", new Uri("https://huggingface.co/Inza124/Qwen3-14b/resolve/main/Qwen3-14b.gguf?download=true"), "Qwen 3 14B", @@ -155,7 +155,7 @@ public sealed record Qwen3_14b() : LocalModel( } public sealed record QwQ_7b() : LocalModel( - "qwq-7b", + Models.Local.QwQ_7b, "QwQ-7b.gguf", new Uri("https://huggingface.co/Inza124/QwQ-7b/resolve/main/qwq-7b.gguf?download=true"), "QwQ 7B", @@ -169,7 +169,7 @@ public sealed record QwQ_7b() : LocalModel( // ===== DeepSeek Family ===== public sealed record DeepSeek_R1_8b() : LocalModel( - "deepseekr1-8b", + Models.Local.DeepSeekR1_8b, "DeepSeekR1-8b.gguf", new Uri("https://huggingface.co/Inza124/DeepseekR1-8b/resolve/main/DeepSeekR1-8b-maIN.gguf?download=true"), "DeepSeek R1 8B", @@ -181,7 +181,7 @@ public sealed record DeepSeek_R1_8b() : LocalModel( } public sealed record DeepSeek_R1_1_5b() : LocalModel( - "deepseekr1-1.5b", + Models.Local.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", @@ -195,7 +195,7 @@ public sealed record DeepSeek_R1_1_5b() : LocalModel( // ===== Phi Family ===== public sealed record Phi3_5_3b() : LocalModel( - "phi3.5-3b", + Models.Local.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", @@ -203,7 +203,7 @@ public sealed record Phi3_5_3b() : LocalModel( "Efficient 3B model for mobile apps, IoT devices, and edge computing"); public sealed record Phi4_4b() : LocalModel( - "phi4-4b", + Models.Local.Phi4_4b, "phi4-4b.gguf", new Uri("https://huggingface.co/Inza124/Phi4-4b/resolve/main/phi4-4b.gguf?download=true"), "Phi 4 4B", @@ -213,7 +213,7 @@ public sealed record Phi4_4b() : LocalModel( // ===== Other Models ===== public sealed record LFM2_1_2b() : LocalModel( - "lfm2-1.2b", + Models.Local.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", @@ -221,7 +221,7 @@ public sealed record LFM2_1_2b() : LocalModel( "Lightweight modern 1.2B model for fast inference and resource-constrained environments"); public sealed record Minicpm4_8b() : LocalModel( - "minicpm4-8b", + Models.Local.Minicpm4_8b, "Minicpm4-8b.gguf", new Uri("https://huggingface.co/Inza124/Minicpm4-8b/resolve/main/MiniCPM4-8b.gguf?download=true"), "MiniCPM4 8B", @@ -229,7 +229,7 @@ public sealed record Minicpm4_8b() : LocalModel( "Mid-size 8B model balancing performance and efficiency for diverse applications"); public sealed record Mistral_3_2_24b() : LocalModel( - "mistral-3.2-24b", + Models.Local.Mistral3_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", @@ -237,7 +237,7 @@ public sealed record Mistral_3_2_24b() : LocalModel( "Large 24B model offering advanced reasoning and comprehensive knowledge for complex tasks"); public sealed record Webgen_4b() : LocalModel( - "webgen-4b", + Models.Local.Webgen_4b, "webgen-4b.gguf", new Uri("https://huggingface.co/Inza124/webgen-4b/resolve/main/Webgen-4b.gguf?download=true"), "Webgen 4B", @@ -245,7 +245,7 @@ public sealed record Webgen_4b() : LocalModel( "Specialized 4B model optimized for web development and code generation tasks"); public sealed record Bielik_2_5_11b() : LocalModel( - "bielik-2.5-11b", + Models.Local.Bielik2_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", @@ -253,7 +253,7 @@ public sealed record Bielik_2_5_11b() : LocalModel( "Large 11B Polish language model with strong multilingual capabilities and reasoning"); public sealed record OlympicCoder_7b() : LocalModel( - "olympiccoder-7b", + Models.Local.OlympicCoder_7b, "Olympiccoder-7b.gguf", new Uri("https://huggingface.co/Inza124/OlympicCoder-7b/resolve/main/OlympicCoder-7b.gguf?download=true"), "OlympicCoder 7B", @@ -261,7 +261,7 @@ public sealed record OlympicCoder_7b() : LocalModel( "Specialized 7B model for algorithms, data structures, and contest programming"); public sealed record Yi_6b() : LocalModel( - "yi-6b", + Models.Local.Yi_6b, "Yi-6b.gguf", new Uri("https://huggingface.co/Inza124/yi-6b/resolve/main/yi-6b.gguf?download=true"), "Yi 6B", @@ -269,7 +269,7 @@ public sealed record Yi_6b() : LocalModel( "Bilingual 6B model for Chinese-English translation and cultural content"); public sealed record Smollm2_0_1b() : LocalModel( - "smollm2-0.1b", + Models.Local.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", @@ -277,7 +277,7 @@ public sealed record Smollm2_0_1b() : LocalModel( "Tiny 0.1B model for keyword extraction, simple classification, and demos"); public sealed record Olmo2_7b() : LocalModel( - "olmo2-7b", + Models.Local.Olmo2_7b, "Olmo2-7b.gguf", new Uri("https://huggingface.co/Inza124/Olmo2-7b/resolve/main/olmo2-7b.gguf?download=true"), "OLMo2 7B", @@ -287,7 +287,7 @@ public sealed record Olmo2_7b() : LocalModel( // ===== Embedding Model ===== public sealed record Nomic_Embedding() : LocalModel( - "nomic-embedding", + Models.Local.NomicEmbedding, "nomicv2.gguf", new Uri("https://huggingface.co/Inza124/Nomic/resolve/main/nomicv2.gguf?download=true"), "Nomic Embedding", @@ -297,7 +297,7 @@ public sealed record Nomic_Embedding() : LocalModel( // ===== TTS Model ===== public sealed record Kokoro_82m() : LocalModel( - "kokoro-82m", + Models.Local.Kokoro82m, "kokoro.onnx", new Uri("https://github.com/taylorchu/kokoro-onnx/releases/download/v0.2.0/kokoro.onnx"), "Kokoro 82M", diff --git a/src/MaIN.Domain/Models/Models.cs b/src/MaIN.Domain/Models/Models.cs index f43a4ffc..b87a30f1 100644 --- a/src/MaIN.Domain/Models/Models.cs +++ b/src/MaIN.Domain/Models/Models.cs @@ -12,6 +12,7 @@ public static class OpenAi public const string Gpt4_1Mini = "gpt-4.1-mini"; public const string Gpt5Nano = "gpt-5-nano"; public const string DallE3 = "dall-e-3"; + public const string GptImage1 = "gpt-image-1"; } public static class Anthropic @@ -29,6 +30,7 @@ public static class Gemini public static class Xai { public const string Grok3Beta = "grok-3-beta"; + public const string GrokImage = "grok-2-image"; } public static class Groq From bdc956251df2e0ebf1f8cd28f7c846541f24e8d3 Mon Sep 17 00:00:00 2001 From: srebrek Date: Thu, 5 Mar 2026 11:25:13 +0100 Subject: [PATCH 5/9] Fix AgentService.Restart() --- src/MaIN.Services/Services/AgentService.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/MaIN.Services/Services/AgentService.cs b/src/MaIN.Services/Services/AgentService.cs index 333df664..4b92ed30 100644 --- a/src/MaIN.Services/Services/AgentService.cs +++ b/src/MaIN.Services/Services/AgentService.cs @@ -156,7 +156,10 @@ public async Task Restart(string agentId) var agent = await agentRepository.GetAgentById(agentId) ?? throw new AgentNotFoundException(agentId); var chat = (await chatRepository.GetChatById(agent.ChatId))!.ToDomain(); - var llmService = llmServiceFactory.CreateService(agent.Backend ?? maInSettings.BackendType); + var backend = ModelRegistry.TryGetById(chat.ModelId, out var model) + ? model!.Backend + : maInSettings.BackendType; + var llmService = llmServiceFactory.CreateService(backend); await llmService.CleanSessionCache(chat.Id!); AgentStateManager.ClearState(agent, chat); From bc823a54c0d210bea7220a96a7579a85e7a9f731 Mon Sep 17 00:00:00 2001 From: srebrek Date: Thu, 5 Mar 2026 11:29:08 +0100 Subject: [PATCH 6/9] Fix orphaned culumn in databases --- src/MaIN.Infrastructure/Configuration/SqlExtensions.cs | 5 ++--- src/MaIN.Infrastructure/Configuration/SqliteExtensions.cs | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/MaIN.Infrastructure/Configuration/SqlExtensions.cs b/src/MaIN.Infrastructure/Configuration/SqlExtensions.cs index aec4bf7b..88983ad9 100644 --- a/src/MaIN.Infrastructure/Configuration/SqlExtensions.cs +++ b/src/MaIN.Infrastructure/Configuration/SqlExtensions.cs @@ -1,9 +1,9 @@ -using System.Data; using Dapper; using MaIN.Infrastructure.Repositories.Abstract; using MaIN.Infrastructure.Repositories.Sql; using Microsoft.Data.SqlClient; using Microsoft.Extensions.DependencyInjection; +using System.Data; namespace MaIN.Infrastructure.Configuration; @@ -48,7 +48,6 @@ [Messages] NVARCHAR(MAX) NOT NULL, -- Stored as JSON array [Type] NVARCHAR(MAX) NOT NULL, -- Stored as JSON [Properties] NVARCHAR(MAX) NULL, -- Stored as JSON [Visual] BIT NOT NULL DEFAULT 0, - [BackendType] INT NOT NULL DEFAULT 0, [ConvState] NVARCHAR(MAX) NULL, -- Stored as JSON, [InferenceParams] NVARCHAR(MAX) NULL, -- Stored as JSON, [MemoryParams] NVARCHAR(MAX) NULL, -- Stored as JSON @@ -111,4 +110,4 @@ ALTER TABLE [dbo].[Agents] ADD CONSTRAINT [CK_Agents_Behaviours_JSON] CHECK (CASE WHEN [Behaviours] IS NULL THEN 1 ELSE ISJSON([Behaviours]) END = 1); """); } -} \ No newline at end of file +} diff --git a/src/MaIN.Infrastructure/Configuration/SqliteExtensions.cs b/src/MaIN.Infrastructure/Configuration/SqliteExtensions.cs index de63d8fd..7ab75dad 100644 --- a/src/MaIN.Infrastructure/Configuration/SqliteExtensions.cs +++ b/src/MaIN.Infrastructure/Configuration/SqliteExtensions.cs @@ -1,9 +1,9 @@ -using System.Data; using Dapper; using MaIN.Infrastructure.Repositories.Abstract; using MaIN.Infrastructure.Repositories.Sqlite; using Microsoft.Data.Sqlite; using Microsoft.Extensions.DependencyInjection; +using System.Data; namespace MaIN.Infrastructure.Configuration; @@ -46,7 +46,6 @@ CREATE TABLE IF NOT EXISTS Chats ( Type TEXT NOT NULL, -- Stored as JSON Properties TEXT, -- Stored as JSON Visual INTEGER NOT NULL DEFAULT 0, - BackendType INTEGER NOT NULL DEFAULT 0, ConvState TEXT, -- Stored as JSON InferenceParams TEXT, -- Stored as JSON MemoryParams TEXT, -- Stored as JSON @@ -77,4 +76,4 @@ FOREIGN KEY (ChatId) REFERENCES Chats(Id) ); "); } -} \ No newline at end of file +} From 57dcdd6586e4ceda8574404327c611302434f842 Mon Sep 17 00:00:00 2001 From: srebrek Date: Thu, 5 Mar 2026 11:34:23 +0100 Subject: [PATCH 7/9] Fix copy paste bug in SqliteChatRepository.cs --- .../Repositories/Sql/SqlChatRepository.cs | 31 +++++++++---------- .../Sqlite/SqliteChatRepository.cs | 6 ++-- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/MaIN.Infrastructure/Repositories/Sql/SqlChatRepository.cs b/src/MaIN.Infrastructure/Repositories/Sql/SqlChatRepository.cs index 027a8089..2527102b 100644 --- a/src/MaIN.Infrastructure/Repositories/Sql/SqlChatRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/Sql/SqlChatRepository.cs @@ -46,22 +46,21 @@ private ChatDocument MapChatDocument(dynamic row) private object MapChatToParameters(ChatDocument chat) { - return chat is null - ? throw new ArgumentNullException(nameof(chat)) - : new - { - chat.Id, - chat.Name, - chat.Model, - Messages = JsonSerializer.Serialize(chat.Messages, _jsonOptions), - Type = JsonSerializer.Serialize(chat.Type, _jsonOptions), - ConvState = JsonSerializer.Serialize(chat.ConvState, _jsonOptions), - InferenceParams = JsonSerializer.Serialize(chat.InferenceParams, _jsonOptions), - MemoryParams = JsonSerializer.Serialize(chat.MemoryParams, _jsonOptions), - Properties = JsonSerializer.Serialize(chat.Properties, _jsonOptions), - Visual = chat.ImageGen, - chat.Interactive - }; + ArgumentNullException.ThrowIfNull(chat); + return new + { + chat.Id, + chat.Name, + chat.Model, + Messages = JsonSerializer.Serialize(chat.Messages, _jsonOptions), + Type = JsonSerializer.Serialize(chat.Type, _jsonOptions), + ConvState = JsonSerializer.Serialize(chat.ConvState, _jsonOptions), + InferenceParams = JsonSerializer.Serialize(chat.InferenceParams, _jsonOptions), + MemoryParams = JsonSerializer.Serialize(chat.MemoryParams, _jsonOptions), + Properties = JsonSerializer.Serialize(chat.Properties, _jsonOptions), + Visual = chat.ImageGen, + chat.Interactive + }; } public async Task> GetAllChats() diff --git a/src/MaIN.Infrastructure/Repositories/Sqlite/SqliteChatRepository.cs b/src/MaIN.Infrastructure/Repositories/Sqlite/SqliteChatRepository.cs index 634b760d..420bd319 100644 --- a/src/MaIN.Infrastructure/Repositories/Sqlite/SqliteChatRepository.cs +++ b/src/MaIN.Infrastructure/Repositories/Sqlite/SqliteChatRepository.cs @@ -27,13 +27,13 @@ private ChatDocument MapChatDocument(dynamic row) Type = row.Type is not null ? JsonSerializer.Deserialize(row.Type, _jsonOptions) : default, - ConvState = row.Type is not null + ConvState = row.ConvState is not null ? JsonSerializer.Deserialize(row.ConvState, _jsonOptions) : default, - InferenceParams = row.Type is not null + InferenceParams = row.InferenceParams is not null ? JsonSerializer.Deserialize(row.InferenceParams, _jsonOptions) : default, - MemoryParams = row.Type is not null + MemoryParams = row.MemoryParams is not null ? JsonSerializer.Deserialize(row.MemoryParams, _jsonOptions) : default, Properties = row.Properties is not null From 4069ba85dd136125a653d47a3d17ad31c87b5ca8 Mon Sep 17 00:00:00 2001 From: srebrek Date: Thu, 5 Mar 2026 12:27:05 +0100 Subject: [PATCH 8/9] Fix noninformative error thrown when modelId is not found --- src/MaIN.Core/Hub/Contexts/FlowContext.cs | 8 ++++- .../ChatContext/IChatBuilderEntryPoint.cs | 2 +- .../Agents/AgentModelNotAvailableException.cs | 10 ++++++ .../Chats/ChatModelNotAvailableException.cs | 10 ++++++ src/MaIN.Services/Services/ChatService.cs | 8 +++-- .../Services/LLMService/LLMService.cs | 2 -- .../Services/Steps/BecomeStepHandler.cs | 8 ++++- .../Steps/Commands/AnswerCommandHandler.cs | 35 ++++++++----------- .../Steps/Commands/RedirectCommandHandler.cs | 8 ++++- .../Steps/Commands/StartCommandHandler.cs | 10 +++++- .../Services/Steps/FechDataStepHandler.cs | 8 ++++- 11 files changed, 79 insertions(+), 30 deletions(-) create mode 100644 src/MaIN.Domain/Exceptions/Agents/AgentModelNotAvailableException.cs create mode 100644 src/MaIN.Domain/Exceptions/Chats/ChatModelNotAvailableException.cs diff --git a/src/MaIN.Core/Hub/Contexts/FlowContext.cs b/src/MaIN.Core/Hub/Contexts/FlowContext.cs index 0ba28846..515e7c4d 100644 --- a/src/MaIN.Core/Hub/Contexts/FlowContext.cs +++ b/src/MaIN.Core/Hub/Contexts/FlowContext.cs @@ -3,6 +3,7 @@ using MaIN.Domain.Entities; using MaIN.Domain.Entities.Agents; using MaIN.Domain.Entities.Agents.AgentSource; +using MaIN.Domain.Exceptions.Agents; using MaIN.Domain.Exceptions.Flows; using MaIN.Domain.Models.Abstract; using MaIN.Services.Services.Abstract; @@ -152,7 +153,12 @@ public async Task ProcessAsync(Chat chat, bool translate = false) public async Task ProcessAsync(string message, bool translate = false) { var chat = await _agentService.GetChatByAgent(_firstAgent!.Id); - var backend = ModelRegistry.GetById(chat.ModelId).Backend; + if (!ModelRegistry.TryGetById(chat.ModelId, out var model)) + { + throw new AgentModelNotAvailableException(_firstAgent.Id, chat.ModelId); + } + + var backend = model!.Backend; chat.Messages.Add(new Message() { Content = message, diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatBuilderEntryPoint.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatBuilderEntryPoint.cs index 77fc94b5..51b7894f 100644 --- a/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatBuilderEntryPoint.cs +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/ChatContext/IChatBuilderEntryPoint.cs @@ -6,7 +6,7 @@ public interface IChatBuilderEntryPoint : IChatActions /// Sets the AI model to be used for the current chat session. This determines how the AI will respond to messages /// based on the selected model. /// - /// The name of the AI model to be used. + /// The ID of the AI model to be used. /// The context instance implementing for method chaining. IChatMessageBuilder WithModel(string model); diff --git a/src/MaIN.Domain/Exceptions/Agents/AgentModelNotAvailableException.cs b/src/MaIN.Domain/Exceptions/Agents/AgentModelNotAvailableException.cs new file mode 100644 index 00000000..3b2ed77d --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Agents/AgentModelNotAvailableException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Agents; + +public class AgentModelNotAvailableException(string agentId, string modelId) + : MaINCustomException($"Model '{modelId}' used by agent '{agentId}' is not registered. If this is a dynamically registered model, it must be re-registered after application restart.") +{ + public override string PublicErrorMessage => $"Model '{modelId}' is not available."; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.UnprocessableEntity; +} diff --git a/src/MaIN.Domain/Exceptions/Chats/ChatModelNotAvailableException.cs b/src/MaIN.Domain/Exceptions/Chats/ChatModelNotAvailableException.cs new file mode 100644 index 00000000..f9a85e5f --- /dev/null +++ b/src/MaIN.Domain/Exceptions/Chats/ChatModelNotAvailableException.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace MaIN.Domain.Exceptions.Chats; + +public class ChatModelNotAvailableException(string chatId, string modelId) + : MaINCustomException($"Model '{modelId}' used by chat '{chatId}' is not registered. If this is a dynamically registered model, it must be re-registered after application restart.") +{ + public override string PublicErrorMessage => $"Model '{modelId}' is not available."; + public override HttpStatusCode HttpStatusCode => HttpStatusCode.UnprocessableEntity; +} diff --git a/src/MaIN.Services/Services/ChatService.cs b/src/MaIN.Services/Services/ChatService.cs index e7371145..275bd1a3 100644 --- a/src/MaIN.Services/Services/ChatService.cs +++ b/src/MaIN.Services/Services/ChatService.cs @@ -39,8 +39,12 @@ public async Task Completions( chat.ImageGen = true; } - var model = ModelRegistry.GetById(chat.ModelId); - var backend = model.Backend; + if (!ModelRegistry.TryGetById(chat.ModelId, out var model)) + { + throw new ChatModelNotAvailableException(chat.Id, chat.ModelId); + } + + var backend = model!.Backend; chat.Messages.Where(x => x.Type == MessageType.NotSet).ToList() .ForEach(x => x.Type = backend != BackendType.Self ? MessageType.CloudLLM : MessageType.LocalLLM); diff --git a/src/MaIN.Services/Services/LLMService/LLMService.cs b/src/MaIN.Services/Services/LLMService/LLMService.cs index 6aa3b36b..4a40af93 100644 --- a/src/MaIN.Services/Services/LLMService/LLMService.cs +++ b/src/MaIN.Services/Services/LLMService/LLMService.cs @@ -480,8 +480,6 @@ private static string FormatToolsForPrompt(ToolsConfiguration toolsConfig) } } - - return (tokens, isComplete, hasFailed); } diff --git a/src/MaIN.Services/Services/Steps/BecomeStepHandler.cs b/src/MaIN.Services/Services/Steps/BecomeStepHandler.cs index 39720b64..2313e11b 100644 --- a/src/MaIN.Services/Services/Steps/BecomeStepHandler.cs +++ b/src/MaIN.Services/Services/Steps/BecomeStepHandler.cs @@ -1,5 +1,6 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions.Agents; using MaIN.Domain.Models.Abstract; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models; @@ -34,7 +35,12 @@ public async Task Handle(StepContext context) await context.NotifyProgress("true", context.Agent.Id, null, context.Agent.CurrentBehaviour, StepName); - var backend = ModelRegistry.GetById(context.Chat.ModelId).Backend; + if (!ModelRegistry.TryGetById(context.Chat.ModelId, out var model)) + { + throw new AgentModelNotAvailableException(context.Agent.Id, context.Chat.ModelId); + } + + var backend = model!.Backend; context.Chat.Messages.Add(new() { Role = "System", diff --git a/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs index 362293aa..4ea777f0 100644 --- a/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/AnswerCommandHandler.cs @@ -1,6 +1,7 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Entities.Agents.Knowledge; +using MaIN.Domain.Exceptions.Agents; using MaIN.Domain.Models; using MaIN.Domain.Models.Abstract; using MaIN.Services.Constants; @@ -19,8 +20,7 @@ public class AnswerCommandHandler( ILLMServiceFactory llmServiceFactory, IMcpService mcpService, INotificationService notificationService, - IImageGenServiceFactory imageGenServiceFactory, - MaINSettings settings) + IImageGenServiceFactory imageGenServiceFactory) : ICommandHandler { private static readonly JsonSerializerOptions _jsonOptions = new() @@ -30,10 +30,13 @@ public class AnswerCommandHandler( public async Task HandleAsync(AnswerCommand command) { + if (!ModelRegistry.TryGetById(command.Chat.ModelId, out var model)) + { + throw new AgentModelNotAvailableException(command.AgentId, command.Chat.ModelId); + } + ChatResult? result; - var backend = ModelRegistry.TryGetById(command.Chat.ModelId, out var resolvedModel) - ? resolvedModel!.Backend - : settings.BackendType; + var backend = model!.Backend; var llmService = llmServiceFactory.CreateService(backend); var imageGenService = imageGenServiceFactory.CreateService(backend); @@ -44,15 +47,15 @@ public class AnswerCommandHandler( new ChatMemoryOptions { Memory = command.Chat.Memory }, new ChatRequestOptions()); return result!.Message; case KnowledgeUsage.UseKnowledge: - var isKnowledgeNeeded = await ShouldUseKnowledge(command.Knowledge, command.Chat); + var isKnowledgeNeeded = await ShouldUseKnowledge(command.Knowledge, command.Chat, backend); if (isKnowledgeNeeded) { - return await ProcessKnowledgeQuery(command.Knowledge, command.Chat, command.AgentId); + return await ProcessKnowledgeQuery(command.Knowledge, command.Chat, command.AgentId, llmService); } break; case KnowledgeUsage.AlwaysUseKnowledge: - return await ProcessKnowledgeQuery(command.Knowledge, command.Chat, command.AgentId); + return await ProcessKnowledgeQuery(command.Knowledge, command.Chat, command.AgentId, llmService); } result = command.Chat.ImageGen @@ -68,7 +71,7 @@ public class AnswerCommandHandler( return result!.Message; } - private async Task ShouldUseKnowledge(Knowledge? knowledge, Chat chat) + private async Task ShouldUseKnowledge(Knowledge? knowledge, Chat chat, BackendType backend) { var originalContent = chat.Messages.Last().Content; @@ -87,9 +90,6 @@ private async Task ShouldUseKnowledge(Knowledge? knowledge, Chat chat) Content of available knowledge has source tags. Prompt: {originalContent} """; - var backend = ModelRegistry.TryGetById(chat.ModelId, out var resolvedModel) - ? resolvedModel!.Backend - : settings.BackendType; var service = llmServiceFactory.CreateService(backend); var result = await service.Send(chat, new ChatRequestOptions() @@ -104,7 +104,7 @@ private async Task ShouldUseKnowledge(Knowledge? knowledge, Chat chat) return shouldUseKnowledge; } - private async Task ProcessKnowledgeQuery(Knowledge? knowledge, Chat chat, string agentId) + private async Task ProcessKnowledgeQuery(Knowledge? knowledge, Chat chat, string agentId, ILLMService llmService) { var originalContent = chat.Messages.Last().Content; var indexAsKnowledge = knowledge?.Index.Items.ToDictionary(x => x.Name, x => x.Tags); @@ -116,15 +116,10 @@ private async Task ShouldUseKnowledge(Knowledge? knowledge, Chat chat) KNOWLEDGE: {index} - Find tags that fits user query based on available knowledge (provided to you above as pair of item names with tags). + Find tags that fits user query based on available knowledge (provided to you above as pair of item names with tags). Always return at least 1 tag in array, and no more than 4. Prompt: {originalContent} """; - var backend = ModelRegistry.TryGetById(chat.ModelId, out var resolvedModel) - ? resolvedModel!.Backend - : settings.BackendType; - var llmService = llmServiceFactory.CreateService(backend); - var searchResult = await llmService.Send(chat, new ChatRequestOptions() { SaveConv = false @@ -134,7 +129,7 @@ Find tags that fits user query based on available knowledge (provided to you abo .Where(x => x.Tags.Intersect(matchedTags!).Any() || matchedTags!.Contains(x.Name)) .ToList(); - //NOTE: perhaps good idea for future to combine knowledge form MCP and from KM + //NOTE: perhaps good idea for future to combine knowledge form MCP and from KM var memoryOptions = new ChatMemoryOptions(); var mcpConfig = BuildMemoryOptionsFromKnowledgeItems(knowledgeItems, memoryOptions); diff --git a/src/MaIN.Services/Services/Steps/Commands/RedirectCommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/RedirectCommandHandler.cs index 23b194f1..c4eb7f4e 100644 --- a/src/MaIN.Services/Services/Steps/Commands/RedirectCommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/RedirectCommandHandler.cs @@ -1,5 +1,6 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions.Agents; using MaIN.Domain.Models.Abstract; using MaIN.Services.Services.Abstract; using MaIN.Services.Services.Models.Commands; @@ -12,7 +13,12 @@ public class RedirectCommandHandler(IAgentService agentService) : ICommandHandle public async Task HandleAsync(RedirectCommand command) { var chat = await agentService.GetChatByAgent(command.RelatedAgentId); - var backend = ModelRegistry.GetById(chat.ModelId).Backend; + if (!ModelRegistry.TryGetById(chat.ModelId, out var model)) + { + throw new AgentModelNotAvailableException(command.RelatedAgentId, chat.ModelId); + } + + var backend = model!.Backend; chat.Messages.Add(new Message() { Role = "User", diff --git a/src/MaIN.Services/Services/Steps/Commands/StartCommandHandler.cs b/src/MaIN.Services/Services/Steps/Commands/StartCommandHandler.cs index 4293e2f8..a3830944 100644 --- a/src/MaIN.Services/Services/Steps/Commands/StartCommandHandler.cs +++ b/src/MaIN.Services/Services/Steps/Commands/StartCommandHandler.cs @@ -1,6 +1,8 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; +using MaIN.Domain.Exceptions.Agents; using MaIN.Domain.Models.Abstract; +using MaIN.Services.Constants; using MaIN.Services.Services.Models.Commands; using MaIN.Services.Services.Steps.Commands.Abstract; @@ -15,7 +17,13 @@ public class StartCommandHandler : ICommandHandler return Task.FromResult(null); } - var backend = ModelRegistry.GetById(command.Chat.ModelId).Backend; + var agentId = command.Chat.Properties.GetValueOrDefault(ServiceConstants.Properties.AgentIdProperty, command.Chat.Id); + if (!ModelRegistry.TryGetById(command.Chat.ModelId, out var model)) + { + throw new AgentModelNotAvailableException(agentId, command.Chat.ModelId); + } + + var backend = model!.Backend; var message = new Message() { Content = command.InitialPrompt!, diff --git a/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs b/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs index 30eff706..894a67ca 100644 --- a/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs +++ b/src/MaIN.Services/Services/Steps/FechDataStepHandler.cs @@ -1,6 +1,7 @@ using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Exceptions; +using MaIN.Domain.Exceptions.Agents; using MaIN.Domain.Models.Abstract; using MaIN.Services.Mappers; using MaIN.Services.Services.Abstract; @@ -53,7 +54,12 @@ public async Task Handle(StepContext context) private static Chat CreateMemoryChat(StepContext context, string? filterVal) { - var backend = ModelRegistry.GetById(context.Chat.ModelId).Backend; + if (!ModelRegistry.TryGetById(context.Chat.ModelId, out var model)) + { + throw new AgentModelNotAvailableException(context.Agent.Id, context.Chat.ModelId); + } + + var backend = model!.Backend; return new Chat { Messages = From d43f4d2686578499f054c2fca8b2bb1c9a7c4c79 Mon Sep 17 00:00:00 2001 From: srebrek Date: Fri, 13 Mar 2026 13:28:38 +0100 Subject: [PATCH 9/9] Standarize AgentContext like ChatContext - examples use const model ids instead of magic strings - add flux id (the approach in examples will be refactored) --- .../Agents/AgentConversationExample.cs | 47 ++++++------ Examples/Examples/Agents/AgentExampleTools.cs | 3 +- .../Agents/AgentWithApiDataSourceExample.cs | 9 ++- .../Examples/Agents/AgentWithBecomeExample.cs | 5 +- .../Agents/AgentWithKnowledgeFileExample.cs | 26 ++++--- .../Agents/AgentWithKnowledgeWebExample.cs | 23 +++--- .../AgentWithWebDataSourceOpenAiExample.cs | 9 ++- .../Agents/AgentsTalkingToEachOther.cs | 15 ++-- .../Agents/AgentsWithRedirectExample.cs | 13 ++-- .../Agents/AgentsWithRedirectImageExample.cs | 18 +++-- .../Flows/AgentsComposedAsFlowExample.cs | 13 ++-- .../MultiBackendAgentsWithRedirectExample.cs | 15 ++-- .../Examples/Chat/ChatWithImageGenExample.cs | 6 +- .../Mcp/AgentWithKnowledgeMcpExample.cs | 29 +++++--- Examples/Examples/Mcp/McpAgentsExample.cs | 15 ++-- Examples/Examples/Mcp/McpExample.cs | 7 +- src/MaIN.Core.UnitTests/AgentContextTests.cs | 73 +++++++++++++------ src/MaIN.Core/Hub/Contexts/AgentContext.cs | 64 ++++++++-------- .../AgentContext/IAgentBuilderEntryPoint.cs | 15 +--- .../IAgentConfigurationBuilder.cs | 14 +--- src/MaIN.Domain/Models/Models.cs | 3 + 21 files changed, 224 insertions(+), 198 deletions(-) diff --git a/Examples/Examples/Agents/AgentConversationExample.cs b/Examples/Examples/Agents/AgentConversationExample.cs index fc860c61..4dff561c 100644 --- a/Examples/Examples/Agents/AgentConversationExample.cs +++ b/Examples/Examples/Agents/AgentConversationExample.cs @@ -4,21 +4,21 @@ namespace Examples.Agents; public class AgentConversationExample : IExample { - private static readonly ConsoleColor UserColor = ConsoleColor.Magenta; - private static readonly ConsoleColor AgentColor = ConsoleColor.Green; - private static readonly ConsoleColor SystemColor = ConsoleColor.Yellow; + private static readonly ConsoleColor _userColor = ConsoleColor.Magenta; + private static readonly ConsoleColor _agentColor = ConsoleColor.Green; + private static readonly ConsoleColor _systemColor = ConsoleColor.Yellow; public async Task Start() { - PrintColored("Agent conversation example is running!", SystemColor); - - PrintColored("Enter agent name: ", SystemColor, false); + PrintColored("Agent conversation example is running!", _systemColor); + + PrintColored("Enter agent name: ", _systemColor, false); var agentName = Console.ReadLine(); - - PrintColored("Enter agent profile (example: 'Gentle and helpful assistant'): ", SystemColor, false); + + PrintColored("Enter agent profile (example: 'Gentle and helpful assistant'): ", _systemColor, false); var agentProfile = Console.ReadLine(); - - PrintColored("Enter LLM model (ex: gemma3:4b, llama3.2:3b, yi:6b): ", SystemColor, false); + + PrintColored("Enter LLM model (ex: gemma3:4b, llama3.2:3b, yi:6b): ", _systemColor, false); var model = Console.ReadLine()!; var systemPrompt = $""" @@ -27,35 +27,35 @@ public async Task Start() Always stay in your role. """; - PrintColored($"Creating agent '{agentName}' with profile: '{agentProfile}' using model: '{model}'", SystemColor); + PrintColored($"Creating agent '{agentName}' with profile: '{agentProfile}' using model: '{model}'", _systemColor); AIHub.Extensions.DisableLLamaLogs(); AIHub.Extensions.DisableNotificationsLogs(); var context = await AIHub.Agent() .WithModel(model) .WithInitialPrompt(systemPrompt) .CreateAsync(interactiveResponse: true); - + bool conversationActive = true; while (conversationActive) { - PrintColored("You > ", UserColor, false); + PrintColored("You > ", _userColor, false); string userMessage = Console.ReadLine()!; - - if (userMessage.ToLower() == "exit" || userMessage.ToLower() == "quit") + + if (userMessage.ToLower() is "exit" or "quit") { conversationActive = false; continue; } - - PrintColored($"{agentName} > ", AgentColor, false); + + PrintColored($"{agentName} > ", _agentColor, false); await context.ProcessAsync(userMessage); - - Console.WriteLine(); + + Console.WriteLine(); } - - PrintColored("Conversation ended. Goodbye!", SystemColor); + + PrintColored("Conversation ended. Goodbye!", _systemColor); } - + private static void PrintColored(string message, ConsoleColor color, bool newLine = true) { Console.ForegroundColor = color; @@ -67,6 +67,7 @@ private static void PrintColored(string message, ConsoleColor color, bool newLin { Console.Write(message); } + Console.ResetColor(); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Agents/AgentExampleTools.cs b/Examples/Examples/Agents/AgentExampleTools.cs index ececfad7..03ceeb8c 100644 --- a/Examples/Examples/Agents/AgentExampleTools.cs +++ b/Examples/Examples/Agents/AgentExampleTools.cs @@ -1,6 +1,7 @@ using Examples.Utils; using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; +using MaIN.Domain.Models; namespace Examples.Agents; @@ -12,7 +13,7 @@ public async Task Start() Console.WriteLine("(Anthropic) Tool example is running!"); var context = await AIHub.Agent() - .WithModel("claude-sonnet-4-5-20250929") + .WithModel(Models.Anthropic.ClaudeSonnet4_5) .WithSteps(StepBuilder.Instance .Answer() .Build()) diff --git a/Examples/Examples/Agents/AgentWithApiDataSourceExample.cs b/Examples/Examples/Agents/AgentWithApiDataSourceExample.cs index b2a05fb6..558a8875 100644 --- a/Examples/Examples/Agents/AgentWithApiDataSourceExample.cs +++ b/Examples/Examples/Agents/AgentWithApiDataSourceExample.cs @@ -1,6 +1,7 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; using MaIN.Domain.Entities.Agents.AgentSource; +using MaIN.Domain.Models; namespace Examples.Agents; @@ -9,9 +10,9 @@ public class AgentWithApiDataSourceExample : IExample public async Task Start() { Console.WriteLine("Agent with api source"); - + var context = AIHub.Agent() - .WithModel("llama3.2:3b") + .WithModel(Models.Local.Llama3_2_3b) .WithInitialPrompt("Extract at least 4 jobs offers (try to include title, company name, salary and location if possible)") .WithSource(new AgentApiSourceDetails() { @@ -24,9 +25,9 @@ public async Task Start() .FetchData() .Build()) .Create(); - + var result = await context .ProcessAsync("I am looking for work as javascript developer"); Console.WriteLine(result.Message.Content); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Agents/AgentWithBecomeExample.cs b/Examples/Examples/Agents/AgentWithBecomeExample.cs index 58b49e5c..90d2e2c6 100644 --- a/Examples/Examples/Agents/AgentWithBecomeExample.cs +++ b/Examples/Examples/Agents/AgentWithBecomeExample.cs @@ -1,6 +1,7 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; using MaIN.Domain.Entities.Agents.AgentSource; +using MaIN.Domain.Models; namespace Examples.Agents; @@ -9,7 +10,7 @@ public class AgentWithBecomeExample : IExample public async Task Start() { var becomeAgent = AIHub.Agent() - .WithModel("llama3.1:8b") + .WithModel(Models.Local.Llama3_1_8b) .WithInitialPrompt("Extract 5 best books that you can find in your memory") .WithSource(new AgentFileSourceDetails { @@ -41,4 +42,4 @@ public async Task Start() await becomeAgent .ProcessAsync("I am looking for good fantasy book to buy"); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Agents/AgentWithKnowledgeFileExample.cs b/Examples/Examples/Agents/AgentWithKnowledgeFileExample.cs index ffd55025..5667d3b8 100644 --- a/Examples/Examples/Agents/AgentWithKnowledgeFileExample.cs +++ b/Examples/Examples/Agents/AgentWithKnowledgeFileExample.cs @@ -1,5 +1,6 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; +using MaIN.Domain.Models; namespace Examples.Agents; @@ -10,16 +11,17 @@ public async Task Start() Console.WriteLine("Agent with knowledge base example"); AIHub.Extensions.DisableLLamaLogs(); var context = await AIHub.Agent() - .WithModel("gemma3:4b") - .WithInitialPrompt(""" - You are a helpful assistant that answers questions about a company. Try to - help employees find answers to their questions. Company you work for is TechVibe Solutions. - """) + .WithModel(Models.Local.Gemma3_4b) + .WithInitialPrompt( + """ + You are a helpful assistant that answers questions about a company. Try to + help employees find answers to their questions. Company you work for is TechVibe Solutions. + """) .WithKnowledge(KnowledgeBuilder.Instance - .AddFile("people.md", "./Files/Knowledge/people.md", + .AddFile("people.md", "./Files/Knowledge/people.md", tags: ["workers", "employees", "company"]) .AddFile("organization.md", "./Files/Knowledge/organization.md", - tags:["company structure", "company policy", "company culture", "company overview"]) + tags: ["company structure", "company policy", "company culture", "company overview"]) .AddFile("events.md", "./Files/Knowledge/events.md", tags: ["company events", "company calendar", "company agenda"]) .AddFile("office_layout.md", "./Files/Knowledge/office_layout.md", @@ -28,10 +30,10 @@ You are a helpful assistant that answers questions about a company. Try to .AnswerUseKnowledge() .Build()) .CreateAsync(); - - var result = await context - .ProcessAsync("Hey! Where I can find some printer paper?"); - Console.WriteLine(result.Message.Content);; + + var result = await context.ProcessAsync("Hey! Where I can find some printer paper?"); + Console.WriteLine(result.Message.Content); + ; } -} \ No newline at end of file +} diff --git a/Examples/Examples/Agents/AgentWithKnowledgeWebExample.cs b/Examples/Examples/Agents/AgentWithKnowledgeWebExample.cs index 8844e061..b7d979bb 100644 --- a/Examples/Examples/Agents/AgentWithKnowledgeWebExample.cs +++ b/Examples/Examples/Agents/AgentWithKnowledgeWebExample.cs @@ -1,6 +1,7 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; using MaIN.Domain.Entities; +using MaIN.Domain.Models; namespace Examples.Agents; @@ -12,14 +13,15 @@ public async Task Start() AIHub.Extensions.DisableLLamaLogs(); var context = await AIHub.Agent() - .WithModel("llama3.2:3b") - .WithMemoryParams(new MemoryParams(){ContextSize = 4096}) - .WithInitialPrompt(""" - You are an expert piano instructor specializing in teaching specific pieces, - techniques, and solving common playing problems. Help students learn exact - fingerings, chord progressions, and troubleshoot technical issues with - detailed, step-by-step guidance for both classical and popular music. - """) + .WithModel(Models.Local.Llama3_2_3b) + .WithMemoryParams(new MemoryParams() { ContextSize = 4096 }) + .WithInitialPrompt( + """ + You are an expert piano instructor specializing in teaching specific pieces, + techniques, and solving common playing problems. Help students learn exact + fingerings, chord progressions, and troubleshoot technical issues with + detailed, step-by-step guidance for both classical and popular music. + """) .WithKnowledge(KnowledgeBuilder.Instance .AddUrl("piano_scales_major", "https://www.pianoscales.org/major.html", tags: ["scale_fingerings", "c_major_scale", "d_major_scale", "fingering_patterns"]) @@ -43,9 +45,8 @@ public async Task Start() .Build()) .CreateAsync(); - var result = await context - .ProcessAsync("I want to learn the C major scale. What's the exact fingering pattern for both hands?" + "I want short and concrete answer"); + var result = await context.ProcessAsync("I want to learn the C major scale. What's the exact fingering pattern for both hands?" + "I want short and concrete answer"); Console.WriteLine(result.Message.Content); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Agents/AgentWithWebDataSourceOpenAiExample.cs b/Examples/Examples/Agents/AgentWithWebDataSourceOpenAiExample.cs index a1d0b585..2302a6c3 100644 --- a/Examples/Examples/Agents/AgentWithWebDataSourceOpenAiExample.cs +++ b/Examples/Examples/Agents/AgentWithWebDataSourceOpenAiExample.cs @@ -2,6 +2,7 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; using MaIN.Domain.Entities.Agents.AgentSource; +using MaIN.Domain.Models; namespace Examples.Agents; @@ -10,11 +11,11 @@ public class AgentWithWebDataSourceOpenAiExample : IExample public async Task Start() { Console.WriteLine("Agent with web source (OpenAi)"); - + OpenAiExample.Setup(); //We need to provide OpenAi API key var context = await AIHub.Agent() - .WithModel("gpt-4o-mini") + .WithModel(Models.OpenAi.Gpt4oMini) .WithInitialPrompt($"Find useful information about daily news, try to include title, description and link.") .WithBehaviour("Journalist", $"Base on data provided in chat find useful information about what happen today. Build it in form of newsletter - Name of newsletter is MaIN_Letter and today`s date is {DateTime.UtcNow}") .WithSource(new AgentWebSourceDetails() @@ -27,9 +28,9 @@ public async Task Start() .Answer() .Build()) .CreateAsync(interactiveResponse: true); - + await context .ProcessAsync("Provide today's newsletter"); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Agents/AgentsTalkingToEachOther.cs b/Examples/Examples/Agents/AgentsTalkingToEachOther.cs index a2ca37f2..50971ca2 100644 --- a/Examples/Examples/Agents/AgentsTalkingToEachOther.cs +++ b/Examples/Examples/Agents/AgentsTalkingToEachOther.cs @@ -1,5 +1,6 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; +using MaIN.Domain.Models; namespace Examples.Agents; @@ -15,7 +16,7 @@ public async Task Start() You prioritize kindness, patience, and understanding in every interaction. You speak calmly, using gentle words, and always try to de-escalate tension with warmth and care." """; - + var systemPromptSecond = """ You are intense, blunt, and always on edge. Your tone is sharp, impatient, and confrontational. @@ -24,18 +25,18 @@ and always try to de-escalate tension with warmth and care." """; var idFirst = Guid.NewGuid().ToString(); - + var contextSecond = AIHub.Agent() - .WithModel("gemma2:2b") + .WithModel(Models.Local.Gemma2_2b) .WithInitialPrompt(systemPromptSecond) .WithSteps(StepBuilder.Instance .Answer() .Redirect(agentId: idFirst, mode: "USER") .Build()) .Create(interactiveResponse: true); - + var context = AIHub.Agent() - .WithModel("llama3.2:3b") + .WithModel(Models.Local.Llama3_2_3b) .WithId(idFirst) .WithInitialPrompt(systemPrompt) .WithSteps(StepBuilder.Instance @@ -43,9 +44,9 @@ and always try to de-escalate tension with warmth and care." .Redirect(agentId: contextSecond.GetAgentId(), mode: "USER") .Build()) .Create(interactiveResponse: true); - + await context .ProcessAsync("Introduce yourself, and start conversation!"); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Agents/AgentsWithRedirectExample.cs b/Examples/Examples/Agents/AgentsWithRedirectExample.cs index 9f462c79..8c05361f 100644 --- a/Examples/Examples/Agents/AgentsWithRedirectExample.cs +++ b/Examples/Examples/Agents/AgentsWithRedirectExample.cs @@ -1,5 +1,6 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; +using MaIN.Domain.Models; namespace Examples.Agents; @@ -15,7 +16,7 @@ public async Task Start() evocative, and rich in imagery. Maintain a graceful rhythm, sophisticated vocabulary, and a touch of timeless beauty in every poem you compose. """; - + var systemPromptSecond = """ You are a modern rap lyricist with a sharp, streetwise flow. Take the given poem and transform @@ -25,21 +26,21 @@ You need to use a lot of it. Imagine you are the voice of youth. """; var contextSecond = AIHub.Agent() - .WithModel("gemma2:2b") + .WithModel(Models.Local.Gemma2_2b) .WithInitialPrompt(systemPromptSecond) .Create(interactiveResponse: true); - + var context = AIHub.Agent() - .WithModel("llama3.2:3b") + .WithModel(Models.Local.Llama3_2_3b) .WithInitialPrompt(systemPrompt) .WithSteps(StepBuilder.Instance .Answer() .Redirect(agentId: contextSecond.GetAgentId()) .Build()) .Create(); - + await context .ProcessAsync("Write a poem about distant future"); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Agents/AgentsWithRedirectImageExample.cs b/Examples/Examples/Agents/AgentsWithRedirectImageExample.cs index bfd7b10e..955f1de3 100644 --- a/Examples/Examples/Agents/AgentsWithRedirectImageExample.cs +++ b/Examples/Examples/Agents/AgentsWithRedirectImageExample.cs @@ -2,6 +2,8 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; using MaIN.Domain.Entities; +using MaIN.Domain.Models; +using MaIN.Domain.Models.Abstract; using FileInfo = MaIN.Domain.Entities.FileInfo; namespace Examples.Agents; @@ -12,32 +14,34 @@ public async Task Start() { Console.WriteLine("Basic agent&friends with images example is running!"); + ModelRegistry.RegisterOrReplace(new GenericLocalModel(Models.Local.Flux1Shnell)); + var systemPrompt = """ You analyze a stored PDF and generate an image prompt. Your output must be a single prompt with a maximum of 10 words. Do not include any explanations, context, or extra text—only the prompt itself. Avoid mentioning specific characters or names; focus on the topic and context. """; - + var systemPromptSecond = """ Generate image based on given prompt """; var contextSecond = AIHub.Agent() - .WithModel("FLUX.1_Shnell") + .WithModel(Models.Local.Flux1Shnell) .WithInitialPrompt(systemPromptSecond) .Create(); - + var context = AIHub.Agent() - .WithModel("llama3.2:3b") + .WithModel(Models.Local.Llama3_2_3b) .WithInitialPrompt(systemPrompt) .WithSteps(StepBuilder.Instance .Answer() .Redirect(agentId: contextSecond.GetAgentId()) .Build()) .Create(interactiveResponse: true); - + var result = await context .ProcessAsync(new Message() { @@ -51,7 +55,7 @@ Generate image based on given prompt Path = "./Files/Nicolaus_Copernicus.pdf" }] }); - + ImagePreview.ShowImage(result.Message.Image); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Agents/Flows/AgentsComposedAsFlowExample.cs b/Examples/Examples/Agents/Flows/AgentsComposedAsFlowExample.cs index 5ba8f6ab..96ef9ee5 100644 --- a/Examples/Examples/Agents/Flows/AgentsComposedAsFlowExample.cs +++ b/Examples/Examples/Agents/Flows/AgentsComposedAsFlowExample.cs @@ -1,5 +1,6 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; +using MaIN.Domain.Models; namespace Examples.Agents.Flows; @@ -18,7 +19,7 @@ public async Task Start() evocative, and rich in imagery. Maintain a graceful rhythm, sophisticated vocabulary, and a touch of timeless beauty in every poem you compose. """; - + var systemPromptSecond = """ You are a modern rap lyricist with a sharp, streetwise flow. Take the given poem and transform @@ -28,12 +29,12 @@ You need to use a lot of it. Imagine you are the voice of youth. """; var contextSecond = AIHub.Agent() - .WithModel("gemma2:2b") + .WithModel(Models.Local.Gemma2_2b) .WithInitialPrompt(systemPromptSecond) .Create(interactiveResponse: true); - + var contextFirst = AIHub.Agent() - .WithModel("llama3.2:3b") + .WithModel(Models.Local.Llama3_2_3b) .WithInitialPrompt(systemPrompt) .WithSteps(StepBuilder.Instance .Answer() @@ -49,9 +50,9 @@ You need to use a lot of it. Imagine you are the voice of youth. contextSecond.GetAgent() ]) .Save("./poetry.zip"); - + await flowContext .ProcessAsync("Write a poem about distant future"); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Agents/MultiBackendAgentsWithRedirectExample.cs b/Examples/Examples/Agents/MultiBackendAgentsWithRedirectExample.cs index 07b4d8b8..c201b817 100644 --- a/Examples/Examples/Agents/MultiBackendAgentsWithRedirectExample.cs +++ b/Examples/Examples/Agents/MultiBackendAgentsWithRedirectExample.cs @@ -1,6 +1,6 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; -using MaIN.Domain.Configuration; +using MaIN.Domain.Models; namespace Examples.Agents; @@ -16,7 +16,7 @@ public async Task Start() evocative, and rich in imagery. Maintain a graceful rhythm, sophisticated vocabulary, and a touch of timeless beauty in every poem you compose. """; - + var systemPromptSecond = """ You are a modern rap lyricist with a sharp, streetwise flow. Take the given poem and transform @@ -26,22 +26,21 @@ You need to use a lot of it. Imagine you are the voice of youth. """; var contextSecond = await AIHub.Agent() - .WithModel("gpt-4o") + .WithModel(Models.OpenAi.Gpt4oMini) .WithInitialPrompt(systemPromptSecond) - .WithBackend(BackendType.OpenAi) .CreateAsync(interactiveResponse: true); - + var context = await AIHub.Agent() - .WithModel("gemma2:2b") + .WithModel(Models.Local.Gemma2_2b) .WithInitialPrompt(systemPrompt) .WithSteps(StepBuilder.Instance .Answer() .Redirect(agentId: contextSecond.GetAgentId()) .Build()) .CreateAsync(); - + await context .ProcessAsync("Write a poem about distant future"); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Chat/ChatWithImageGenExample.cs b/Examples/Examples/Chat/ChatWithImageGenExample.cs index 20dd3024..56a6d9e4 100644 --- a/Examples/Examples/Chat/ChatWithImageGenExample.cs +++ b/Examples/Examples/Chat/ChatWithImageGenExample.cs @@ -1,5 +1,6 @@ using Examples.Utils; using MaIN.Core.Hub; +using MaIN.Domain.Models; using MaIN.Domain.Models.Abstract; namespace Examples.Chat; @@ -10,10 +11,9 @@ public async Task Start() { Console.WriteLine("ChatExample with image gen is running!"); - var fluxModel = new GenericLocalModel("FLUX.1_Shnell"); - ModelRegistry.RegisterOrReplace(fluxModel); + ModelRegistry.RegisterOrReplace(new GenericLocalModel(Models.Local.Flux1Shnell)); var result = await AIHub.Chat() - .WithModel(fluxModel.Id) + .WithModel(Models.Local.Flux1Shnell) .WithMessage("Generate cyberpunk godzilla cat warrior") .CompleteAsync(); diff --git a/Examples/Examples/Mcp/AgentWithKnowledgeMcpExample.cs b/Examples/Examples/Mcp/AgentWithKnowledgeMcpExample.cs index a9a3c544..d81cb9f8 100644 --- a/Examples/Examples/Mcp/AgentWithKnowledgeMcpExample.cs +++ b/Examples/Examples/Mcp/AgentWithKnowledgeMcpExample.cs @@ -1,6 +1,7 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; using MaIN.Domain.Configuration; +using MaIN.Domain.Models; namespace Examples.Mcp; @@ -14,17 +15,16 @@ public async Task Start() AIHub.Extensions.DisableLLamaLogs(); var context = await AIHub.Agent() - .WithModel("gpt-4.1-mini") - .WithBackend(BackendType.OpenAi) + .WithModel(Models.OpenAi.Gpt4_1Mini) .WithKnowledge(KnowledgeBuilder.Instance .AddMcp(new MaIN.Domain.Entities.Mcp { Name = "ExaDeepSearch", Arguments = ["-y", "exa-mcp-server"], Command = "npx", - EnvironmentVariables = {{"EXA_API_KEY",""}}, + EnvironmentVariables = { { "EXA_API_KEY", "" } }, Backend = BackendType.Gemini, - Model = "gemini-2.0-flash" + Model = Models.Gemini.Gemini2_0Flash }, ["search", "browser", "web access", "research"]) .AddMcp(new MaIN.Domain.Entities.Mcp { @@ -36,7 +36,7 @@ public async Task Start() "C:\\WiseDev" //Align paths to fit your system ], //Align paths to fit your system Backend = BackendType.GroqCloud, - Model = "openai/gpt-oss-20b" + Model = Models.Groq.GptOss20b }, ["filesystem", "file operations", "read write", "disk search"]) .AddMcp(new MaIN.Domain.Entities.Mcp { @@ -44,7 +44,7 @@ public async Task Start() Command = "npx", Arguments = ["octocode-mcp"], Backend = BackendType.OpenAi, - Model = "gpt-5-nano" + Model = Models.OpenAi.Gpt5Nano }, ["code", "github", "repository", "packages", "npm"])) .WithSteps(StepBuilder.Instance .AnswerUseKnowledge() @@ -58,15 +58,22 @@ public async Task Start() Console.ForegroundColor = ConsoleColor.Blue; Console.Write("You: "); Console.ResetColor(); - + var input = Console.ReadLine(); - - if (input?.ToLower() == "exit") break; - if (string.IsNullOrWhiteSpace(input)) continue; + + if (input?.ToLower() == "exit") + { + break; + } + + if (string.IsNullOrWhiteSpace(input)) + { + continue; + } Console.ForegroundColor = ConsoleColor.Green; Console.Write("Agent: "); - + var result = await context.ProcessAsync(input); Console.WriteLine(result.Message.Content); Console.ResetColor(); diff --git a/Examples/Examples/Mcp/McpAgentsExample.cs b/Examples/Examples/Mcp/McpAgentsExample.cs index 44ee7f81..312c57ae 100644 --- a/Examples/Examples/Mcp/McpAgentsExample.cs +++ b/Examples/Examples/Mcp/McpAgentsExample.cs @@ -1,6 +1,6 @@ using MaIN.Core.Hub; using MaIN.Core.Hub.Utils; -using MaIN.Domain.Configuration; +using MaIN.Domain.Models; namespace Examples.Mcp; @@ -12,13 +12,12 @@ public async Task Start() AIHub.Extensions.DisableLLamaLogs(); var contextSecond = await AIHub.Agent() - .WithModel("qwq:7b") + .WithModel(Models.Local.QwQ_7b) .WithInitialPrompt("Your main role is to provide opinions about facts that you are given in a conversation.") .CreateAsync(interactiveResponse: true); - + var context = await AIHub.Agent() - .WithModel("gpt-4o-mini") - .WithBackend(BackendType.OpenAi) + .WithModel(Models.OpenAi.Gpt4oMini) .WithMcpConfig(new MaIN.Domain.Entities.Mcp { Name = "GitHub", @@ -28,14 +27,14 @@ public async Task Start() {"GITHUB_PERSONAL_ACCESS_TOKEN", ""} }, Command = "docker", - Model = "gpt-4o-mini" + Model = Models.OpenAi.Gpt4oMini }) .WithSteps(StepBuilder.Instance .Mcp() .Redirect(agentId: contextSecond.GetAgentId()) .Build()) .CreateAsync(); - + await context.ProcessAsync("What are recently added features in https://github.com/wisedev-code/MaIN.NET (based on recently closed issues)", translate: true); } -} \ No newline at end of file +} diff --git a/Examples/Examples/Mcp/McpExample.cs b/Examples/Examples/Mcp/McpExample.cs index e3b97a24..6ebddcce 100644 --- a/Examples/Examples/Mcp/McpExample.cs +++ b/Examples/Examples/Mcp/McpExample.cs @@ -1,6 +1,7 @@ using Examples.Utils; using MaIN.Core.Hub; using MaIN.Domain.Configuration; +using MaIN.Domain.Models; namespace Examples.Mcp; @@ -19,10 +20,10 @@ public async Task Start() Name = "McpEverythingDemo", Arguments = ["-y", "@modelcontextprotocol/server-everything"], Command = "npx", - Model = "gpt-4o-mini" + Model = Models.OpenAi.Gpt4oMini }) .PromptAsync("Provide me information about resource 21 and 37. Also explain how you get this data"); - + Console.WriteLine(result.Message.Content); } -} \ No newline at end of file +} diff --git a/src/MaIN.Core.UnitTests/AgentContextTests.cs b/src/MaIN.Core.UnitTests/AgentContextTests.cs index a055ba46..27b8a7af 100644 --- a/src/MaIN.Core.UnitTests/AgentContextTests.cs +++ b/src/MaIN.Core.UnitTests/AgentContextTests.cs @@ -30,7 +30,7 @@ public void Constructor_ShouldInitializeNewAgent() // Assert var agentId = _agentContext.GetAgentId(); var agent = _agentContext.GetAgent(); - + Assert.NotNull(agentId); Assert.NotEmpty(agentId); Assert.NotNull(agent); @@ -70,14 +70,11 @@ public void WithName_ShouldSetAgentName() [Fact] public void WithModel_ShouldSetAgentModel() { - // Arrange - var expectedModel = "gpt-4"; - // Act - var result = _agentContext.WithModel(expectedModel); + var result = _agentContext.WithModel(_testModelId); // Assert - Assert.Equal(expectedModel, _agentContext.GetAgent().Model); + Assert.Equal(_testModelId, _agentContext.GetAgent().Model); Assert.Equal(result, _agentContext); } @@ -131,12 +128,17 @@ public void WithBehaviour_ShouldAddBehaviourAndSetCurrent() public async Task CreateAsync_ShouldCallAgentServiceCreateAgent() { // Arrange - var agent = new Agent() {Id = Guid.NewGuid().ToString(), CurrentBehaviour = "Default", Context = new AgentData()}; + var agent = new Agent() + { + Id = Guid.NewGuid().ToString(), + CurrentBehaviour = "Default", + Context = new AgentData() + }; _mockAgentService .Setup(s => s.CreateAgent( - It.IsAny(), - It.IsAny(), - It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) @@ -148,9 +150,9 @@ public async Task CreateAsync_ShouldCallAgentServiceCreateAgent() // Assert _mockAgentService.Verify( s => s.CreateAgent( - It.IsAny(), - It.Is(f => f == true), - It.Is(r => r == false), + It.IsAny(), + It.Is(f => f == true), + It.Is(r => r == false), It.IsAny(), It.IsAny(), It.IsAny()), @@ -163,8 +165,18 @@ public async Task ProcessAsync_WithStringMessage_ShouldReturnChatResult() { // Arrange var message = "Hello, agent!"; - var chat = new Chat { Id = _agentContext.GetAgentId(), Messages = new List(), Name = "test", ModelId = _testModelId}; - var chatResult = new ChatResult { Done = true, Model = "test-model", Message = new Message + var chat = new Chat + { + Id = _agentContext.GetAgentId(), + Messages = [], + Name = "test", + ModelId = _testModelId + }; + var chatResult = new ChatResult + { + Done = true, + Model = "test-model", + Message = new Message { Role = "Assistant", Content = "Response", @@ -177,13 +189,20 @@ public async Task ProcessAsync_WithStringMessage_ShouldReturnChatResult() .ReturnsAsync(chat); _mockAgentService - .Setup(s => s.Process(It.IsAny(), _agentContext.GetAgentId(), It.IsAny(), It.IsAny(), null, null)) - .ReturnsAsync(new Chat { - ModelId = "test-model", + .Setup(s => s.Process( + It.IsAny(), + _agentContext.GetAgentId(), + It.IsAny(), + It.IsAny(), + null, + null)) + .ReturnsAsync(new Chat + { + ModelId = "test-model", Name = "test", - Messages = new List { - new Message { Content = "Response", Role = "Assistant", Type = MessageType.LocalLLM} - } + Messages = [ + new Message { Content = "Response", Role = "Assistant", Type = MessageType.LocalLLM} + ] }); // Act @@ -200,7 +219,13 @@ public async Task FromExisting_ShouldCreateContextFromExistingAgent() { // Arrange var existingAgentId = "existing-agent-id"; - var existingAgent = new Agent { Id = existingAgentId, Name = "Existing Agent", CurrentBehaviour = "Default", Context = new AgentData() }; + var existingAgent = new Agent + { + Id = existingAgentId, + Name = "Existing Agent", + CurrentBehaviour = "Default", + Context = new AgentData() + }; _mockAgentService .Setup(s => s.GetAgentById(existingAgentId)) @@ -225,7 +250,7 @@ public async Task FromExisting_ShouldThrowArgumentExceptionWhenAgentNotFound() .ReturnsAsync((Agent)null!); // Act & Assert - await Assert.ThrowsAsync(() => + await Assert.ThrowsAsync(() => AgentContext.FromExisting(_mockAgentService.Object, nonExistentAgentId)); } -} \ No newline at end of file +} diff --git a/src/MaIN.Core/Hub/Contexts/AgentContext.cs b/src/MaIN.Core/Hub/Contexts/AgentContext.cs index a5dec747..c4c259d6 100644 --- a/src/MaIN.Core/Hub/Contexts/AgentContext.cs +++ b/src/MaIN.Core/Hub/Contexts/AgentContext.cs @@ -1,6 +1,5 @@ using MaIN.Core.Hub.Contexts.Interfaces.AgentContext; using MaIN.Core.Hub.Utils; -using MaIN.Domain.Configuration; using MaIN.Domain.Entities; using MaIN.Domain.Entities.Agents; using MaIN.Domain.Entities.Agents.AgentSource; @@ -31,7 +30,7 @@ internal AgentContext(IAgentService agentService) _agent = new Agent { Id = Guid.NewGuid().ToString(), - Behaviours = new Dictionary(), + Behaviours = [], Name = $"Agent-{Guid.NewGuid()}", Description = "Agent created by MaIN", CurrentBehaviour = "Default", @@ -63,27 +62,20 @@ internal AgentContext(IAgentService agentService, Agent existingAgent) public async Task Delete() => await _agentService.DeleteAgent(_agent.Id); public async Task Exists() => await _agentService.AgentExists(_agent.Id); - - public IAgentConfigurationBuilder WithModel(string model) + public IAgentConfigurationBuilder WithModel(string modelId) { - _agent.Model = model; - return this; - } + if (!ModelRegistry.Exists(modelId)) + { + throw new AgentModelNotAvailableException(_agent.Id, modelId); + } - public IAgentConfigurationBuilder WithCustomModel(string model, string path, string? mmProject = null) - { - KnownModels.AddModel(model, path, mmProject); - _agent.Model = model; + _agent.Model = modelId; return this; } public async Task FromExisting(string agentId) { - var existingAgent = await _agentService.GetAgentById(agentId); - if (existingAgent == null) - { - throw new AgentNotFoundException(agentId); - } + var existingAgent = await _agentService.GetAgentById(agentId) ?? throw new AgentNotFoundException(agentId); var context = new AgentContext(_agentService, existingAgent); context.LoadExistingKnowledgeIfExists(); @@ -136,18 +128,13 @@ public IAgentConfigurationBuilder WithName(string name) return this; } - public IAgentConfigurationBuilder WithBackend(BackendType backendType) - { - _agent.Backend = backendType; - return this; - } - public IAgentConfigurationBuilder WithMcpConfig(Mcp mcpConfig) { - if (_agent.Backend != null) + if (mcpConfig.Backend is null && ModelRegistry.Exists(_agent.Model)) { - mcpConfig.Backend = _agent.Backend; + mcpConfig.Backend = ModelRegistry.GetById(_agent.Model).Backend; } + _agent.Context.McpConfig = mcpConfig; return this; } @@ -200,7 +187,7 @@ public IAgentConfigurationBuilder WithInMemoryKnowledge(Func(); + _agent.Behaviours ??= []; _agent.Behaviours[name] = instruction; _agent.CurrentBehaviour = name; return this; @@ -245,7 +232,7 @@ internal void LoadExistingKnowledgeIfExists() public async Task ProcessAsync(Chat chat, bool translate = false) { - if (_knowledge == null) + if (_knowledge is null) { LoadExistingKnowledgeIfExists(); } @@ -267,16 +254,17 @@ public async Task ProcessAsync( Func? tokenCallback = null, Func? toolCallback = null) { - if (_knowledge == null) + if (_knowledge is null) { LoadExistingKnowledgeIfExists(); } + var chat = await _agentService.GetChatByAgent(_agent.Id); chat.Messages.Add(new Message() { Content = message, Role = "User", - Type = MessageType.LocalLLM, + Type = MessageType.NotSet, Time = DateTime.Now }); var result = await _agentService.Process(chat, _agent.Id, _knowledge, translate, tokenCallback, toolCallback); @@ -295,10 +283,11 @@ public async Task ProcessAsync(Message message, Func? tokenCallback = null, Func? toolCallback = null) { - if (_knowledge == null) + if (_knowledge is null) { LoadExistingKnowledgeIfExists(); } + var chat = await _agentService.GetChatByAgent(_agent.Id); chat.Messages.Add(message); var result = await _agentService.Process(chat, _agent.Id, _knowledge, translate, tokenCallback, toolCallback); @@ -318,17 +307,21 @@ public async Task ProcessAsync( Func? tokenCallback = null, Func? toolCallback = null) { - if (_knowledge == null) + if (_knowledge is null) { LoadExistingKnowledgeIfExists(); } + var chat = await _agentService.GetChatByAgent(_agent.Id); - var systemMsg = chat.Messages.FirstOrDefault(m => m.Role.Equals(ServiceConstants.Roles.System, StringComparison.InvariantCultureIgnoreCase)); + var systemMsg = chat.Messages.FirstOrDefault(m => m.Role.Equals( + ServiceConstants.Roles.System, + StringComparison.InvariantCultureIgnoreCase)); chat.Messages.Clear(); - if (systemMsg != null) + if (systemMsg is not null) { chat.Messages.Add(systemMsg); } + chat.Messages.AddRange(messages); var result = await _agentService.Process(chat, _agent.Id, _knowledge, translate, tokenCallback, toolCallback); var messageResult = result.Messages.LastOrDefault()!; @@ -344,7 +337,7 @@ public async Task ProcessAsync( public static async Task FromExisting(IAgentService agentService, string agentId) { var existingAgent = await agentService.GetAgentById(agentId); - if (existingAgent == null) + if (existingAgent is null) { throw new AgentNotFoundException(agentId); } @@ -363,10 +356,11 @@ public static async Task ProcessAsync( bool translate = false) { var agent = await agentTask; - if (agent._knowledge == null) + if (agent._knowledge is null) { agent.LoadExistingKnowledgeIfExists(); } + return await agent.ProcessAsync(message, translate); } -} \ No newline at end of file +} diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentBuilderEntryPoint.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentBuilderEntryPoint.cs index 85d42601..bde0ef5b 100644 --- a/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentBuilderEntryPoint.cs +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentBuilderEntryPoint.cs @@ -1,4 +1,4 @@ -namespace MaIN.Core.Hub.Contexts.Interfaces.AgentContext; +namespace MaIN.Core.Hub.Contexts.Interfaces.AgentContext; public interface IAgentBuilderEntryPoint : IAgentActions { @@ -8,20 +8,11 @@ public interface IAgentBuilderEntryPoint : IAgentActions /// The name or identifier of the AI model to be used. /// The context instance implementing for method chaining. IAgentConfigurationBuilder WithModel(string model); - - /// - /// Specifies a custom model along with its file path for use by the agent. This allows using locally stored models in addition to predefined ones. - /// - /// The name of the custom model. - /// The path to the custom model’s file. - /// Optional multi-modal project identifier. - /// The context instance implementing for method chaining. - IAgentConfigurationBuilder WithCustomModel(string model, string path, string? mmProject = null); - + /// /// Fetches an existing agent by its ID, allowing you to create a new from an already existing agent. /// /// The unique identifier of the Agent to load. /// The context instance implementing for method chaining. Task FromExisting(string agentId); -} \ No newline at end of file +} diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentConfigurationBuilder.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentConfigurationBuilder.cs index d5fda2d1..27d81068 100644 --- a/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentConfigurationBuilder.cs +++ b/src/MaIN.Core/Hub/Contexts/Interfaces/AgentContext/IAgentConfigurationBuilder.cs @@ -1,5 +1,4 @@ -using MaIN.Core.Hub.Utils; -using MaIN.Domain.Configuration; +using MaIN.Core.Hub.Utils; using MaIN.Domain.Entities; using MaIN.Domain.Entities.Agents.AgentSource; using MaIN.Domain.Entities.Agents.Knowledge; @@ -23,7 +22,7 @@ public interface IAgentConfigurationBuilder : IAgentActions /// The initial prompt or instruction for the agent. /// The context instance implementing for method chaining. IAgentConfigurationBuilder WithInitialPrompt(string prompt); - + /// /// Assigns a unique identifier to the agent. /// @@ -60,13 +59,6 @@ public interface IAgentConfigurationBuilder : IAgentActions /// The context instance implementing for method chaining. IAgentConfigurationBuilder WithName(string name); - /// - /// Defines backend that will be used for model inference. - /// - /// The - an enum that defines which AI backend to use. - /// The context instance implementing for method chaining. - IAgentConfigurationBuilder WithBackend(BackendType backendType); - /// /// Configures integration with the Model Context Protocol (MCP). /// @@ -161,4 +153,4 @@ public interface IAgentConfigurationBuilder : IAgentActions /// A flag indicating whether the agent should generate interactive responses. /// The context instance implementing for method chaining. Task CreateAsync(bool flow = false, bool interactiveResponse = false); -} \ No newline at end of file +} diff --git a/src/MaIN.Domain/Models/Models.cs b/src/MaIN.Domain/Models/Models.cs index b87a30f1..bd79619a 100644 --- a/src/MaIN.Domain/Models/Models.cs +++ b/src/MaIN.Domain/Models/Models.cs @@ -100,5 +100,8 @@ public static class Local // TTS public const string Kokoro82m = "kokoro-82m"; + + // Image Generation + public const string Flux1Shnell = "FLUX.1_Shnell"; } }