From c1826426d90bed17032018292302b128a5caf0c8 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 26 Aug 2025 10:59:56 +0200 Subject: [PATCH] Refactored configuration objects (#538) --- app/MindWork AI Studio/Agents/AgentBase.cs | 7 +- .../Agents/AgentDataSourceSelection.cs | 7 +- .../Agents/AgentRetrievalContextValidation.cs | 7 +- .../Agents/AgentTextContentCleaner.cs | 2 +- app/MindWork AI Studio/Agents/IAgent.cs | 2 +- .../Assistants/AssistantBase.razor.cs | 4 +- .../Assistants/I18N/allTexts.lua | 7 +- .../Components/ChatComponent.razor.cs | 8 +- .../ConfigurationProviderSelection.razor.cs | 3 + .../Components/ProviderSelection.razor.cs | 9 +- .../Components/ReadWebContent.razor.cs | 10 +- .../Settings/SettingsPanelProviders.razor.cs | 3 + app/MindWork AI Studio/Pages/Chat.razor.cs | 2 +- app/MindWork AI Studio/Pages/Writer.razor.cs | 2 +- .../plugin.lua | 7 +- .../plugin.lua | 7 +- .../Settings/ChatTemplate.cs | 107 +++++- .../Settings/ExpressionExtensions.cs | 26 -- app/MindWork AI Studio/Settings/Provider.cs | 115 ++++++- .../Settings/SettingsManager.cs | 16 +- .../Tools/ComponentsExtensions.cs | 51 +-- .../Tools/ExpressionExtensions.cs | 96 ++++++ .../Tools/IncrementResult.cs | 10 + app/MindWork AI Studio/Tools/IncrementType.cs | 14 + .../PluginSystem/ConfigurationBaseObject.cs | 23 ++ .../PluginSystem/IConfigurationObject.cs | 32 ++ .../PluginSystem/NoConfigurationObject.cs | 26 ++ .../Tools/PluginSystem/PluginConfiguration.cs | 321 +----------------- .../PluginSystem/PluginConfigurationObject.cs | 184 ++++++++++ .../PluginSystem/PluginFactory.Loading.cs | 85 +---- .../Tools/Services/DataSourceService.cs | 2 +- 31 files changed, 723 insertions(+), 472 deletions(-) delete mode 100644 app/MindWork AI Studio/Settings/ExpressionExtensions.cs create mode 100644 app/MindWork AI Studio/Tools/ExpressionExtensions.cs create mode 100644 app/MindWork AI Studio/Tools/IncrementResult.cs create mode 100644 app/MindWork AI Studio/Tools/IncrementType.cs create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/ConfigurationBaseObject.cs create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/IConfigurationObject.cs create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/NoConfigurationObject.cs diff --git a/app/MindWork AI Studio/Agents/AgentBase.cs b/app/MindWork AI Studio/Agents/AgentBase.cs index 66f13146..2b9239d1 100644 --- a/app/MindWork AI Studio/Agents/AgentBase.cs +++ b/app/MindWork AI Studio/Agents/AgentBase.cs @@ -54,7 +54,7 @@ public abstract class AgentBase(ILogger logger, SettingsManager setti #region Implementation of IAgent - public abstract AIStudio.Settings.Provider? ProviderSettings { get; set; } + public abstract AIStudio.Settings.Provider ProviderSettings { get; set; } public abstract Task ProcessContext(ChatThread chatThread, IDictionary additionalData); @@ -103,10 +103,9 @@ public abstract class AgentBase(ILogger logger, SettingsManager setti protected async Task AddAIResponseAsync(ChatThread thread, IContent lastUserPrompt, DateTimeOffset time) { - if(this.ProviderSettings is null) + if(this.ProviderSettings == Settings.Provider.NONE) return; - var providerSettings = this.ProviderSettings.Value; var aiText = new ContentText { // We have to wait for the remote @@ -127,6 +126,6 @@ public abstract class AgentBase(ILogger logger, SettingsManager setti // Use the selected provider to get the AI response. // By awaiting this line, we wait for the entire // content to be streamed. - await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger), providerSettings.Model, lastUserPrompt, thread); + await aiText.CreateFromProviderAsync(this.ProviderSettings.CreateProvider(this.Logger), this.ProviderSettings.Model, lastUserPrompt, thread); } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs b/app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs index 6fe1f4e2..42d395be 100644 --- a/app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs +++ b/app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs @@ -86,7 +86,7 @@ public sealed class AgentDataSourceSelection (ILogger """; /// - public override Settings.Provider? ProviderSettings { get; set; } + public override Settings.Provider ProviderSettings { get; set; } = Settings.Provider.NONE; /// /// The data source selection agent does not work with context. Use @@ -141,6 +141,11 @@ public sealed class AgentDataSourceSelection (ILogger // We start with the provider currently selected by the user: var agentProvider = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_DATA_SOURCE_SELECTION, provider.Id, true); + if (agentProvider == Settings.Provider.NONE) + { + logger.LogWarning("No provider is selected for the agent. The agent cannot select data sources."); + return []; + } // Assign the provider settings to the agent: logger.LogInformation($"The agent for the data source selection uses the provider '{agentProvider.InstanceName}' ({agentProvider.UsedLLMProvider.ToName()}, confidence={agentProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level.GetName()})."); diff --git a/app/MindWork AI Studio/Agents/AgentRetrievalContextValidation.cs b/app/MindWork AI Studio/Agents/AgentRetrievalContextValidation.cs index 9a4fdff5..7d7b9bb6 100644 --- a/app/MindWork AI Studio/Agents/AgentRetrievalContextValidation.cs +++ b/app/MindWork AI Studio/Agents/AgentRetrievalContextValidation.cs @@ -71,7 +71,7 @@ public sealed class AgentRetrievalContextValidation (ILogger - public override Settings.Provider? ProviderSettings { get; set; } + public override Settings.Provider ProviderSettings { get; set; } = Settings.Provider.NONE; /// /// The retrieval context validation agent does not work with context. Use @@ -133,6 +133,11 @@ public sealed class AgentRetrievalContextValidation (ILogger logger, SettingsM #region Overrides of AgentBase - public override AIStudio.Settings.Provider? ProviderSettings { get; set; } + public override AIStudio.Settings.Provider ProviderSettings { get; set; } = AIStudio.Settings.Provider.NONE; protected override Type Type => Type.SYSTEM; diff --git a/app/MindWork AI Studio/Agents/IAgent.cs b/app/MindWork AI Studio/Agents/IAgent.cs index e101f2ef..3ee77d8b 100644 --- a/app/MindWork AI Studio/Agents/IAgent.cs +++ b/app/MindWork AI Studio/Agents/IAgent.cs @@ -12,7 +12,7 @@ public interface IAgent /// /// The provider to use for this agent. /// - public AIStudio.Settings.Provider? ProviderSettings { get; set; } + public Settings.Provider ProviderSettings { get; set; } /// /// Processes a chat thread (i.e., context) and returns the updated thread. diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index 39bf2659..12ad2757 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs @@ -85,7 +85,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher protected virtual IReadOnlyList FooterButtons => []; - protected AIStudio.Settings.Provider providerSettings; + protected AIStudio.Settings.Provider providerSettings = Settings.Provider.NONE; protected MudForm? form; protected bool inputIsValid; protected Profile currentProfile = Profile.NO_PROFILE; @@ -352,7 +352,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher private async Task InnerResetForm() { this.resultingContentBlock = null; - this.providerSettings = default; + this.providerSettings = Settings.Provider.NONE; await this.JsRuntime.ClearDiv(RESULT_DIV_ID); await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID); diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 338057f4..fe74646a 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -5440,11 +5440,14 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T91464 -- The SETTINGS table does not exist or is not a valid table. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1148682011"] = "The SETTINGS table does not exist or is not a valid table." +-- At least one configured LLM provider is not valid or could not be parsed, or the LLM_PROVIDERS table does not exist. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3262676428"] = "At least one configured LLM provider is not valid or could not be parsed, or the LLM_PROVIDERS table does not exist." + -- The CONFIG table does not exist or is not a valid table. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3331620576"] = "The CONFIG table does not exist or is not a valid table." --- The LLM_PROVIDERS table does not exist or is not a valid table. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T806592324"] = "The LLM_PROVIDERS table does not exist or is not a valid table." +-- At least one configured chat template is not valid or could not be parsed, or the CHAT_TEMPLATES table does not exist. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T445358428"] = "At least one configured chat template is not valid or could not be parsed, or the CHAT_TEMPLATES table does not exist." -- The field IETF_TAG does not exist or is not a valid string. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "The field IETF_TAG does not exist or is not a valid string." diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index 3c4a8d38..6af2a5b7 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -18,9 +18,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable [Parameter] public EventCallback ChatThreadChanged { get; set; } - + [Parameter] - public AIStudio.Settings.Provider Provider { get; set; } + public AIStudio.Settings.Provider Provider { get; set; } = AIStudio.Settings.Provider.NONE; [Parameter] public EventCallback ProviderChanged { get; set; } @@ -634,7 +634,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable default: case AddChatProviderBehavior.ADDED_CHATS_USE_LATEST_PROVIDER: - if(this.Provider == default) + if(this.Provider == AIStudio.Settings.Provider.NONE) { this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT); await this.ProviderChanged.InvokeAsync(this.Provider); @@ -797,7 +797,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable break; case LoadingChatProviderBehavior.ALWAYS_USE_LATEST_CHAT_PROVIDER: - if(this.Provider == default) + if(this.Provider == AIStudio.Settings.Provider.NONE) this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT); break; } diff --git a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs index 8eced0f2..39ae76be 100644 --- a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs @@ -42,6 +42,9 @@ public partial class ConfigurationProviderSelection : MSGComponentBase foreach (var providerId in this.Data) { var provider = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == providerId.Value); + if (provider is null) + continue; + if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) yield return providerId; } diff --git a/app/MindWork AI Studio/Components/ProviderSelection.razor.cs b/app/MindWork AI Studio/Components/ProviderSelection.razor.cs index b476b19f..ded13ebd 100644 --- a/app/MindWork AI Studio/Components/ProviderSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ProviderSelection.razor.cs @@ -11,9 +11,9 @@ public partial class ProviderSelection : MSGComponentBase { [CascadingParameter] public AssistantBase? AssistantBase { get; set; } - + [Parameter] - public AIStudio.Settings.Provider ProviderSettings { get; set; } + public AIStudio.Settings.Provider ProviderSettings { get; set; } = AIStudio.Settings.Provider.NONE; [Parameter] public EventCallback ProviderSettingsChanged { get; set; } @@ -32,7 +32,8 @@ public partial class ProviderSelection : MSGComponentBase { var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.AssistantBase?.Component ?? Tools.Components.NONE); foreach (var provider in this.SettingsManager.ConfigurationData.Providers) - if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) - yield return provider; + if (provider.UsedLLMProvider != LLMProviders.NONE) + if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) + yield return provider; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ReadWebContent.razor.cs b/app/MindWork AI Studio/Components/ReadWebContent.razor.cs index 455c87dd..df007d49 100644 --- a/app/MindWork AI Studio/Components/ReadWebContent.razor.cs +++ b/app/MindWork AI Studio/Components/ReadWebContent.razor.cs @@ -20,7 +20,7 @@ public partial class ReadWebContent : MSGComponentBase public EventCallback ContentChanged { get; set; } [Parameter] - public AIStudio.Settings.Provider ProviderSettings { get; set; } + public AIStudio.Settings.Provider ProviderSettings { get; set; } = AIStudio.Settings.Provider.NONE; [Parameter] public bool AgentIsRunning { get; set; } @@ -34,7 +34,7 @@ public partial class ReadWebContent : MSGComponentBase [Parameter] public bool PreselectContentCleanerAgent { get; set; } - private Process process = Process.INSTANCE; + private readonly Process process = Process.INSTANCE; private ProcessStepValue processStep; private bool showWebContentReader; @@ -43,7 +43,7 @@ public partial class ReadWebContent : MSGComponentBase private bool urlIsValid; private bool isProviderValid; - private AIStudio.Settings.Provider providerSettings; + private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE; #region Overrides of ComponentBase @@ -86,7 +86,7 @@ public partial class ReadWebContent : MSGComponentBase this.StateHasChanged(); markdown = this.HTMLParser.ParseToMarkdown(html); - if (this.useContentCleanerAgent) + if (this.useContentCleanerAgent && this.providerSettings != AIStudio.Settings.Provider.NONE) { this.AgentTextContentCleaner.ProviderSettings = this.providerSettings; var additionalData = new Dictionary @@ -149,7 +149,7 @@ public partial class ReadWebContent : MSGComponentBase private string? ValidateProvider(bool shouldUseAgent) { - if(shouldUseAgent && this.providerSettings == default) + if(shouldUseAgent && this.providerSettings == AIStudio.Settings.Provider.NONE) { this.isProviderValid = false; return T("Please select a provider to use the cleanup agent."); diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs index 608dec29..a2e650da 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs @@ -54,6 +54,9 @@ public partial class SettingsPanelProviders : SettingsPanelBase [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] private async Task EditLLMProvider(AIStudio.Settings.Provider provider) { + if(provider == AIStudio.Settings.Provider.NONE) + return; + if (provider.IsEnterpriseConfiguration) return; diff --git a/app/MindWork AI Studio/Pages/Chat.razor.cs b/app/MindWork AI Studio/Pages/Chat.razor.cs index deeda47f..420828c6 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor.cs +++ b/app/MindWork AI Studio/Pages/Chat.razor.cs @@ -21,7 +21,7 @@ public partial class Chat : MSGComponentBase private IDialogService DialogService { get; init; } = null!; private ChatThread? chatThread; - private AIStudio.Settings.Provider providerSettings; + private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE; private bool workspaceOverlayVisible; private string currentWorkspaceName = string.Empty; private Workspaces? workspaces; diff --git a/app/MindWork AI Studio/Pages/Writer.razor.cs b/app/MindWork AI Studio/Pages/Writer.razor.cs index 56fd0c5d..8bd80016 100644 --- a/app/MindWork AI Studio/Pages/Writer.razor.cs +++ b/app/MindWork AI Studio/Pages/Writer.razor.cs @@ -18,7 +18,7 @@ public partial class Writer : MSGComponentBase private readonly Timer typeTimer = new(TimeSpan.FromMilliseconds(1_500)); private MudTextField textField = null!; - private AIStudio.Settings.Provider providerSettings; + private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE; private ChatThread? chatThread; private bool isStreaming; private string userInput = string.Empty; diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 4777ab01..ec8fe2c2 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -5442,11 +5442,14 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T91464 -- The SETTINGS table does not exist or is not a valid table. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1148682011"] = "Die Tabelle SETTINGS existiert nicht oder ist keine gültige Tabelle." +-- At least one configured LLM provider is not valid or could not be parsed, or the LLM_PROVIDERS table does not exist. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3262676428"] = "Mindestens ein konfigurierter LLM-Anbieter ist ungültig oder konnte nicht verarbeitet werden, oder die Tabelle LLM_PROVIDERS existiert nicht." + -- The CONFIG table does not exist or is not a valid table. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3331620576"] = "Die Tabelle CONFIG existiert nicht oder ist keine gültige Tabelle." --- The LLM_PROVIDERS table does not exist or is not a valid table. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T806592324"] = "Die Tabelle LLM_PROVIDERS existiert nicht oder ist keine gültige Tabelle." +-- At least one configured chat template is not valid or could not be parsed, or the CHAT_TEMPLATES table does not exist. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T445358428"] = "Mindestens eine konfigurierte Chat-Vorlage ist ungültig oder konnte nicht verarbeitet werden, oder die Tabelle CHAT_TEMPLATES existiert nicht." -- The field IETF_TAG does not exist or is not a valid string. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "Das Feld IETF_TAG existiert nicht oder ist keine gültige Zeichenkette." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 5d546552..fa754177 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -5442,11 +5442,14 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T91464 -- The SETTINGS table does not exist or is not a valid table. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1148682011"] = "The SETTINGS table does not exist or is not a valid table." +-- At least one configured LLM provider is not valid or could not be parsed, or the LLM_PROVIDERS table does not exist. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3262676428"] = "At least one configured LLM provider is not valid or could not be parsed, or the LLM_PROVIDERS table does not exist." + -- The CONFIG table does not exist or is not a valid table. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3331620576"] = "The CONFIG table does not exist or is not a valid table." --- The LLM_PROVIDERS table does not exist or is not a valid table. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T806592324"] = "The LLM_PROVIDERS table does not exist or is not a valid table." +-- At least one configured chat template is not valid or could not be parsed, or the CHAT_TEMPLATES table does not exist. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T445358428"] = "At least one configured chat template is not valid or could not be parsed, or the CHAT_TEMPLATES table does not exist." -- The field IETF_TAG does not exist or is not a valid string. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "The field IETF_TAG does not exist or is not a valid string." diff --git a/app/MindWork AI Studio/Settings/ChatTemplate.cs b/app/MindWork AI Studio/Settings/ChatTemplate.cs index 6842ae00..38f3623d 100644 --- a/app/MindWork AI Studio/Settings/ChatTemplate.cs +++ b/app/MindWork AI Studio/Settings/ChatTemplate.cs @@ -1,9 +1,20 @@ using AIStudio.Chat; using AIStudio.Tools.PluginSystem; +using Lua; + namespace AIStudio.Settings; -public record ChatTemplate(uint Num, string Id, string Name, string SystemPrompt, string PredefinedUserPrompt, List ExampleConversation, bool AllowProfileUsage, bool IsEnterpriseConfiguration = false, Guid EnterpriseConfigurationPluginId = default) +public record ChatTemplate( + uint Num, + string Id, + string Name, + string SystemPrompt, + string PredefinedUserPrompt, + List ExampleConversation, + bool AllowProfileUsage, + bool IsEnterpriseConfiguration = false, + Guid EnterpriseConfigurationPluginId = default) : ConfigurationBaseObject { public ChatTemplate() : this(0, Guid.Empty.ToString(), string.Empty, string.Empty, string.Empty, [], false) { @@ -11,6 +22,8 @@ public record ChatTemplate(uint Num, string Id, string Name, string SystemPrompt private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatTemplate).Namespace, nameof(ChatTemplate)); + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); + public static readonly ChatTemplate NO_CHAT_TEMPLATE = new() { Name = TB("Use no chat template"), @@ -41,4 +54,96 @@ public record ChatTemplate(uint Num, string Id, string Name, string SystemPrompt return this.SystemPrompt; } + + public static bool TryParseChatTemplateTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject template) + { + template = NO_CHAT_TEMPLATE; + if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var idText) || !Guid.TryParse(idText, out var id)) + { + LOGGER.LogWarning($"The configured chat template {idx} does not contain a valid ID. The ID must be a valid GUID."); + return false; + } + + if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead(out var name)) + { + LOGGER.LogWarning($"The configured chat template {idx} does not contain a valid name."); + return false; + } + + if (!table.TryGetValue("SystemPrompt", out var sysPromptValue) || !sysPromptValue.TryRead(out var systemPrompt)) + { + LOGGER.LogWarning($"The configured chat template {idx} does not contain a valid system prompt."); + return false; + } + + var predefinedUserPrompt = string.Empty; + if (table.TryGetValue("PredefinedUserPrompt", out var preUserValue) && preUserValue.TryRead(out var preUser)) + predefinedUserPrompt = preUser; + + var allowProfileUsage = false; + if (table.TryGetValue("AllowProfileUsage", out var allowProfileValue) && allowProfileValue.TryRead(out var allow)) + allowProfileUsage = allow; + + template = new ChatTemplate + { + Num = 0, + Id = id.ToString(), + Name = name, + SystemPrompt = systemPrompt, + PredefinedUserPrompt = predefinedUserPrompt, + ExampleConversation = ParseExampleConversation(idx, table), + AllowProfileUsage = allowProfileUsage, + IsEnterpriseConfiguration = true, + EnterpriseConfigurationPluginId = configPluginId, + }; + + return true; + } + + private static List ParseExampleConversation(int idx, LuaTable table) + { + var exampleConversation = new List(); + if (!table.TryGetValue("ExampleConversation", out var exConvValue) || !exConvValue.TryRead(out var exConvTable)) + return exampleConversation; + + var numBlocks = exConvTable.ArrayLength; + for (var j = 1; j <= numBlocks; j++) + { + var blockValue = exConvTable[j]; + if (!blockValue.TryRead(out var blockTable)) + { + LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} is not a valid table."); + continue; + } + + if (!blockTable.TryGetValue("Role", out var roleValue) || !roleValue.TryRead(out var roleText) || !Enum.TryParse(roleText, true, out var parsedRole)) + { + LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} does not contain a valid role."); + continue; + } + + if (!blockTable.TryGetValue("Content", out var contentValue) || !contentValue.TryRead(out var content)) + { + LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} does not contain a valid content message."); + continue; + } + + if (string.IsNullOrWhiteSpace(content)) + { + LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} contains an empty content message."); + continue; + } + + exampleConversation.Add(new ContentBlock + { + Time = DateTimeOffset.UtcNow, + Role = parsedRole, + Content = new ContentText { Text = content }, + ContentType = ContentType.TEXT, + HideFromUser = true, + }); + } + + return exampleConversation; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ExpressionExtensions.cs b/app/MindWork AI Studio/Settings/ExpressionExtensions.cs deleted file mode 100644 index 5d567c03..00000000 --- a/app/MindWork AI Studio/Settings/ExpressionExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Linq.Expressions; - -namespace AIStudio.Settings; - -public static class ExpressionExtensions -{ - private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(typeof(ExpressionExtensions)); - - public static MemberExpression GetMemberExpression(this Expression> expression) - { - switch (expression.Body) - { - // Case for value types, which are wrapped in UnaryExpression: - case UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression: - return (MemberExpression)unaryExpression.Operand; - - // Case for reference types, which are directly MemberExpressions: - case MemberExpression memberExpression: - return memberExpression; - - default: - LOGGER.LogError($"Expression '{expression}' is not a valid property expression."); - throw new ArgumentException($"Expression '{expression}' is not a valid property expression.", nameof(expression)); - } - } -} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/Provider.cs b/app/MindWork AI Studio/Settings/Provider.cs index 4cef58df..94a1a747 100644 --- a/app/MindWork AI Studio/Settings/Provider.cs +++ b/app/MindWork AI Studio/Settings/Provider.cs @@ -2,6 +2,10 @@ using System.Text.Json.Serialization; using AIStudio.Provider; using AIStudio.Provider.HuggingFace; +using AIStudio.Tools.PluginSystem; + +using Lua; + using Host = AIStudio.Provider.SelfHosted.Host; namespace AIStudio.Settings; @@ -16,7 +20,7 @@ namespace AIStudio.Settings; /// Whether the provider is self-hosted. /// The hostname of the provider. Useful for self-hosted providers. /// The LLM model to use for chat. -public readonly record struct Provider( +public sealed record Provider( uint Num, string Id, string InstanceName, @@ -27,8 +31,24 @@ public readonly record struct Provider( Guid EnterpriseConfigurationPluginId = default, string Hostname = "http://localhost:1234", Host Host = Host.NONE, - HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE) : ISecretId + HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE) : ConfigurationBaseObject, ISecretId { + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); + + public static readonly Provider NONE = new(); + + public Provider() : this( + 0, + Guid.Empty.ToString(), + string.Empty, + LLMProviders.NONE, + default, + false, + false, + Guid.Empty) + { + } + #region Overrides of ValueType /// @@ -57,4 +77,95 @@ public readonly record struct Provider( public string SecretName => this.InstanceName; #endregion + + #region Implementation of IConfigurationObject + + public override string Name + { + get => this.InstanceName; + init => this.InstanceName = value; + } + + #endregion + + public static bool TryParseProviderTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject provider) + { + provider = NONE; + if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var idText) || !Guid.TryParse(idText, out var id)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid ID. The ID must be a valid GUID."); + return false; + } + + if (!table.TryGetValue("InstanceName", out var instanceNameValue) || !instanceNameValue.TryRead(out var instanceName)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid instance name."); + return false; + } + + if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead(out var usedLLMProviderText) || !Enum.TryParse(usedLLMProviderText, true, out var usedLLMProvider)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid LLM provider enum value."); + return false; + } + + if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead(out var hostText) || !Enum.TryParse(hostText, true, out var host)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid host enum value."); + return false; + } + + if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead(out var hostname)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid hostname."); + return false; + } + + if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead(out var modelTable)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model table."); + return false; + } + + if (!TryReadModelTable(idx, modelTable, out var model)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model configuration."); + return false; + } + + provider = new Provider + { + Num = 0, + Id = id.ToString(), + InstanceName = instanceName, + UsedLLMProvider = usedLLMProvider, + Model = model, + IsSelfHosted = usedLLMProvider is LLMProviders.SELF_HOSTED, + IsEnterpriseConfiguration = true, + EnterpriseConfigurationPluginId = configPluginId, + Hostname = hostname, + Host = host + }; + + return true; + } + + private static bool TryReadModelTable(int idx, LuaTable table, out Model model) + { + model = default; + if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var id)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model ID."); + return false; + } + + if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead(out var displayName)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model display name."); + return false; + } + + model = new(id, displayName); + return true; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 7cad25a2..54e6f1b3 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -215,18 +215,18 @@ public sealed class SettingsManager return this.ConfigurationData.Providers[0]; // Is there a current provider with a sufficiently high confidence level? - Provider currentProvider = default; + var currentProvider = Provider.NONE; if (currentProviderId is not null && !string.IsNullOrWhiteSpace(currentProviderId)) { var currentProviderProbe = this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == currentProviderId); - if (currentProviderProbe.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) + if (currentProviderProbe is not null && currentProviderProbe.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) currentProvider = currentProviderProbe; } // Is there a component-preselected provider with a sufficiently high confidence level? - Provider preselectedProvider = default; + var preselectedProvider = Provider.NONE; var preselectedProviderProbe = component.PreselectedProvider(this); - if(preselectedProviderProbe != default && preselectedProviderProbe.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) + if(preselectedProviderProbe != Provider.NONE && preselectedProviderProbe.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) preselectedProvider = preselectedProviderProbe; // @@ -234,14 +234,14 @@ public sealed class SettingsManager // and the preselected provider is available and has a confidence level // that is high enough. // - if(usePreselectionBeforeCurrentProvider && preselectedProvider != default) + if(usePreselectionBeforeCurrentProvider && preselectedProvider != Provider.NONE) return preselectedProvider; // // Case: The current provider is available and has a confidence level that is // high enough. // - if(currentProvider != default) + if(currentProvider != Provider.NONE) return currentProvider; // @@ -250,11 +250,11 @@ public sealed class SettingsManager // level that is high enough. The preselected provider is available and // has a confidence level that is high enough. // - if(preselectedProvider != default) + if(preselectedProvider != Provider.NONE) return preselectedProvider; // When there is an app-wide preselected provider, and it has a confidence level that is high enough, we return it: - return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider && x.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel); + return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider && x.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) ?? Provider.NONE; } public Profile GetPreselectedProfile(Tools.Components component) diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs index 18ac4f41..a2126970 100644 --- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs +++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs @@ -89,32 +89,37 @@ public static class ComponentsExtensions }; [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] - public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) => component switch + public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) { - Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : default, - Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : default, - Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : default, - Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Translation.PreselectedProvider) : default, - Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Agenda.PreselectedProvider) : default, - Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Coding.PreselectedProvider) : default, - Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : default, - Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.EMail.PreselectedProvider) : default, - Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : default, - Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : default, - Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : default, - Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : default, - Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : default, - Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProvider) : default, - Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.I18N.PreselectedProvider) : default, + var preselectedProvider = component switch + { + Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : null, + Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : null, + Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : null, + Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Translation.PreselectedProvider) : null, + Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Agenda.PreselectedProvider) : null, + Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Coding.PreselectedProvider) : null, + Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : null, + Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.EMail.PreselectedProvider) : null, + Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : null, + Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : null, + Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : null, + Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : null, + Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : null, + Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProvider) : null, + Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.I18N.PreselectedProvider) : null, - Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProvider) : default, + Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProvider) : null, + + Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : null, + Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : null, + Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : null, + + _ => Settings.Provider.NONE, + }; - Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : default, - Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : default, - Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : default, - - _ => default, - }; + return preselectedProvider ?? Settings.Provider.NONE; + } public static Profile PreselectedProfile(this Components component, SettingsManager settingsManager) => component switch { diff --git a/app/MindWork AI Studio/Tools/ExpressionExtensions.cs b/app/MindWork AI Studio/Tools/ExpressionExtensions.cs new file mode 100644 index 00000000..bb57fad3 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ExpressionExtensions.cs @@ -0,0 +1,96 @@ +using System.Linq.Expressions; +using System.Numerics; +using System.Reflection; + +namespace AIStudio.Tools; + +public static class ExpressionExtensions +{ + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(typeof(ExpressionExtensions)); + + /// + /// Extracts the member expression from a given lambda expression representing a property. + /// + /// A lambda expression specifying the property for which the member expression is to be extracted. + /// The lambda expression body must represent member access. + /// The type of the object containing the property referenced in the lambda expression. + /// The type of the property being accessed in the lambda expression. + /// The member expression that represents the property access. + /// Thrown if the provided lambda expression does not represent a valid property expression. + public static MemberExpression GetMemberExpression(this Expression> expression) + { + switch (expression.Body) + { + // Case for value types, which are wrapped in UnaryExpression: + case UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression: + return (MemberExpression)unaryExpression.Operand; + + // Case for reference types, which are directly MemberExpressions: + case MemberExpression memberExpression: + return memberExpression; + + default: + LOGGER.LogError($"Expression '{expression}' is not a valid property expression."); + throw new ArgumentException($"Expression '{expression}' is not a valid property expression.", nameof(expression)); + } + } + + /// + /// Attempts to increment the value of an uint property for a specified object using a + /// provided expression. + /// + /// An expression representing the property to be incremented. The property + /// must be of type uint and belong to the provided object. + /// The object that contains the property referenced by the expression. + /// The type of increment operation to perform (e.g., prefix or postfix). + /// The type of the object that contains the property to be incremented. + /// The type of the property to be incremented. + /// An IncrementResult object containing the result of the increment operation. + public static IncrementResult TryIncrement(this Expression> expression, TIn data, IncrementType type) where TOut : IBinaryInteger + { + // Ensure that the expression body is a member expression: + if (expression.Body is not MemberExpression memberExpression) + return new(false, TOut.Zero); + + // Ensure that the member expression is a property: + if (memberExpression.Member is not PropertyInfo propertyInfo) + return new(false, TOut.Zero); + + // Ensure that the member expression has a target object: + if (memberExpression.Expression is null) + return new(false, TOut.Zero); + + // Get the target object for the expression, which is the object containing the property to increment: + var targetObjectExpression = Expression.Lambda(memberExpression.Expression, expression.Parameters); + + // Compile the lambda expression to get the target object + // (which is the object containing the property to increment): + var targetObject = targetObjectExpression.Compile().DynamicInvoke(data); + + // Was the compilation successful? + if (targetObject is null) + return new(false, TOut.Zero); + + // Read the current value of the property: + if (propertyInfo.GetValue(targetObject) is not TOut value) + return new(false, TOut.Zero); + + // Increment the value: + switch (type) + { + case IncrementType.PRE: + var nextValue = value + TOut.CreateChecked(1); + propertyInfo.SetValue(targetObject, nextValue); + return new(true, nextValue); + + case IncrementType.POST: + var currentValue = value; + var incrementedValue = value + TOut.CreateChecked(1); + propertyInfo.SetValue(targetObject, incrementedValue); + return new(true, currentValue); + + default: + return new(false, TOut.Zero); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/IncrementResult.cs b/app/MindWork AI Studio/Tools/IncrementResult.cs new file mode 100644 index 00000000..76efecd4 --- /dev/null +++ b/app/MindWork AI Studio/Tools/IncrementResult.cs @@ -0,0 +1,10 @@ +using System.Numerics; + +namespace AIStudio.Tools; + +/// +/// Represents the result of an increment operation. It encapsulates whether the operation +/// was successful and the increased value. +/// +/// The type of the incremented value, constrained to implement the IBinaryInteger interface. +public sealed record IncrementResult(bool Success, TOut UpdatedValue) where TOut : IBinaryInteger; \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/IncrementType.cs b/app/MindWork AI Studio/Tools/IncrementType.cs new file mode 100644 index 00000000..a675f009 --- /dev/null +++ b/app/MindWork AI Studio/Tools/IncrementType.cs @@ -0,0 +1,14 @@ +namespace AIStudio.Tools; + +public enum IncrementType +{ + /// + /// Increments the value before returning it. So, the incremented value is returned. + /// + PRE, + + /// + /// Increments the value after returning it. So, the original value is returned. + /// + POST, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationBaseObject.cs b/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationBaseObject.cs new file mode 100644 index 00000000..97d48d61 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationBaseObject.cs @@ -0,0 +1,23 @@ +namespace AIStudio.Tools.PluginSystem; + +public abstract record ConfigurationBaseObject : IConfigurationObject +{ + #region Implementation of IConfigurationObject + + /// + public abstract string Id { get; init; } + + /// + public abstract uint Num { get; init; } + + /// + public abstract string Name { get; init; } + + /// + public abstract bool IsEnterpriseConfiguration { get; init; } + + /// + public abstract Guid EnterpriseConfigurationPluginId { get; init; } + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/IConfigurationObject.cs b/app/MindWork AI Studio/Tools/PluginSystem/IConfigurationObject.cs new file mode 100644 index 00000000..a46da2ef --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/IConfigurationObject.cs @@ -0,0 +1,32 @@ +namespace AIStudio.Tools.PluginSystem; + +/// +/// Represents a configuration object, such as a chat template or a LLM provider. +/// +public interface IConfigurationObject +{ + /// + /// The unique ID of the configuration object. + /// + public string Id { get; } + + /// + /// The continuous number of the configuration object. + /// + public uint Num { get; } + + /// + /// The name of the configuration object. + /// + public string Name { get; } + + /// + /// Is this configuration object an enterprise configuration? + /// + public bool IsEnterpriseConfiguration { get; } + + /// + /// The ID of the enterprise configuration plugin. + /// + public Guid EnterpriseConfigurationPluginId { get; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/NoConfigurationObject.cs b/app/MindWork AI Studio/Tools/PluginSystem/NoConfigurationObject.cs new file mode 100644 index 00000000..d42cb6f7 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/NoConfigurationObject.cs @@ -0,0 +1,26 @@ +namespace AIStudio.Tools.PluginSystem; + +public sealed record NoConfigurationObject : ConfigurationBaseObject +{ + public static readonly NoConfigurationObject INSTANCE = new(); + + private NoConfigurationObject() + { + this.Id = Guid.Empty.ToString(); + this.Name = "No Configuration"; + } + + #region Overrides of ConfigurationBaseObject + + public override string Id { get; init; } + + public override uint Num { get; init; } + + public override string Name { get; init; } + + public override bool IsEnterpriseConfiguration { get; init; } + + public override Guid EnterpriseConfigurationPluginId { get; init; } + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index 9e309b10..5e5efd3e 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -1,21 +1,15 @@ -using AIStudio.Provider; using AIStudio.Settings; -using AIStudio.Chat; using Lua; -using Host = AIStudio.Provider.SelfHosted.Host; -using Model = AIStudio.Provider.Model; - namespace AIStudio.Tools.PluginSystem; public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginType type) : PluginBase(isInternal, state, type) { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfiguration).Namespace, nameof(PluginConfiguration)); - private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); - private readonly List configObjects = []; + private List configObjects = []; /// /// The list of configuration objects. Configuration objects are, e.g., providers or chat templates. @@ -51,317 +45,34 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT return false; } - // - // =========================================== - // Configured settings - // =========================================== - // + // Check for the main SETTINGS table: if (!mainTable.TryGetValue("SETTINGS", out var settingsValue) || !settingsValue.TryRead(out var settingsTable)) { message = TB("The SETTINGS table does not exist or is not a valid table."); return false; } - // Check for updates, and if so, how often? + // Config: check for updates, and if so, how often? ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UpdateBehavior, this.Id, settingsTable, dryRun); - // Allow the user to add providers? + // Config: allow the user to add providers? ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun); - - // - // Configured providers: - // - if (!mainTable.TryGetValue("LLM_PROVIDERS", out var providersValue) || !providersValue.TryRead(out var providersTable)) + + // Handle configured LLM providers: + if (!PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun)) { - message = TB("The LLM_PROVIDERS table does not exist or is not a valid table."); + message = TB("At least one configured LLM provider is not valid or could not be parsed, or the LLM_PROVIDERS table does not exist."); return false; } - + + // Handle configured chat templates: + if (!PluginConfigurationObject.TryParse(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, x => x.NextChatTemplateNum, mainTable, this.Id, ref this.configObjects, dryRun)) + { + message = TB("At least one configured chat template is not valid or could not be parsed, or the CHAT_TEMPLATES table does not exist."); + return false; + } + message = string.Empty; - var numberProviders = providersTable.ArrayLength; - var configuredProviders = new List(numberProviders); - for (var i = 1; i <= numberProviders; i++) - { - var providerLuaTableValue = providersTable[i]; - if (!providerLuaTableValue.TryRead(out var providerLuaTable)) - { - LOGGER.LogWarning($"The LLM_PROVIDERS table at index {i} is not a valid table."); - continue; - } - - if(this.TryReadProviderTable(i, providerLuaTable, out var provider)) - configuredProviders.Add(provider); - else - LOGGER.LogWarning($"The LLM_PROVIDERS table at index {i} does not contain a valid provider configuration."); - } - - // - // Apply the configured providers to the system settings: - // - #pragma warning disable MWAIS0001 - foreach (var configuredProvider in configuredProviders) - { - // The iterating variable is immutable, so we need to create a local copy: - var provider = configuredProvider; - - // Store this provider in the config object list: - this.configObjects.Add(new() - { - ConfigPluginId = this.Id, - Id = Guid.Parse(provider.Id), - Type = PluginConfigurationObjectType.LLM_PROVIDER, - }); - - if (dryRun) - continue; - - var providerIndex = SETTINGS_MANAGER.ConfigurationData.Providers.FindIndex(p => p.Id == provider.Id); - if (providerIndex > -1) - { - // Case: The provider already exists, we update it: - var existingProvider = SETTINGS_MANAGER.ConfigurationData.Providers[providerIndex]; - provider = provider with { Num = existingProvider.Num }; // Keep the original number - SETTINGS_MANAGER.ConfigurationData.Providers[providerIndex] = provider; - } - else - { - // Case: The provider does not exist, we add it: - provider = provider with { Num = SETTINGS_MANAGER.ConfigurationData.NextProviderNum++ }; - SETTINGS_MANAGER.ConfigurationData.Providers.Add(provider); - } - } - - #pragma warning restore MWAIS0001 - - // - // Configured chat templates: - // - if (mainTable.TryGetValue("CHAT_TEMPLATES", out var templatesValue) && templatesValue.TryRead(out var templatesTable)) - { - var numberTemplates = templatesTable.ArrayLength; - var configuredTemplates = new List(numberTemplates); - for (var i = 1; i <= numberTemplates; i++) - { - var templateLuaTableValue = templatesTable[i]; - if (!templateLuaTableValue.TryRead(out var templateLuaTable)) - { - LOGGER.LogWarning($"The CHAT_TEMPLATES table at index {i} is not a valid table."); - continue; - } - - if(this.TryReadChatTemplateTable(i, templateLuaTable, out var template) && template != ChatTemplate.NO_CHAT_TEMPLATE) - configuredTemplates.Add(template); - else - LOGGER.LogWarning($"The CHAT_TEMPLATES table at index {i} does not contain a valid chat template configuration."); - } - - // Apply configured chat templates to the system settings: - foreach (var configuredTemplate in configuredTemplates) - { - // The iterating variable is immutable, so we need to create a local copy: - var template = configuredTemplate; - - // Store this provider in the config object list: - this.configObjects.Add(new() - { - ConfigPluginId = this.Id, - Id = Guid.Parse(template.Id), - Type = PluginConfigurationObjectType.CHAT_TEMPLATE, - }); - - if (dryRun) - continue; - - var tplIndex = SETTINGS_MANAGER.ConfigurationData.ChatTemplates.FindIndex(t => t.Id == template.Id); - if (tplIndex > -1) - { - // Case: The template already exists, we update it: - var existingTemplate = SETTINGS_MANAGER.ConfigurationData.ChatTemplates[tplIndex]; - template = template with { Num = existingTemplate.Num }; - SETTINGS_MANAGER.ConfigurationData.ChatTemplates[tplIndex] = template; - } - else - { - // Case: The template does not exist, we add it: - template = template with { Num = SETTINGS_MANAGER.ConfigurationData.NextChatTemplateNum++ }; - SETTINGS_MANAGER.ConfigurationData.ChatTemplates.Add(template); - } - } - } - return true; } - - private bool TryReadProviderTable(int idx, LuaTable table, out Settings.Provider provider) - { - provider = default; - if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var idText) || !Guid.TryParse(idText, out var id)) - { - LOGGER.LogWarning($"The configured provider {idx} does not contain a valid ID. The ID must be a valid GUID."); - return false; - } - - if (!table.TryGetValue("InstanceName", out var instanceNameValue) || !instanceNameValue.TryRead(out var instanceName)) - { - LOGGER.LogWarning($"The configured provider {idx} does not contain a valid instance name."); - return false; - } - - if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead(out var usedLLMProviderText) || !Enum.TryParse(usedLLMProviderText, true, out var usedLLMProvider)) - { - LOGGER.LogWarning($"The configured provider {idx} does not contain a valid LLM provider enum value."); - return false; - } - - if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead(out var hostText) || !Enum.TryParse(hostText, true, out var host)) - { - LOGGER.LogWarning($"The configured provider {idx} does not contain a valid host enum value."); - return false; - } - - if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead(out var hostname)) - { - LOGGER.LogWarning($"The configured provider {idx} does not contain a valid hostname."); - return false; - } - - if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead(out var modelTable)) - { - LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model table."); - return false; - } - - if (!this.TryReadModelTable(idx, modelTable, out var model)) - { - LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model configuration."); - return false; - } - - provider = new() - { - Num = 0, - Id = id.ToString(), - InstanceName = instanceName, - UsedLLMProvider = usedLLMProvider, - Model = model, - IsSelfHosted = usedLLMProvider is LLMProviders.SELF_HOSTED, - IsEnterpriseConfiguration = true, - EnterpriseConfigurationPluginId = this.Id, - Hostname = hostname, - Host = host - }; - - return true; - } - - private bool TryReadModelTable(int idx, LuaTable table, out Model model) - { - model = default; - if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var id)) - { - LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model ID."); - return false; - } - - if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead(out var displayName)) - { - LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model display name."); - return false; - } - - model = new(id, displayName); - return true; - } - - private bool TryReadChatTemplateTable(int idx, LuaTable table, out ChatTemplate template) - { - template = ChatTemplate.NO_CHAT_TEMPLATE; - if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var idText) || !Guid.TryParse(idText, out var id)) - { - LOGGER.LogWarning($"The configured chat template {idx} does not contain a valid ID. The ID must be a valid GUID."); - return false; - } - - if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead(out var name)) - { - LOGGER.LogWarning($"The configured chat template {idx} does not contain a valid name."); - return false; - } - - if (!table.TryGetValue("SystemPrompt", out var sysPromptValue) || !sysPromptValue.TryRead(out var systemPrompt)) - { - LOGGER.LogWarning($"The configured chat template {idx} does not contain a valid system prompt."); - return false; - } - - var predefinedUserPrompt = string.Empty; - if (table.TryGetValue("PredefinedUserPrompt", out var preUserValue) && preUserValue.TryRead(out var preUser)) - predefinedUserPrompt = preUser; - - var allowProfileUsage = false; - if (table.TryGetValue("AllowProfileUsage", out var allowProfileValue) && allowProfileValue.TryRead(out var allow)) - allowProfileUsage = allow; - - template = new() - { - Num = 0, - Id = id.ToString(), - Name = name, - SystemPrompt = systemPrompt, - PredefinedUserPrompt = predefinedUserPrompt, - ExampleConversation = ParseExampleConversation(idx, table), - AllowProfileUsage = allowProfileUsage, - IsEnterpriseConfiguration = true, - EnterpriseConfigurationPluginId = this.Id - }; - - return true; - } - - private static List ParseExampleConversation(int idx, LuaTable table) - { - var exampleConversation = new List(); - if (!table.TryGetValue("ExampleConversation", out var exConvValue) || !exConvValue.TryRead(out var exConvTable)) - return exampleConversation; - - var numBlocks = exConvTable.ArrayLength; - for (var j = 1; j <= numBlocks; j++) - { - var blockValue = exConvTable[j]; - if (!blockValue.TryRead(out var blockTable)) - { - LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} is not a valid table."); - continue; - } - - if (!blockTable.TryGetValue("Role", out var roleValue) || !roleValue.TryRead(out var roleText) || !Enum.TryParse(roleText, true, out var parsedRole)) - { - LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} does not contain a valid role."); - continue; - } - - if (!blockTable.TryGetValue("Content", out var contentValue) || !contentValue.TryRead(out var content)) - { - LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} does not contain a valid content message."); - continue; - } - - if (string.IsNullOrWhiteSpace(content)) - { - LOGGER.LogWarning($"The ExampleConversation entry {j} in chat template {idx} contains an empty content message."); - continue; - } - - exampleConversation.Add(new ContentBlock - { - Time = DateTimeOffset.UtcNow, - Role = parsedRole, - Content = new ContentText { Text = content }, - ContentType = ContentType.TEXT, - HideFromUser = true, - }); - } - - return exampleConversation; - } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs index 258e6c3c..5be55ddb 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs @@ -1,3 +1,10 @@ +using System.Linq.Expressions; + +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +using Lua; + namespace AIStudio.Tools.PluginSystem; /// @@ -6,6 +13,9 @@ namespace AIStudio.Tools.PluginSystem; /// public sealed record PluginConfigurationObject { + private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); + private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(); + /// /// The id of the configuration plugin to which this configuration object belongs. /// @@ -20,4 +30,178 @@ public sealed record PluginConfigurationObject /// The type of the configuration object. /// public required PluginConfigurationObjectType Type { get; init; } = PluginConfigurationObjectType.NONE; + + /// + /// Parses Lua table entries into configuration objects of the specified type, populating the + /// provided list with results. + /// + /// The type of configuration object to parse, which must + /// inherit from . + /// The type of configuration object to process, as specified + /// in . + /// An expression to retrieve existing configuration objects from + /// the main configuration data. + /// An expression to retrieve the next available configuration + /// object number from the main configuration data. + /// The Lua table containing entries to parse into configuration objects. + /// The unique identifier of the plugin associated with the configuration + /// objects being parsed. + /// The list to populate with the parsed configuration objects. + /// This parameter is passed by reference. + /// Specifies whether to perform the operation as a dry run, where changes + /// are not persisted. + /// Returns true if parsing succeeds and configuration objects are added + /// to the list; otherwise, false. + public static bool TryParse( + PluginConfigurationObjectType configObjectType, + Expression>> configObjectSelection, + Expression> nextConfigObjectNumSelection, + LuaTable mainTable, + Guid configPluginId, + ref List configObjects, + bool dryRun + ) where TClass : ConfigurationBaseObject + { + var luaTableName = configObjectType switch + { + PluginConfigurationObjectType.LLM_PROVIDER => "LLM_PROVIDERS", + PluginConfigurationObjectType.CHAT_TEMPLATE => "CHAT_TEMPLATES", + PluginConfigurationObjectType.DATA_SOURCE => "DATA_SOURCES", + PluginConfigurationObjectType.EMBEDDING_PROVIDER => "EMBEDDING_PROVIDERS", + PluginConfigurationObjectType.PROFILE => "PROFILES", + + _ => null, + }; + + if (luaTableName is null) + { + LOG.LogError($"The configuration object type '{configObjectType}' is not supported yet."); + return false; + } + + if (!mainTable.TryGetValue(luaTableName, out var luaValue) || !luaValue.TryRead(out var luaTable)) + { + LOG.LogWarning($"The {luaTableName} table does not exist or is not a valid table."); + return false; + } + + var storedObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData); + var numberObjects = luaTable.ArrayLength; + ThreadSafeRandom? random = null; + for (var i = 1; i <= numberObjects; i++) + { + var luaObjectTableValue = luaTable[i]; + if (!luaObjectTableValue.TryRead(out var luaObjectTable)) + { + LOG.LogWarning($"The {luaObjectTable} table at index {i} is not a valid table."); + continue; + } + + var (wasParsingSuccessful, configObject) = configObjectType switch + { + PluginConfigurationObjectType.LLM_PROVIDER => (Settings.Provider.TryParseProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Settings.Provider.NONE, configurationObject), + PluginConfigurationObjectType.CHAT_TEMPLATE => (ChatTemplate.TryParseChatTemplateTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != ChatTemplate.NO_CHAT_TEMPLATE, configurationObject), + + _ => (false, NoConfigurationObject.INSTANCE) + }; + + if (wasParsingSuccessful) + { + // Store it in the config object list: + configObjects.Add(new() + { + ConfigPluginId = configPluginId, + Id = Guid.Parse(configObject.Id), + Type = configObjectType, + }); + + if (dryRun) + continue; + + var objectIndex = storedObjects.FindIndex(t => t.Id == configObject.Id); + + // Case: The object already exists, we update it: + if (objectIndex > -1) + { + var existingObject = storedObjects[objectIndex]; + configObject = configObject with { Num = existingObject.Num }; + storedObjects[objectIndex] = (TClass)configObject; + } + + // Case: The object does not exist, we have to add it + else + { + if (nextConfigObjectNumSelection.TryIncrement(SETTINGS_MANAGER.ConfigurationData, IncrementType.POST) is { Success: true, UpdatedValue: var nextNum }) + { + // Case: Increment the next number was successful + configObject = configObject with { Num = nextNum }; + storedObjects.Add((TClass)configObject); + } + else + { + // Case: The next number could not be incremented, we use a random number + random ??= new ThreadSafeRandom(); + configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) }; + storedObjects.Add((TClass)configObject); + LOG.LogWarning($"The next number for the configuration object '{configObject.Name}' (id={configObject.Id}) could not be incremented. Using a random number instead."); + } + } + } + else + LOG.LogWarning($"The {luaObjectTable} table at index {i} does not contain a valid chat template configuration."); + } + + return true; + } + + /// + /// Cleans up configuration objects of a specified type that are no longer associated with any available plugin. + /// + /// The type of configuration object to clean up. + /// The type of configuration object to process. + /// A selection expression to retrieve the configuration objects from the main configuration. + /// A list of currently available plugins. + /// A list of all existing configuration objects. + /// Returns true if the configuration was altered during cleanup; otherwise, false. + public static bool CleanLeftOverConfigurationObjects( + PluginConfigurationObjectType configObjectType, + Expression>> configObjectSelection, + IList availablePlugins, + IList configObjectList) where TClass : IConfigurationObject + { + var configuredObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData); + var leftOverObjects = new List(); + foreach (var configuredObject in configuredObjects) + { + if(!configuredObject.IsEnterpriseConfiguration) + continue; + + var configObjectSourcePluginId = configuredObject.EnterpriseConfigurationPluginId; + if(configObjectSourcePluginId == Guid.Empty) + continue; + + var templateSourcePlugin = availablePlugins.FirstOrDefault(plugin => plugin.Id == configObjectSourcePluginId); + if(templateSourcePlugin is null) + { + LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is based on a plugin that is not available anymore. Removing the chat template from the settings."); + leftOverObjects.Add(configuredObject); + } + + if(!configObjectList.Any(configObject => + configObject.Type == configObjectType && + configObject.ConfigPluginId == configObjectSourcePluginId && + configObject.Id.ToString() == configuredObject.Id)) + { + LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is not present in the configuration plugin anymore. Removing the chat template from the settings."); + leftOverObjects.Add(configuredObject); + } + } + + // Remove collected items after enumeration to avoid modifying the collection during iteration: + var wasConfigurationChanged = leftOverObjects.Count > 0; + foreach (var item in leftOverObjects.Distinct()) + configuredObjects.Remove(item); + + return wasConfigurationChanged; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 5972b3a4..2553b537 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -92,10 +92,10 @@ public static partial class PluginFactory case { IsValid: false }: LOG.LogError($"Was not able to load plugin '{pluginMainFile}', because the Lua code is not a valid AI Studio plugin. There are {plugin.Issues.Count()} issues to fix. First issue is: {plugin.Issues.FirstOrDefault()}"); - #if DEBUG +#if DEBUG foreach (var pluginIssue in plugin.Issues) LOG.LogError($"Plugin issue: {pluginIssue}"); - #endif +#endif continue; case { IsMaintained: false }: @@ -125,89 +125,24 @@ public static partial class PluginFactory // // ========================================================= - // Next, we have to clean up our settings. It is possible that a configuration plugin was removed. - // We have to remove the related settings as well: + // Next, we have to clean up our settings. It is possible + // that a configuration plugin was removed. We have to + // remove the related settings as well: // ========================================================= // - var wasConfigurationChanged = false; - // // Check LLM providers: - // - #pragma warning disable MWAIS0001 - var configuredProviders = SETTINGS_MANAGER.ConfigurationData.Providers.ToList(); - foreach (var configuredProvider in configuredProviders) - { - if(!configuredProvider.IsEnterpriseConfiguration) - continue; - - var providerSourcePluginId = configuredProvider.EnterpriseConfigurationPluginId; - if(providerSourcePluginId == Guid.Empty) - continue; - - var providerSourcePlugin = AVAILABLE_PLUGINS.FirstOrDefault(plugin => plugin.Id == providerSourcePluginId); - if(providerSourcePlugin is null) - { - LOG.LogWarning($"The configured LLM provider '{configuredProvider.InstanceName}' (id={configuredProvider.Id}) is based on a plugin that is not available anymore. Removing the provider from the settings."); - SETTINGS_MANAGER.ConfigurationData.Providers.Remove(configuredProvider); - wasConfigurationChanged = true; - } - - if(!configObjectList.Any(configObject => - configObject.Type is PluginConfigurationObjectType.LLM_PROVIDER && - configObject.ConfigPluginId == providerSourcePluginId && - configObject.Id.ToString() == configuredProvider.Id)) - { - LOG.LogWarning($"The configured LLM provider '{configuredProvider.InstanceName}' (id={configuredProvider.Id}) is not present in the configuration plugin anymore. Removing the provider from the settings."); - SETTINGS_MANAGER.ConfigurationData.Providers.Remove(configuredProvider); - wasConfigurationChanged = true; - } - } - #pragma warning restore MWAIS0001 + var wasConfigurationChanged = PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, AVAILABLE_PLUGINS, configObjectList); - // // Check chat templates: - // - var configuredTemplates = SETTINGS_MANAGER.ConfigurationData.ChatTemplates.ToList(); - foreach (var configuredTemplate in configuredTemplates) - { - if(!configuredTemplate.IsEnterpriseConfiguration) - continue; - - var templateSourcePluginId = configuredTemplate.EnterpriseConfigurationPluginId; - if(templateSourcePluginId == Guid.Empty) - continue; - - var templateSourcePlugin = AVAILABLE_PLUGINS.FirstOrDefault(plugin => plugin.Id == templateSourcePluginId); - if(templateSourcePlugin is null) - { - LOG.LogWarning($"The configured chat template '{configuredTemplate.Name}' (id={configuredTemplate.Id}) is based on a plugin that is not available anymore. Removing the chat template from the settings."); - SETTINGS_MANAGER.ConfigurationData.ChatTemplates.Remove(configuredTemplate); - wasConfigurationChanged = true; - } - - if(!configObjectList.Any(configObject => - configObject.Type is PluginConfigurationObjectType.CHAT_TEMPLATE && - configObject.ConfigPluginId == templateSourcePluginId && - configObject.Id.ToString() == configuredTemplate.Id)) - { - LOG.LogWarning($"The configured chat template '{configuredTemplate.Name}' (id={configuredTemplate.Id}) is not present in the configuration plugin anymore. Removing the chat template from the settings."); - SETTINGS_MANAGER.ConfigurationData.ChatTemplates.Remove(configuredTemplate); - wasConfigurationChanged = true; - } - } + if(PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, configObjectList)) + wasConfigurationChanged = true; - // - // ========================================================== - // Check all possible settings: - // ========================================================== - // - - // Check for updates, and if so, how often? + // Check for update behavior: if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateBehavior, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; - // Allow the user to add providers? + // Check for users allowed to added providers: if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; diff --git a/app/MindWork AI Studio/Tools/Services/DataSourceService.cs b/app/MindWork AI Studio/Tools/Services/DataSourceService.cs index f545aca5..06804474 100644 --- a/app/MindWork AI Studio/Tools/Services/DataSourceService.cs +++ b/app/MindWork AI Studio/Tools/Services/DataSourceService.cs @@ -37,7 +37,7 @@ public sealed class DataSourceService // does not mean anything. We cannot filter the data sources by any means. // We return an empty list. Better safe than sorry. // - if (selectedLLMProvider == default) + if (selectedLLMProvider == Settings.Provider.NONE) { this.logger.LogWarning("The selected LLM provider is not set. We cannot filter the data sources by any means."); return new([], []);