diff --git a/.junie/guidelines.md b/.junie/guidelines.md new file mode 100644 index 00000000..f8dfd452 --- /dev/null +++ b/.junie/guidelines.md @@ -0,0 +1,17 @@ +# Project Guidelines + +## Repository Structure +- The repository and the app consist of a Rust project in the `runtime` folder and a .NET solution in the `app` folder. +- The .NET solution then contains 4 .NET projects: + - `Build Script` is not required for running the app; instead, it contains the build script for creating new releases, for example. + - `MindWork AI Studio` contains the actual app code. + - `SharedTools` contains types that are needed in the build script and in the app, for example. + - `SourceCodeRules` is a Roslyn analyzer project. It contains analyzers and code fixes that we use to enforce code style rules within the team. + +## Changelogs +- There is a changelog in Markdown format for each version. +- All changelogs are located in the folder `app/MindWork AI Studio/wwwroot/changelog`. +- These changelogs are intended for end users, not for developers. +- Therefore, we don't mention all changes in the changelog: changes that end users wouldn't understand remain unmentioned. For complex refactorings, for example, we mention a generic point that the code quality has been improved to enhance future maintenance. +- The changelog is always written in US English. +- The changelog doesn't mention bug fixes if the bug was never shipped and users don't know about it. \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index ec9a8de8..338057f4 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -3415,6 +3415,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T32678 -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3448155331"] = "Close" +-- This template is managed by your organization. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "This template is managed by your organization." + -- Edit Chat Template UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Edit Chat Template" diff --git a/app/MindWork AI Studio/Chat/ChatThread.cs b/app/MindWork AI Studio/Chat/ChatThread.cs index d38ddad2..0193ce28 100644 --- a/app/MindWork AI Studio/Chat/ChatThread.cs +++ b/app/MindWork AI Studio/Chat/ChatThread.cs @@ -109,7 +109,7 @@ public string PrepareSystemPrompt(SettingsManager settingsManager, ChatThread ch else { var chatTemplate = settingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == chatThread.SelectedChatTemplate); - if(chatTemplate == default) + if(chatTemplate == null) systemPromptTextWithChatTemplate = chatThread.SystemPrompt; else { diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index 43f79a10..3c4a8d38 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -327,7 +327,9 @@ private async Task ProfileWasChanged(Profile profile) private async Task ChatTemplateWasChanged(ChatTemplate chatTemplate) { this.currentChatTemplate = chatTemplate; - this.userInput = this.currentChatTemplate.PredefinedUserPrompt; + if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt)) + this.userInput = this.currentChatTemplate.PredefinedUserPrompt; + if(this.ChatThread is null) return; @@ -435,7 +437,7 @@ private async Task SendMessage(bool reuseLastUserPrompt = false) DataSourceOptions = this.earlyDataSourceOptions, Name = this.ExtractThreadName(this.userInput), Seed = this.RNG.Next(), - Blocks = this.currentChatTemplate == default ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(), + Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(), }; await this.ChatThreadChanged.InvokeAsync(this.ChatThread); @@ -673,7 +675,7 @@ private async Task StartNewChat(bool useSameWorkspace = false, bool deletePrevio ChatId = Guid.NewGuid(), Name = string.Empty, Seed = this.RNG.Next(), - Blocks = this.currentChatTemplate == default ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(), + Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(), }; } @@ -813,9 +815,8 @@ private async Task SelectProviderWhenLoadingChat() // Try to select the chat template: if (!string.IsNullOrWhiteSpace(chatChatTemplate)) { - this.currentChatTemplate = this.SettingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == chatChatTemplate); - if(this.currentChatTemplate == default) - this.currentChatTemplate = ChatTemplate.NO_CHAT_TEMPLATE; + var selectedTemplate = this.SettingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == chatChatTemplate); + this.currentChatTemplate = selectedTemplate ?? ChatTemplate.NO_CHAT_TEMPLATE; } } diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs index 16ba7727..608dec29 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 @@ private async Task AddLLMProvider() [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] private async Task EditLLMProvider(AIStudio.Settings.Provider provider) { + if (provider.IsEnterpriseConfiguration) + return; + var dialogParameters = new DialogParameters { { x => x.DataNum, provider.Num }, diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs index e95b0d76..3f9378ef 100644 --- a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs @@ -129,6 +129,9 @@ protected override async Task OnAfterRenderAsync(bool firstRender) PredefinedUserPrompt = this.PredefinedUserPrompt, ExampleConversation = this.dataExampleConversation, AllowProfileUsage = this.AllowProfileUsage, + + EnterpriseConfigurationPluginId = Guid.Empty, + IsEnterpriseConfiguration = false, }; private void RemoveMessage(ContentBlock item) diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor index a11c9de0..bb1fb816 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor @@ -31,14 +31,23 @@ @context.Num @context.Name - - - - - - - - + @if (context.IsEnterpriseConfiguration) + { + + + + } + else + { + + + + + + + + + } diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs index 364eb49a..73ed5fa5 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs @@ -53,6 +53,9 @@ private async Task AddChatTemplate() private async Task EditChatTemplate(ChatTemplate chatTemplate) { + if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration) + return; + var dialogParameters = new DialogParameters { { x => x.DataNum, chatTemplate.Num }, diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index b81e8686..5513e016 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -68,4 +68,28 @@ CONFIG["SETTINGS"] = {} -- Configure the user permission to add providers: -- Allowed values are: true, false --- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false \ No newline at end of file +-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false + +-- Example chat templates for this configuration: +CONFIG["CHAT_TEMPLATES"] = {} + +-- A simple example chat template: +CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { + ["Id"] = "00000000-0000-0000-0000-000000000000", + ["Name"] = "", + ["SystemPrompt"] = "You are 's helpful AI assistant for . Your task is ...", + ["PredefinedUserPrompt"] = "Please help me with ...", + ["AllowProfileUsage"] = true, + ["ExampleConversation"] = { + { + -- Allowed values are: USER, AI, SYSTEM + ["Role"] = "USER", + ["Content"] = "Hello! Can you help me with a quick task?" + }, + { + -- Allowed values are: USER, AI, SYSTEM + ["Role"] = "AI", + ["Content"] = "Of course. What do you need?" + } + } +} 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 c79650fb..4777ab01 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 @@ -3417,6 +3417,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T32678 -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3448155331"] = "Schließen" +-- This template is managed by your organization. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "Diesee Vorlage wird von Ihrer Organisation verwaltet." + -- Edit Chat Template UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Chat-Vorlage bearbeiten" @@ -4584,7 +4587,7 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2348849647"] = "Letztes Änderungsproto -- Choose the provider and model best suited for your current task. UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2588488920"] = "Wählen Sie den Anbieter und das Modell aus, die am besten zu ihrer aktuellen Aufgabe passen." --- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. +-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2900280782"] = "Sie sind an keinen einzelnen Anbieter gebunden. Stattdessen können Sie den Anbieter wählen, der am besten zu ihren Bedürfnissen passt. Derzeit unterstützen wir OpenAI (GPT5, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face und selbst gehostete Modelle mit vLLM, llama.cpp, ollama, LM Studio, Groq oder Fireworks. Für Wissenschaftler und Mitarbeiter von Forschungseinrichtungen unterstützen wir auch die KI-Dienste von Helmholtz und GWDG. Diese sind über föderierte Anmeldungen wie eduGAIN für alle 18 Helmholtz-Zentren, die Max-Planck-Gesellschaft, die meisten deutschen und viele internationale Universitäten verfügbar." -- Quick Start Guide 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 943d3c35..5d546552 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 @@ -3417,6 +3417,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T32678 -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3448155331"] = "Close" +-- This template is managed by your organization. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "This template is managed by your organization." + -- Edit Chat Template UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Edit Chat Template" @@ -4584,7 +4587,7 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2348849647"] = "Last Changelog" -- Choose the provider and model best suited for your current task. UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2588488920"] = "Choose the provider and model best suited for your current task." --- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. +-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2900280782"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities." -- Quick Start Guide diff --git a/app/MindWork AI Studio/Settings/ChatTemplate.cs b/app/MindWork AI Studio/Settings/ChatTemplate.cs index 02e0061c..6842ae00 100644 --- a/app/MindWork AI Studio/Settings/ChatTemplate.cs +++ b/app/MindWork AI Studio/Settings/ChatTemplate.cs @@ -3,8 +3,12 @@ namespace AIStudio.Settings; -public readonly record struct ChatTemplate(uint Num, string Id, string Name, string SystemPrompt, string PredefinedUserPrompt, List ExampleConversation, bool AllowProfileUsage) +public record ChatTemplate(uint Num, string Id, string Name, string SystemPrompt, string PredefinedUserPrompt, List ExampleConversation, bool AllowProfileUsage, bool IsEnterpriseConfiguration = false, Guid EnterpriseConfigurationPluginId = default) { + public ChatTemplate() : this(0, Guid.Empty.ToString(), string.Empty, string.Empty, string.Empty, [], false) + { + } + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatTemplate).Namespace, nameof(ChatTemplate)); public static readonly ChatTemplate NO_CHAT_TEMPLATE = new() @@ -16,6 +20,8 @@ public readonly record struct ChatTemplate(uint Num, string Id, string Name, str Num = uint.MaxValue, ExampleConversation = [], AllowProfileUsage = true, + EnterpriseConfigurationPluginId = Guid.Empty, + IsEnterpriseConfiguration = false, }; #region Overrides of ValueType diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 059d0f12..7cad25a2 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -270,11 +270,11 @@ public Profile GetPreselectedProfile(Tools.Components component) public ChatTemplate GetPreselectedChatTemplate(Tools.Components component) { var preselection = component.PreselectedChatTemplate(this); - if (preselection != default) + if (preselection != ChatTemplate.NO_CHAT_TEMPLATE) return preselection; preselection = this.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedChatTemplate); - return preselection != default ? preselection : ChatTemplate.NO_CHAT_TEMPLATE; + return preselection ?? ChatTemplate.NO_CHAT_TEMPLATE; } public ConfidenceLevel GetConfiguredConfidenceLevel(LLMProviders llmProvider) diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs index e4bd317c..18ac4f41 100644 --- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs +++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs @@ -133,8 +133,8 @@ public static class ComponentsExtensions public static ChatTemplate PreselectedChatTemplate(this Components component, SettingsManager settingsManager) => component switch { - Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) : default, + Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) ?? ChatTemplate.NO_CHAT_TEMPLATE : ChatTemplate.NO_CHAT_TEMPLATE, - _ => default, + _ => ChatTemplate.NO_CHAT_TEMPLATE, }; } \ 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 7de7137f..9e309b10 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -1,5 +1,6 @@ using AIStudio.Provider; using AIStudio.Settings; +using AIStudio.Chat; using Lua; @@ -13,7 +14,14 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT 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 = []; + + /// + /// The list of configuration objects. Configuration objects are, e.g., providers or chat templates. + /// + public IEnumerable ConfigObjects => this.configObjects; + public async Task InitializeAsync(bool dryRun) { if(!this.TryProcessConfiguration(dryRun, out var issue)) @@ -29,11 +37,13 @@ public async Task InitializeAsync(bool dryRun) /// /// Tries to initialize the UI text content of the plugin. /// - /// When true, the method will not apply any changes, but only check if the configuration can be read. + /// When true, the method will not apply any changes but only check if the configuration can be read. /// The error message, when the UI text content could not be read. /// True, when the UI text content could be read successfully. private bool TryProcessConfiguration(bool dryRun, out string message) { + this.configObjects.Clear(); + // Ensure that the main CONFIG table exists and is a valid Lua table: if (!this.state.Environment["CONFIG"].TryRead(out var mainTable)) { @@ -59,7 +69,7 @@ private bool TryProcessConfiguration(bool dryRun, out string message) ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun); // - // Configured providers + // Configured providers: // if (!mainTable.TryGetValue("LLM_PROVIDERS", out var providersValue) || !providersValue.TryRead(out var providersTable)) { @@ -94,6 +104,17 @@ private bool TryProcessConfiguration(bool dryRun, out string message) // 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) { @@ -109,8 +130,65 @@ private bool TryProcessConfiguration(bool dryRun, out string message) 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; } @@ -194,4 +272,96 @@ private bool TryReadModelTable(int idx, LuaTable table, out Model model) 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 new file mode 100644 index 00000000..258e6c3c --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs @@ -0,0 +1,23 @@ +namespace AIStudio.Tools.PluginSystem; + +/// +/// Represents metadata for a configuration object from a configuration plugin. These are +/// complex objects such as configured LLM providers, chat templates, etc. +/// +public sealed record PluginConfigurationObject +{ + /// + /// The id of the configuration plugin to which this configuration object belongs. + /// + public required Guid ConfigPluginId { get; init; } = Guid.NewGuid(); + + /// + /// The id of the configuration object, e.g., the id of a chat template. + /// + public required Guid Id { get; init; } = Guid.NewGuid(); + + /// + /// The type of the configuration object. + /// + public required PluginConfigurationObjectType Type { get; init; } = PluginConfigurationObjectType.NONE; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObjectType.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObjectType.cs new file mode 100644 index 00000000..1cb4f604 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObjectType.cs @@ -0,0 +1,13 @@ +namespace AIStudio.Tools.PluginSystem; + +public enum PluginConfigurationObjectType +{ + NONE, + UNKNOWN, + + PROFILE, + DATA_SOURCE, + LLM_PROVIDER, + CHAT_TEMPLATE, + EMBEDDING_PROVIDER, +} \ 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 667ed867..5972b3a4 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -40,6 +40,8 @@ public static async Task LoadAll(CancellationToken cancellationToken = default) if (!await PLUGIN_LOAD_SEMAPHORE.WaitAsync(0, cancellationToken)) return; + var configObjectList = new List(); + try { LOG.LogInformation("Start loading plugins."); @@ -112,7 +114,8 @@ public static async Task LoadAll(CancellationToken cancellationToken = default) } // Start or restart all plugins: - await RestartAllPlugins(cancellationToken); + var configObjects = await RestartAllPlugins(cancellationToken); + configObjectList.AddRange(configObjects); } finally { @@ -149,9 +152,51 @@ public static async Task LoadAll(CancellationToken cancellationToken = default) 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 + + // + // 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; + } + } + // // ========================================================== // Check all possible settings: diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs index 0943a48e..5d734b06 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs @@ -11,9 +11,10 @@ public static partial class PluginFactory /// public static IReadOnlyCollection RunningPlugins => RUNNING_PLUGINS; - private static async Task RestartAllPlugins(CancellationToken cancellationToken = default) + private static async Task> RestartAllPlugins(CancellationToken cancellationToken = default) { LOG.LogInformation("Try to start or restart all plugins."); + var configObjects = new List(); RUNNING_PLUGINS.Clear(); // @@ -65,7 +66,12 @@ private static async Task RestartAllPlugins(CancellationToken cancellationToken { if (availablePlugin.IsInternal || SETTINGS_MANAGER.IsPluginEnabled(availablePlugin) || availablePlugin.Type == PluginType.CONFIGURATION) if(await Start(availablePlugin, cancellationToken) is { IsValid: true } plugin) + { + if (plugin is PluginConfiguration configPlugin) + configObjects.AddRange(configPlugin.ConfigObjects); + RUNNING_PLUGINS.Add(plugin); + } } catch (Exception e) { @@ -75,6 +81,7 @@ private static async Task RestartAllPlugins(CancellationToken cancellationToken // Inform all components that the plugins have been reloaded or started: await MessageBus.INSTANCE.SendMessage(null, Event.PLUGINS_RELOADED); + return configObjects; } private static async Task Start(IAvailablePlugin meta, CancellationToken cancellationToken = default) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.51.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.51.md index 8e3eaf85..77ab7d97 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.51.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.51.md @@ -1,3 +1,5 @@ # v0.9.51, build 226 (2025-08-xx xx:xx UTC) +- Added support for predefined chat templates in configuration plugins to help enterprises roll out consistent templates across the organization. - Improved memory usage in several areas of the app. +- Improved plugin management for configuration plugins so that hot reload detects when a provider or chat template has been removed. - Fixed a bug in various assistants where some text fields were not reset when resetting.