From 559f3f7fa6f4c9f1c670535a67e46f84fceae955 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 22 Sep 2026 16:39:17 +0200 Subject: [PATCH] Let chat templates preselect tools and data sources (#995) --- .../Assistants/I18N/allTexts.lua | 60 ++++ .../Components/ChatComponent.razor.cs | 120 +++++-- .../Components/DataSourceSelection.razor | 10 +- .../Components/DataSourceSelection.razor.cs | 27 +- .../Components/DirectChatLauncherForm.razor | 18 +- .../DirectChatLauncherForm.razor.cs | 15 + .../Dialogs/ChatTemplateDialog.razor | 30 ++ .../Dialogs/ChatTemplateDialog.razor.cs | 35 ++ .../SettingsDialogChatTemplate.razor.cs | 40 +++ .../Plugins/assistants/README.md | 5 +- .../Plugins/assistants/plugin.lua | 8 +- .../Plugins/configuration/plugin.lua | 50 +++ .../plugin.lua | 60 ++++ .../plugin.lua | 60 ++++ .../Settings/ChatTemplate.cs | 269 +++++++++++++++- .../Tools/Services/DirectChatService.cs | 124 ++++++-- .../wwwroot/changelog/v26.9.1.md | 3 + .../ChatTemplateConfigurationTests.cs | 299 ++++++++++++++++++ .../Settings/ChatTemplatePrecedenceTests.cs | 165 ++++++++++ documentation/Enterprise IT.md | 59 +++- 20 files changed, 1374 insertions(+), 83 deletions(-) create mode 100644 app/Tests/Settings/ChatTemplateConfigurationTests.cs create mode 100644 app/Tests/Settings/ChatTemplatePrecedenceTests.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index d3ea7596..2a17926d 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -3991,6 +3991,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = " -- Chat profile UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2412069346"] = "Chat profile" +-- The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2545184598"] = "The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own." + -- {0} data source(s) selected UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2777836629"] = "{0} data source(s) selected" @@ -4009,6 +4012,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = " -- Use the normal chat data source defaults UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3898572329"] = "Use the normal chat data source defaults" +-- The chosen chat template brings tools of its own, and those win over a selection made here. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T4038774259"] = "The chosen chat template brings tools of its own, and those win over a selection made here." + -- Use no chat template UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T4258819635"] = "Use no chat template" @@ -5668,12 +5674,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1352914344"] = "Only te -- Please enter a message for the example conversation. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1362948628"] = "Please enter a message for the example conversation." +-- No, chats keep the tools from your chat options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1363645855"] = "No, chats keep the tools from your chat options" + -- The chat template name must be unique; the chosen name is already in use. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "The chat template name must be unique; the chosen name is already in use." +-- The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1442266827"] = "The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options." + -- Please enter a name for the chat template. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Please enter a name for the chat template." +-- Yes, this template decides which data a chat starts with +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T17861006"] = "Yes, this template decides which data a chat starts with" + -- Load predefined user input from file UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Load predefined user input from file" @@ -5695,6 +5710,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "File At -- Role UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Role" +-- Yes, this template decides which tools a chat starts with +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2494694135"] = "Yes, this template decides which tools a chat starts with" + +-- Tools +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2499909372"] = "Tools" + -- What predefined user input do you want to use? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2501284417"] = "What predefined user input do you want to use?" @@ -5740,6 +5761,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Are you -- Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here." +-- No, chats keep the data source options from your chat options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "No, chats keep the data source options from your chat options" + +-- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says." + -- Add a message UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message" @@ -5758,6 +5785,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3675108201"] = "Yes, al -- Add a new message below UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3757779731"] = "Add a new message below" +-- Does this chat template preselect data sources? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3779813414"] = "Does this chat template preselect data sources?" + -- Example Conversation UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T380891852"] = "Example Conversation" @@ -5770,6 +5800,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3883091650"] = "Load sy -- Messages per page UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3893704289"] = "Messages per page" +-- Does this chat template preselect tools? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T399377711"] = "Does this chat template preselect tools?" + -- Use the default system prompt UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4051106111"] = "Use the default system prompt" @@ -5782,15 +5815,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Create -- Enter a message UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Enter a message" +-- Data Sources +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T558345131"] = "Data Sources" + -- System Prompt UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System Prompt" +-- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself." + +-- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use." + -- Allow the use of profiles together with this chat template? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Allow the use of profiles together with this chat template?" -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel" +-- Preselected tools +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T975962532"] = "Preselected tools" + -- {0} LLM providers UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T121235760"] = "{0} LLM providers" @@ -7786,6 +7831,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T20545 -- No chat templates configured yet. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "No chat templates configured yet." +-- This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2563631533"] = "This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway?" + -- Chat Template Name UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T275026390"] = "Chat Template Name" @@ -12136,12 +12184,21 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DATASOURCELOCALRETRIEVALSERVICE::T93 -- The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0} UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T103791004"] = "The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}" +-- The following data sources selected by the chat template '{0}' are currently unavailable or not permitted for the selected provider: {1} +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T1164564929"] = "The following data sources selected by the chat template '{0}' are currently unavailable or not permitted for the selected provider: {1}" + +-- The chat template '{0}' references data source '{1}', but that data source does not exist. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T1951110186"] = "The chat template '{0}' references data source '{1}', but that data source does not exist." + -- The assistant chat launcher references profile '{0}', but that profile does not exist. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T2466659933"] = "The assistant chat launcher references profile '{0}', but that profile does not exist." -- The assistant chat launcher references data source '{0}', but that data source does not exist. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T289191545"] = "The assistant chat launcher references data source '{0}', but that data source does not exist." +-- The chat template '{0}' selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3082876173"] = "The chat template '{0}' selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created." + -- The data sources selected by the assistant chat launcher could not be checked. No chat was created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3232401465"] = "The data sources selected by the assistant chat launcher could not be checked. No chat was created." @@ -12151,6 +12208,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3242713584"] = " -- The provider '{0}' selected by the assistant chat launcher is not permitted for chats at the required confidence level. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3491209726"] = "The provider '{0}' selected by the assistant chat launcher is not permitted for chats at the required confidence level." +-- The data sources selected by the chat template '{0}' could not be checked. No chat was created. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T361525913"] = "The data sources selected by the chat template '{0}' could not be checked. No chat was created." + -- The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3780395901"] = "The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created." diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index a11730ba..6af7ec72 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -74,7 +74,7 @@ public partial class ChatComponent : MSGComponentBase private DataSourceSelection? dataSourceSelectionComponent; private DataSourceOptions earlyDataSourceOptions = new(); - private DataSourceOptions lastAppliedStandardDataSourceOptions = new(); + private DataSourceOptions lastAppliedAutomaticDataSourceOptions = new(); private Profile currentProfile = Profile.NO_PROFILE; private ChatTemplate currentChatTemplate = ChatTemplate.NO_CHAT_TEMPLATE; private bool hasUnsavedChanges; @@ -253,9 +253,9 @@ public partial class ChatComponent : MSGComponentBase this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT); if (!this.ComposerState.HasUserDraft && !this.ComposerState.HasComposerContent) this.ComposerState.ApplyTemplate(this.currentChatTemplate); - this.selectedToolIds = ToolSelectionRules.NormalizeSelection(this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT)); - this.lastAppliedStandardDataSourceOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy(); + await this.ApplyChatTemplateToolSelectionAsync(); + this.lastAppliedAutomaticDataSourceOptions = this.GetAutomaticDataSourceOptions(); var deferredInput = MessageBus.INSTANCE.TakeDeferredMessages(Event.SEND_TO_CHAT_INPUT).LastOrDefault(); if (!string.IsNullOrWhiteSpace(deferredInput)) @@ -343,7 +343,7 @@ public partial class ChatComponent : MSGComponentBase // // No, the user did not send an assistant result to the chat. // - this.ApplyStandardDataSourceOptions(); + this.ApplyAutomaticDataSourceOptions(); } // @@ -694,35 +694,75 @@ public partial class ChatComponent : MSGComponentBase } } - private void ApplyStandardDataSourceOptions() + /// + /// Picks the tools a chat starts with: those of its chat template, or the chat defaults. + /// + /// + /// A preselection, not a limit — the user changes it in the chat as usual. A template without a + /// tool selection says nothing about tools and therefore leaves the chat default in place, + /// which is a different statement from a template that selects no tool at all. + /// + private async Task ApplyChatTemplateToolSelectionAsync() { - var chatDefaultOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy(); - this.lastAppliedStandardDataSourceOptions = chatDefaultOptions.CreateCopy(); - this.earlyDataSourceOptions = chatDefaultOptions; - if(this.ChatThread is not null) - this.ChatThread.DataSourceOptions = chatDefaultOptions; - - this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(chatDefaultOptions); - } - - private async Task ApplyUpdatedStandardDataSourceOptionsAfterConfigurationChange() - { - var updatedStandardOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy(); - var previousStandardOptions = this.lastAppliedStandardDataSourceOptions; - this.lastAppliedStandardDataSourceOptions = updatedStandardOptions.CreateCopy(); - - if (this.ChatThread is null) + if (this.currentChatTemplate.ToolIds is not { } templateToolIds) { - this.earlyDataSourceOptions = updatedStandardOptions; - this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(updatedStandardOptions); + this.selectedToolIds = ToolSelectionRules.NormalizeSelection(this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT)); return; } - if (!DataSourceOptionsAreEqual(this.ChatThread.DataSourceOptions, previousStandardOptions)) + // + // Only the tools the user could have switched on themselves: a template may name one whose + // settings are incomplete — an unconfigured web search, say — and starting with it enabled + // would show a state the user cannot produce by hand and cannot fix from the chat. + // + this.selectedToolIds = await this.ToolRegistry.FilterSelectableToolIdsAsync(Tools.Components.CHAT, templateToolIds); + } + + /// + /// The data source options a chat starts with: those of its chat template, or the chat defaults. + /// + /// + /// As with the tools, a template which carries no options says nothing and leaves the chat + /// defaults in place. A template which carries them answers more than which sources to search: + /// whether data sources are used at all, and whether an agent picks them for each message. + /// + private DataSourceOptions GetAutomaticDataSourceOptions() => + this.currentChatTemplate.DataSourceOptions?.CreateCopy() ?? this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy(); + + private void ApplyAutomaticDataSourceOptions() + { + var automaticOptions = this.GetAutomaticDataSourceOptions(); + this.lastAppliedAutomaticDataSourceOptions = automaticOptions.CreateCopy(); + this.earlyDataSourceOptions = automaticOptions; + if(this.ChatThread is not null) + this.ChatThread.DataSourceOptions = automaticOptions; + + this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(automaticOptions); + } + + private async Task ApplyUpdatedAutomaticDataSourceOptionsAfterConfigurationChange() + { + // + // What a chat would start with right now. The chat template is asked first, so that editing + // the template of the current chat reaches it — a change of the chat defaults it does not + // use would say nothing about it. + // + var updatedAutomaticOptions = this.GetAutomaticDataSourceOptions(); + var previousAutomaticOptions = this.lastAppliedAutomaticDataSourceOptions; + this.lastAppliedAutomaticDataSourceOptions = updatedAutomaticOptions.CreateCopy(); + + if (this.ChatThread is null) + { + this.earlyDataSourceOptions = updatedAutomaticOptions; + this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(updatedAutomaticOptions); + return; + } + + if (!DataSourceOptionsAreEqual(this.ChatThread.DataSourceOptions, previousAutomaticOptions)) return; - await this.SetCurrentDataSourceOptions(updatedStandardOptions); - this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(updatedStandardOptions, this.ChatThread.AISelectedDataSources); + await this.SetCurrentDataSourceOptions(updatedAutomaticOptions); + this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(updatedAutomaticOptions, this.ChatThread.AISelectedDataSources); await this.ChatThreadChanged.InvokeAsync(this.ChatThread); } @@ -782,7 +822,18 @@ public partial class ChatComponent : MSGComponentBase this.ComposerState.ReplaceFileAttachments(this.currentChatTemplate.FileAttachments); if (this.ChatThread is not null) + { + // Starting the new chat is what hands the selection of the new template to it: await this.StartNewChat(true); + return; + } + + // + // Without a thread there is nothing to start anew, so the selection of the new template is + // applied right here. It travels into the thread which the first message creates. + // + await this.ApplyChatTemplateToolSelectionAsync(); + this.ApplyAutomaticDataSourceOptions(); } private void RefreshCurrentProfileAndChatTemplate() @@ -819,7 +870,7 @@ public partial class ChatComponent : MSGComponentBase if (!this.ComposerState.HasUserDraft && previousChatTemplate != this.currentChatTemplate) this.ComposerState.ApplyTemplate(this.currentChatTemplate); - await this.ApplyUpdatedStandardDataSourceOptionsAfterConfigurationChange(); + await this.ApplyUpdatedAutomaticDataSourceOptionsAfterConfigurationChange(); } private IReadOnlyList GetAgentSelectedDataSources() @@ -1184,7 +1235,6 @@ public partial class ChatComponent : MSGComponentBase // this.hasUnsavedChanges = false; this.ComposerState.Clear(); - this.selectedToolIds = ToolSelectionRules.NormalizeSelection(this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT)); this.RefreshCurrentProfileAndChatTemplate(); // @@ -1233,9 +1283,11 @@ public partial class ChatComponent : MSGComponentBase this.ComposerState.ApplyTemplate(this.currentChatTemplate); - // Now, we have to reset the data source options as well: - this.ApplyStandardDataSourceOptions(); - + // Now, the chat starts with what its template asks for, and with the chat defaults wherever + // that template says nothing: + await this.ApplyChatTemplateToolSelectionAsync(); + this.ApplyAutomaticDataSourceOptions(); + // Notify the parent component about the change: await this.SyncForegroundChatAsync(); this.MarkCurrentChatAsLoadedParameter(); @@ -1306,9 +1358,9 @@ public partial class ChatComponent : MSGComponentBase this.loadedParameterWorkspaceId = Guid.Empty; this.ClearWorkspaceHeaderState(); await this.SyncForegroundChatAsync(); - this.ApplyStandardDataSourceOptions(); + this.ApplyAutomaticDataSourceOptions(); } - + await this.SelectProviderWhenLoadingChat(); if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading) { @@ -1341,7 +1393,7 @@ public partial class ChatComponent : MSGComponentBase this.ChatThread = null; this.MarkCurrentChatAsLoadedParameter(); await this.SyncForegroundChatAsync(); - this.ApplyStandardDataSourceOptions(); + this.ApplyAutomaticDataSourceOptions(); await this.ChatThreadChanged.InvokeAsync(this.ChatThread); } diff --git a/app/MindWork AI Studio/Components/DataSourceSelection.razor b/app/MindWork AI Studio/Components/DataSourceSelection.razor index 11001174..3388b710 100644 --- a/app/MindWork AI Studio/Components/DataSourceSelection.razor +++ b/app/MindWork AI Studio/Components/DataSourceSelection.razor @@ -179,13 +179,13 @@ else if (this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE) } - + @if (this.areDataSourcesEnabled) { - - - - + + + + @* The configuration mode lists what was configured, without filtering it, so no row here is ever waiting for an index. diff --git a/app/MindWork AI Studio/Components/DataSourceSelection.razor.cs b/app/MindWork AI Studio/Components/DataSourceSelection.razor.cs index f1e619c0..170d6a0b 100644 --- a/app/MindWork AI Studio/Components/DataSourceSelection.razor.cs +++ b/app/MindWork AI Studio/Components/DataSourceSelection.razor.cs @@ -38,7 +38,28 @@ public partial class DataSourceSelection : MSGComponentBase [Parameter] public bool AutoSaveAppSettings { get; set; } - + + /// + /// Shows the options without letting the user change them. + /// + /// + /// For options somebody else decided on, such as those of a chat template an organization + /// rolled out. Seeing which data such a chat will search is the point; changing it here is not. + /// + [Parameter] + public bool ReadOnly { get; set; } + + /// + /// Whether the options edited here are the data source defaults of the chat. + /// + /// + /// Those defaults can be locked by a configuration plugin, and this component reads the locks + /// from the chat settings. Wherever the same options belong to something else — to a chat + /// template, say — the locks of the chat defaults have nothing to say about them. + /// + [Parameter] + public bool ConfiguresChatDefaults { get; set; } = true; + [Inject] private DataSourceService DataSourceService { get; init; } = null!; @@ -334,6 +355,7 @@ public partial class DataSourceSelection : MSGComponentBase private bool IsPreselectedDataSourcesDisabledLocked() { return this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE + && this.ConfiguresChatDefaults && ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedDataSourcesDisabled, out var meta) && meta.IsLocked; } @@ -341,6 +363,7 @@ public partial class DataSourceSelection : MSGComponentBase private bool IsPreselectedDataSourcesAutomaticSelectionLocked() { return this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE + && this.ConfiguresChatDefaults && ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedDataSourcesAutomaticSelection, out var meta) && meta.IsLocked; } @@ -348,6 +371,7 @@ public partial class DataSourceSelection : MSGComponentBase private bool IsPreselectedDataSourcesAutomaticValidationLocked() { return this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE + && this.ConfiguresChatDefaults && ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedDataSourcesAutomaticValidation, out var meta) && meta.IsLocked; } @@ -355,6 +379,7 @@ public partial class DataSourceSelection : MSGComponentBase private bool IsPreselectedDataSourceIdsLocked() { return this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE + && this.ConfiguresChatDefaults && ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedDataSourceIds, out var meta) && meta.IsLocked; } diff --git a/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor b/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor index af72de38..2ccad984 100644 --- a/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor +++ b/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor @@ -51,4 +51,20 @@ } - \ No newline at end of file +@* A chat template wins over what is chosen here, so the form says so before somebody picks + something which would never take effect: *@ +@if (this.SelectedChatTemplate.DataSourceOptions is not null) +{ + + @T("The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own.") + +} + + + +@if (this.SelectedChatTemplate.ToolIds is not null) +{ + + @T("The chosen chat template brings tools of its own, and those win over a selection made here.") + +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor.cs b/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor.cs index bdd363ac..902d39ee 100644 --- a/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor.cs +++ b/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor.cs @@ -1,3 +1,5 @@ +using AIStudio.Settings; + using Microsoft.AspNetCore.Components; namespace AIStudio.Components; @@ -81,6 +83,19 @@ public partial class DirectChatLauncherForm : MSGComponentBase /// private bool OpensTemporaryChat => string.IsNullOrWhiteSpace(this.WorkspaceName); + /// + /// The chat template the launcher would open its chat with, as far as it is known here. + /// + /// + /// With "use chat default" chosen, this is whichever template the chat options name right now, + /// and that may well be another one by the time somebody opens the launcher. The form therefore + /// only says what such a template brings along instead of disabling the fields below it: a field + /// which locks itself behind the user's back is worse than a sentence explaining the situation. + /// + private ChatTemplate SelectedChatTemplate => string.IsNullOrWhiteSpace(this.ChatTemplateId) + ? this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT) + : this.SettingsManager.GetChatTemplateById(this.ChatTemplateId); + private IReadOnlyList availableWorkspaces = []; private static readonly Dictionary USER_INPUT_ATTRIBUTES = new(); diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor index 638e9b9c..c26d4872 100644 --- a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor @@ -1,4 +1,5 @@ @using AIStudio.Chat +@using AIStudio.Settings.DataModel @inherits MSGComponentBase @@ -109,6 +110,35 @@ + + @T("Tools") + + + @T("A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says.") + + + @if (this.preselectTools) + { + + + + } + + @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) + { + + @T("Data Sources") + + + @T("The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options.") + + + @if (this.preselectDataSources) + { + + } + } + @T("Example Conversation") diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs index 24d0b0e7..942d2727 100644 --- a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs @@ -1,6 +1,7 @@ using AIStudio.Chat; using AIStudio.Components; using AIStudio.Settings; +using AIStudio.Settings.DataModel; using Microsoft.AspNetCore.Components; @@ -59,6 +60,18 @@ public partial class ChatTemplateDialog : MSGComponentBase [Parameter] public bool AllowProfileUsage { get; set; } = true; + /// + /// The tools this template preselects, or null when it says nothing about tools. + /// + [Parameter] + public HashSet? ToolIds { get; set; } + + /// + /// The data source options this template preselects, or null when it says nothing about them. + /// + [Parameter] + public DataSourceOptions? DataSourceOptions { get; set; } + [Parameter] public bool CreateFromExistingChatThread { get; set; } @@ -78,6 +91,10 @@ public partial class ChatTemplateDialog : MSGComponentBase private bool dataIsValid; private List dataExampleConversation = []; private HashSet fileAttachments = []; + private bool preselectTools; + private HashSet selectedToolIds = new(StringComparer.Ordinal); + private bool preselectDataSources; + private DataSourceOptions templateDataSourceOptions = new(); private string[] dataIssues = []; private string dataEditingPreviousName = string.Empty; private bool isInlineEditOnGoing; @@ -97,6 +114,20 @@ public partial class ChatTemplateDialog : MSGComponentBase // Load the used instance names: this.UsedNames = this.SettingsManager.ConfigurationData.ChatTemplates.Select(x => x.Name.ToLowerInvariant()).ToList(); + // + // The two switches below carry the third state of the preselection: switched off, this + // template says nothing, and a chat started with it uses the defaults from the chat + // options. Their working copies live apart from the parameters, so switching a + // preselection off and on again does not throw away what was picked. + // + this.preselectTools = this.ToolIds is not null; + this.selectedToolIds = this.ToolIds is null ? new(StringComparer.Ordinal) : new(this.ToolIds, StringComparer.Ordinal); + this.preselectDataSources = this.DataSourceOptions is not null; + + // Saying that this template preselects data sources is already the statement that it wants + // them, so the switch inside the selection starts on instead of at its usual default: + this.templateDataSourceOptions = this.DataSourceOptions?.CreateCopy() ?? new DataSourceOptions { DisableDataSources = false }; + // When editing, we need to load the data: if(this.IsEditing) { @@ -138,11 +169,15 @@ public partial class ChatTemplateDialog : MSGComponentBase ExampleConversation = this.dataExampleConversation, FileAttachments = this.fileAttachments.Select(attachment => attachment.Normalize()).ToList(), AllowProfileUsage = this.AllowProfileUsage, + ToolIds = this.preselectTools ? new HashSet(this.selectedToolIds, StringComparer.Ordinal) : null, + DataSourceOptions = this.preselectDataSources ? this.templateDataSourceOptions.CreateCopy() : null, EnterpriseConfigurationPluginId = Guid.Empty, IsEnterpriseConfiguration = false, }; + private void SetSelectedToolIds(HashSet toolIds) => this.selectedToolIds = toolIds; + private void RemoveMessage(ContentBlock item) { if (this.IsReadOnly) diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs index becd4645..b941beab 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs @@ -69,6 +69,8 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase { x => x.ExampleConversation, chatTemplate.ExampleConversation }, { x => x.FileAttachments, chatTemplate.FileAttachments }, { x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage }, + { x => x.ToolIds, chatTemplate.ToolIds }, + { x => x.DataSourceOptions, chatTemplate.DataSourceOptions }, }; var dialogReference = await this.DialogService.ShowAsync(T("Edit Chat Template"), dialogParameters, DialogOptions.FULLSCREEN); @@ -97,6 +99,8 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase { x => x.ExampleConversation, chatTemplate.ExampleConversation }, { x => x.FileAttachments, chatTemplate.FileAttachments }, { x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage }, + { x => x.ToolIds, chatTemplate.ToolIds }, + { x => x.DataSourceOptions, chatTemplate.DataSourceOptions }, }; await this.DialogService.ShowAsync(T("View Chat Template"), dialogParameters, DialogOptions.FULLSCREEN); @@ -128,6 +132,9 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration) return; + if (!await this.ConfirmExportOfLocalDataSources(chatTemplate)) + return; + await this.CopyChatTemplateLuaToClipboard(chatTemplate); } @@ -141,10 +148,14 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase if (chatTemplate.FileAttachments.Count == 0) { + // That way asks about the local data sources itself, so we must not ask twice: await this.ExportChatTemplateWithSharedAttachmentPaths(chatTemplate); return; } + if (!await this.ConfirmExportOfLocalDataSources(chatTemplate)) + return; + this.isPluginDirectoryDialogOpen = true; try { @@ -160,6 +171,35 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase } } + /// + /// Asks whether to export a template although it preselects data sources of this machine. + /// + /// + /// The export writes the preselected data source IDs unchanged, which is what makes a template + /// usable across an organization — but a local file or folder exists here and nowhere else, so + /// its ID points at nothing on the machine reading the plugin. Nothing breaks, the chat simply + /// starts without that source, and that is precisely why it has to be said beforehand: nobody + /// would notice it afterwards. Exporting anyway is a fair choice, because the rest of the + /// template is worth rolling out. + /// + /// The chat template about to be exported. + /// True when the export may go ahead. + private async Task ConfirmExportOfLocalDataSources(ChatTemplate chatTemplate) + { + var localDataSourceNames = ChatTemplate.GetPreselectedLocalDataSourceNames(chatTemplate, this.SettingsManager.ConfigurationData.DataSources); + if (localDataSourceNames.Count == 0) + return true; + + var dialogParameters = new DialogParameters + { + { x => x.Message, string.Format(T("This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway?"), string.Join(", ", localDataSourceNames)) }, + }; + + var dialogReference = await this.DialogService.ShowAsync(T("Export Chat Template"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + return dialogResult is { Canceled: false }; + } + private async Task CopyChatTemplateLuaToClipboard(ChatTemplate chatTemplate) { if (!chatTemplate.TryExportAsConfigurationSection(out var luaCode, out var issue)) diff --git a/app/MindWork AI Studio/Plugins/assistants/README.md b/app/MindWork AI Studio/Plugins/assistants/README.md index 443a89b8..311c952d 100644 --- a/app/MindWork AI Studio/Plugins/assistants/README.md +++ b/app/MindWork AI Studio/Plugins/assistants/README.md @@ -147,10 +147,13 @@ ASSISTANT = { - `OPEN_TEMPORARY_CHAT` must not carry a `WorkspaceName`. A name next to it stops the plugin from loading rather than being ignored, so a leftover or mistyped name cannot silently turn a workspace launcher into a disappearing one. - Omitted optional IDs use the chat defaults active when the tile is opened. An explicit empty GUID selects no profile or no chat template; an empty provider or data-source GUID is invalid. - `ProviderId` overrides both the chat-specific and app-wide default provider. It must name a provider that is permitted for chats at the required confidence level. -- Explicit data sources are enabled and manually preselected, automatic source selection is disabled, and the normal automatic-validation setting is retained. Every referenced source must currently be available and permitted for the effective provider. +- Explicit data sources are enabled and manually preselected, automatic source selection is disabled, and the normal automatic-validation setting is retained. Every referenced source must currently be available and permitted for the effective provider. This describes a launcher whose chat template brings no data source options of its own; see the rule below for the case where it does. - Invalid or unavailable references stop the launch with an error before a workspace or chat is created. - A selected chat template supplies the chat system prompt, profile allowance, predefined user prompt, attachments, and cloned example conversation. A launcher `SystemPrompt`, if retained in an older plugin, is ignored, so there is never a second competing system prompt. - When the selected chat template does not allow profiles, the template wins: the launcher `ProfileId` is dropped and the chat starts without a profile. This matches the disabled profile selection such a template produces in the chat. +- A chat template may preselect tools and data sources as well. When it does, it decides them alone: the launcher `ToolIds` and `DataSourceIds` are dropped, and AI Studio writes a warning into the log naming both sides. The rule is the same for tools and for data sources, so there is only one to remember. +- The reason the template wins as a whole rather than field by field is a difference in what the two can express. A launcher can only ever say "these sources, picked by hand", while a chat template carries the whole data source options and can also say "let an agent pick the sources for each message". A mix of both would be something neither of them asked for. +- The data sources of a chat template are checked exactly like the ones of a launcher: a source which no longer exists, or which is not permitted for the effective provider, stops the launch before a workspace or chat is created. The message then names the chat template as the cause, not the launcher. A template which leaves the choice to an agent names no source and is therefore not checked here; that decision is made per message in the chat. - The predefined user prompt and the attachments of the selected chat template are placed into the chat input, unless the user already has an unsent draft there. ### Editing a launcher in AI Studio diff --git a/app/MindWork AI Studio/Plugins/assistants/plugin.lua b/app/MindWork AI Studio/Plugins/assistants/plugin.lua index 21d57742..54a28f2e 100644 --- a/app/MindWork AI Studio/Plugins/assistants/plugin.lua +++ b/app/MindWork AI Studio/Plugins/assistants/plugin.lua @@ -454,12 +454,18 @@ ASSISTANT = { ["ProviderId"] = "", ["ProfileId"] = "", ["ChatTemplateId"] = "", + -- Optional: the data sources the chat starts with. A chat template chosen above may bring + -- data source options of its own. It then decides them alone, the IDs named here are dropped, + -- and AI Studio writes a warning into the log. Only a chat template can also say that the AI + -- picks the sources for each message, which is why it wins as a whole instead of field by + -- field. ["DataSourceIds"] = { "", }, -- Optional: the tools preselected when the chat opens. Users may change the selection -- in the chat afterwards, and every tool has to meet the confidence requirements of the - -- provider in use. A tool ID unknown to the installation is ignored. + -- provider in use. A tool ID unknown to the installation is ignored. The same rule as for the + -- data sources applies here: a chat template which names tools of its own wins over this list. -- Tool IDs include: web_search, read_web_page ["ToolIds"] = { "", diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 8068531b..61590c55 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -1034,6 +1034,56 @@ CONFIG["CHAT_TEMPLATES"] = {} -- } -- } +-- An example chat template which preselects tools and data sources: +-- Both are optional and independent of each other. Leaving a field out is not the same as +-- leaving it empty: +-- +-- ToolIds omitted -> the chat starts with the tools set as its default +-- ToolIds = {} -> the chat starts with no tools at all +-- DataSourceOptions omitted -> the chat starts with the data source defaults +-- DataSourceOptions = { ... } -> the chat starts with exactly what this table says +-- +-- Both are a preselection, not a limit: users change either of them in the chat as usual. +-- CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { +-- ["Id"] = "00000000-0000-0000-0000-000000000002", +-- ["Name"] = "Intranet Research", +-- ["SystemPrompt"] = "You are 's research assistant. Answer from our own documents and say where each answer comes from.", +-- ["AllowProfileUsage"] = true, +-- +-- -- Optional: the tools a chat with this template starts with, by tool ID. +-- -- A tool ID unknown to the installation is ignored, and so is a tool your +-- -- organization switched off. A tool has to meet the confidence requirements of the +-- -- provider in use, so it may stay unavailable even though this template names it. +-- -- Tool IDs include: web_search, read_web_page +-- ["ToolIds"] = { +-- "read_web_page", +-- }, +-- +-- -- Optional: the data source options a chat with this template starts with. +-- -- Every field inside is optional as well. DisableDataSources defaults to false here, +-- -- because writing this table at all says that the template wants data sources; the +-- -- other three default to false and an empty list. +-- ["DataSourceOptions"] = { +-- -- Set to true to start the chat with data sources switched off. +-- ["DisableDataSources"] = false, +-- +-- -- Let an agent choose the fitting data sources for each question. When true, +-- -- PreselectedDataSourceIds is not used. +-- ["AutomaticDataSourceSelection"] = false, +-- +-- -- Let an agent check whether the retrieved data fits the question. +-- ["AutomaticValidation"] = true, +-- +-- -- Must contain IDs from CONFIG["DATA_SOURCES"] or user-configured data sources. +-- -- IDs from another configuration of your organization work as well: they are +-- -- resolved against every known data source, not only against the ones defined +-- -- here. IDs that resolve to nothing are ignored. +-- ["PreselectedDataSourceIds"] = { +-- "00000000-0000-0000-0000-000000000000", +-- }, +-- }, +-- } + -- Introduction texts shown as expansion panels on the welcome page: CONFIG["INTRODUCTIONS"] = {} 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 c0e2d70d..2392f8b3 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 @@ -3993,6 +3993,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = " -- Chat profile UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2412069346"] = "Chat-Profil" +-- The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2545184598"] = "Die ausgewählte Chat-Vorlage bringt eigene Datenquellen mit; diese haben Vorrang vor einer hier getroffenen Auswahl. Nur eine Vorlage kann die Auswahl der Quellen auch der KI überlassen, weshalb allein die Vorlage darüber entscheidet." + -- {0} data source(s) selected UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2777836629"] = "{0} Datenquelle(n) ausgewählt" @@ -4011,6 +4014,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = " -- Use the normal chat data source defaults UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3898572329"] = "Die Standardwerte der Datenquelle für den normalen Chat verwenden" +-- The chosen chat template brings tools of its own, and those win over a selection made here. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T4038774259"] = "Die ausgewählte Chat-Vorlage bringt eigene Werkzeuge mit; diese haben Vorrang vor einer hier getroffenen Auswahl." + -- Use no chat template UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T4258819635"] = "Kein Chat-Template verwenden" @@ -5670,12 +5676,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1352914344"] = "Im Bear -- Please enter a message for the example conversation. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1362948628"] = "Bitte gib eine Nachricht für die Beispiel-Konversation ein." +-- No, chats keep the tools from your chat options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1363645855"] = "Nein, Chats behalten die Werkzeuge aus Ihren Chat-Optionen" + -- The chat template name must be unique; the chosen name is already in use. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "Der Name der Chat-Vorlage muss eindeutig sein; der gewählte Name wird bereits verwendet." +-- The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1442266827"] = "Das Gleiche gilt für Ihre Daten. Eine Chat-Vorlage kann eigene Datenquellen-Optionen mitbringen – dazu gehört auch, die Auswahl der Quellen der KI zu überlassen. Ohne solche Optionen starten diese Chats mit den Datenquellen-Optionen aus Ihren Chat-Optionen." + -- Please enter a name for the chat template. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Bitte geben Sie einen Namen für die Chat-Vorlage ein." +-- Yes, this template decides which data a chat starts with +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T17861006"] = "Ja, diese Vorlage legt fest, mit welchen Daten ein Chat startet" + -- Load predefined user input from file UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Vordefinierte Benutzereingabe aus Datei laden" @@ -5697,6 +5712,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "Dateian -- Role UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Rolle" +-- Yes, this template decides which tools a chat starts with +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2494694135"] = "Ja, diese Vorlage legt fest, mit welchen Werkzeugen ein Chat startet" + +-- Tools +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2499909372"] = "Werkzeuge" + -- What predefined user input do you want to use? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2501284417"] = "Welche vordefinierte Benutzereingabe möchten Sie verwenden?" @@ -5742,6 +5763,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Sind Si -- Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Die gleichzeitige Verwendung einiger Chat-Vorlagen mit Profilen kann zu Problemen führen. Deshalb könnten Sie hier die Nutzung von Profilen untersagen." +-- No, chats keep the data source options from your chat options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "Nein, Chats behalten die Datenquellen-Optionen aus Ihren Chat-Optionen" + +-- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "Eine Chat-Vorlage kann festlegen, mit welchen Werkzeugen ein Chat startet. Ohne eine solche Festlegung starten diese Chats mit den Werkzeugen, die Sie in den Chat-Optionen als Standard ausgewählt haben. Legen Sie es fest und wählen dann nichts aus, ist das eine andere Aussage: Solche Chats starten ohne jedes Werkzeug, ganz gleich, was Ihr Standard vorsieht." + -- Add a message UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Nachricht hinzufügen" @@ -5760,6 +5787,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3675108201"] = "Ja, Pro -- Add a new message below UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3757779731"] = "Neue Nachricht unten hinzufügen" +-- Does this chat template preselect data sources? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3779813414"] = "Wählt diese Chat-Vorlage Datenquellen vorab aus?" + -- Example Conversation UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T380891852"] = "Beispiel-Konversation" @@ -5772,6 +5802,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3883091650"] = "System- -- Messages per page UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3893704289"] = "Nachrichten pro Seite" +-- Does this chat template preselect tools? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T399377711"] = "Wählt diese Chat-Vorlage Werkzeuge vorab aus?" + -- Use the default system prompt UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4051106111"] = "Verwenden Sie den Standard-System-Prompt" @@ -5784,15 +5817,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Erstell -- Enter a message UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Nachricht eingeben" +-- Data Sources +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T558345131"] = "Datenquellen" + -- System Prompt UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System-Prompt" +-- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "Ein mit dieser Vorlage gestarteter Chat beginnt mit diesen Datenquellen und Optionen. Alles davon lässt sich im Chat selbst weiterhin ändern." + +-- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "Die Auswahl lässt sich im Chat weiterhin ändern, und jedes Werkzeug muss die Vertrauensanforderungen des verwendeten Anbieters erfüllen." + -- Allow the use of profiles together with this chat template? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Erlauben Sie die Verwendung von Profilen zusammen mit dieser Chat-Vorlage?" -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Abbrechen" +-- Preselected tools +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T975962532"] = "Vorausgewählte Werkzeuge" + -- {0} LLM providers UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T121235760"] = "{0} LLM-Anbieter" @@ -7788,6 +7833,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T20545 -- No chat templates configured yet. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "Noch keine Chat-Vorlagen konfiguriert." +-- This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2563631533"] = "Diese Chat-Vorlage wählt Datenquellen vorab aus, die es nur auf diesem Rechner gibt: {0}. Solche Quellen lassen sich nicht bereitstellen; ein Chat, der auf einem anderen Rechner mit dieser Vorlage startet, beginnt daher ohne sie. Möchten Sie die Vorlage trotzdem exportieren?" + -- Chat Template Name UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T275026390"] = "Name der Chat-Vorlage" @@ -12138,12 +12186,21 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DATASOURCELOCALRETRIEVALSERVICE::T93 -- The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0} UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T103791004"] = "Die folgenden vom Chat-Schnellstart-Assistenten ausgewählten Datenquellen sind derzeit nicht verfügbar oder für den ausgewählten Anbieter nicht zugelassen: {0}" +-- The following data sources selected by the chat template '{0}' are currently unavailable or not permitted for the selected provider: {1} +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T1164564929"] = "Die folgenden von der Chat-Vorlage „{0}“ ausgewählten Datenquellen sind derzeit nicht verfügbar oder für den ausgewählten Anbieter nicht zugelassen: {1}" + +-- The chat template '{0}' references data source '{1}', but that data source does not exist. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T1951110186"] = "Die Chat-Vorlage „{0}“ verweist auf die Datenquelle „{1}“, diese Datenquelle existiert jedoch nicht." + -- The assistant chat launcher references profile '{0}', but that profile does not exist. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T2466659933"] = "Der Chat-Schnellstart-Assistent verweist auf das Profil „{0}“, aber dieses Profil existiert nicht." -- The assistant chat launcher references data source '{0}', but that data source does not exist. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T289191545"] = "Der Chat-Schnellstart-Assistent verweist auf die Datenquelle „{0}“, aber diese Datenquelle existiert nicht." +-- The chat template '{0}' selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3082876173"] = "Die Chat-Vorlage „{0}“ wählt Datenquellen aus, aber für Chats ist kein Anbieter verfügbar. Bitte wählen Sie zuerst einen Standardanbieter für Chats aus. Es wurde kein Chat erstellt." + -- The data sources selected by the assistant chat launcher could not be checked. No chat was created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3232401465"] = "Die vom Chat-Schnellstart-Assistenten ausgewählten Datenquellen konnten nicht geprüft werden. Es wurde kein Chat erstellt." @@ -12153,6 +12210,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3242713584"] = " -- The provider '{0}' selected by the assistant chat launcher is not permitted for chats at the required confidence level. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3491209726"] = "Der vom Chat-Schnellstart-Assistenten ausgewählte Anbieter „{0}“ ist für Chats mit der erforderlichen Zuverlässigkeitsstufe nicht zugelassen." +-- The data sources selected by the chat template '{0}' could not be checked. No chat was created. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T361525913"] = "Die von der Chat-Vorlage „{0}“ ausgewählten Datenquellen konnten nicht geprüft werden. Es wurde kein Chat erstellt." + -- The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3780395901"] = "Der Chat-Schnellstart-Assistent wählt Datenquellen aus, aber für Chats ist kein Anbieter verfügbar. Bitte wählen Sie zuerst einen Standardanbieter für Chats aus. Es wurde kein Chat erstellt." 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 4725c524..41d5692c 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 @@ -3993,6 +3993,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = " -- Chat profile UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2412069346"] = "Chat profile" +-- The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2545184598"] = "The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own." + -- {0} data source(s) selected UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2777836629"] = "{0} data source(s) selected" @@ -4011,6 +4014,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = " -- Use the normal chat data source defaults UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3898572329"] = "Use the normal chat data source defaults" +-- The chosen chat template brings tools of its own, and those win over a selection made here. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T4038774259"] = "The chosen chat template brings tools of its own, and those win over a selection made here." + -- Use no chat template UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T4258819635"] = "Use no chat template" @@ -5670,12 +5676,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1352914344"] = "Only te -- Please enter a message for the example conversation. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1362948628"] = "Please enter a message for the example conversation." +-- No, chats keep the tools from your chat options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1363645855"] = "No, chats keep the tools from your chat options" + -- The chat template name must be unique; the chosen name is already in use. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "The chat template name must be unique; the chosen name is already in use." +-- The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1442266827"] = "The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options." + -- Please enter a name for the chat template. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Please enter a name for the chat template." +-- Yes, this template decides which data a chat starts with +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T17861006"] = "Yes, this template decides which data a chat starts with" + -- Load predefined user input from file UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Load predefined user input from file" @@ -5697,6 +5712,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "File At -- Role UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Role" +-- Yes, this template decides which tools a chat starts with +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2494694135"] = "Yes, this template decides which tools a chat starts with" + +-- Tools +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2499909372"] = "Tools" + -- What predefined user input do you want to use? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2501284417"] = "What predefined user input do you want to use?" @@ -5742,6 +5763,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Are you -- Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here." +-- No, chats keep the data source options from your chat options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "No, chats keep the data source options from your chat options" + +-- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says." + -- Add a message UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message" @@ -5760,6 +5787,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3675108201"] = "Yes, al -- Add a new message below UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3757779731"] = "Add a new message below" +-- Does this chat template preselect data sources? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3779813414"] = "Does this chat template preselect data sources?" + -- Example Conversation UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T380891852"] = "Example Conversation" @@ -5772,6 +5802,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3883091650"] = "Load sy -- Messages per page UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3893704289"] = "Messages per page" +-- Does this chat template preselect tools? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T399377711"] = "Does this chat template preselect tools?" + -- Use the default system prompt UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4051106111"] = "Use the default system prompt" @@ -5784,15 +5817,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Create -- Enter a message UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Enter a message" +-- Data Sources +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T558345131"] = "Data Sources" + -- System Prompt UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System Prompt" +-- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself." + +-- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use." + -- Allow the use of profiles together with this chat template? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Allow the use of profiles together with this chat template?" -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel" +-- Preselected tools +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T975962532"] = "Preselected tools" + -- {0} LLM providers UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T121235760"] = "{0} LLM providers" @@ -7788,6 +7833,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T20545 -- No chat templates configured yet. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "No chat templates configured yet." +-- This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2563631533"] = "This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway?" + -- Chat Template Name UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T275026390"] = "Chat Template Name" @@ -12138,12 +12186,21 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DATASOURCELOCALRETRIEVALSERVICE::T93 -- The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0} UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T103791004"] = "The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}" +-- The following data sources selected by the chat template '{0}' are currently unavailable or not permitted for the selected provider: {1} +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T1164564929"] = "The following data sources selected by the chat template '{0}' are currently unavailable or not permitted for the selected provider: {1}" + +-- The chat template '{0}' references data source '{1}', but that data source does not exist. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T1951110186"] = "The chat template '{0}' references data source '{1}', but that data source does not exist." + -- The assistant chat launcher references profile '{0}', but that profile does not exist. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T2466659933"] = "The assistant chat launcher references profile '{0}', but that profile does not exist." -- The assistant chat launcher references data source '{0}', but that data source does not exist. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T289191545"] = "The assistant chat launcher references data source '{0}', but that data source does not exist." +-- The chat template '{0}' selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3082876173"] = "The chat template '{0}' selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created." + -- The data sources selected by the assistant chat launcher could not be checked. No chat was created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3232401465"] = "The data sources selected by the assistant chat launcher could not be checked. No chat was created." @@ -12153,6 +12210,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3242713584"] = " -- The provider '{0}' selected by the assistant chat launcher is not permitted for chats at the required confidence level. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3491209726"] = "The provider '{0}' selected by the assistant chat launcher is not permitted for chats at the required confidence level." +-- The data sources selected by the chat template '{0}' could not be checked. No chat was created. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T361525913"] = "The data sources selected by the chat template '{0}' could not be checked. No chat was created." + -- The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3780395901"] = "The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created." diff --git a/app/MindWork AI Studio/Settings/ChatTemplate.cs b/app/MindWork AI Studio/Settings/ChatTemplate.cs index c3d93ad9..ceacdc2f 100644 --- a/app/MindWork AI Studio/Settings/ChatTemplate.cs +++ b/app/MindWork AI Studio/Settings/ChatTemplate.cs @@ -1,6 +1,7 @@ using System.Text; using AIStudio.Chat; +using AIStudio.Settings.DataModel; using AIStudio.Tools.PluginSystem; using SharedTools; @@ -26,6 +27,29 @@ public record ChatTemplate( public ChatTemplate() : this(0, Guid.Empty.ToString(), string.Empty, string.Empty, string.Empty, [], [], false) { } + + /// + /// The tools this template preselects for a chat started with it. + /// + /// + /// Null means the template says nothing about tools, so the chat starts with the tools chosen + /// as its default in the app settings. An empty set is the opposite statement: this template + /// wants no tools at all, whatever that default says.

+ /// A preselection, not a limit: the user changes the selection in the chat as usual, and a + /// tool still has to meet the confidence requirements of the provider in use. + ///
+ public HashSet? ToolIds { get; init; } + + /// + /// The data source options a chat started with this template begins with. + /// + /// + /// Null means the template says nothing, so the chat starts with the data source defaults from + /// the app settings. Anything else is the template's own answer, and it carries more than a + /// list of sources: whether data sources are used at all, whether an agent picks them, and + /// whether the retrieved data is validated. + /// + public DataSourceOptions? DataSourceOptions { get; init; } private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatTemplate).Namespace, nameof(ChatTemplate)); @@ -41,6 +65,8 @@ public record ChatTemplate( ExampleConversation = [], FileAttachments = [], AllowProfileUsage = true, + ToolIds = null, + DataSourceOptions = null, EnterpriseConfigurationPluginId = Guid.Empty, IsEnterpriseConfiguration = false, }; @@ -76,10 +102,75 @@ public record ChatTemplate( { if(this.Num == uint.MaxValue) return string.Empty; - + return this.SystemPrompt; } + /// + /// Decides whose tools a chat started by a launcher begins with. + /// + /// + /// A launcher may name tools itself and may choose a chat template which names tools as well. + /// When both do, the template wins as a whole — the same rule as for the data sources, so that + /// nobody has to remember two of them. + /// + /// The chat template the launcher opens its chat with. + /// The tools the launcher names itself, or null when it names none. + /// The tools to start with — null when neither says anything, which leaves the chat default in place — and whether the launcher's own choice was dropped for it. + public static (IReadOnlyCollection? ToolIds, bool LauncherChoiceDropped) ChooseToolIds(ChatTemplate chatTemplate, IReadOnlyCollection? launcherToolIds) + { + if (chatTemplate.ToolIds is not { } templateToolIds) + return (launcherToolIds, false); + + return (templateToolIds, launcherToolIds is not null); + } + + /// + /// Decides whose data source options a chat started by a launcher begins with. + /// + /// + /// The two sides are not equally expressive: a launcher can only ever say "these sources, picked + /// by hand", while a chat template carries the whole options and can also say "let an agent pick + /// them for each message". Mixing them field by field would produce something neither of them + /// asked for, so the template wins as a whole. + /// + /// The chat template the launcher opens its chat with. + /// The options built from the data sources the launcher names, or null when it names none. + /// The options to start with — null when neither says anything, which leaves the chat default in place — and whether the launcher's own choice was dropped for them. + public static (DataSourceOptions? Options, bool LauncherChoiceDropped) ChooseDataSourceOptions(ChatTemplate chatTemplate, DataSourceOptions? launcherOptions) + { + if (chatTemplate.DataSourceOptions is not { } templateOptions) + return (launcherOptions, false); + + return (templateOptions.CreateCopy(), launcherOptions is not null); + } + + /// + /// Names the preselected data sources which exist on this machine only. + /// + /// + /// Such a source is a sensible choice inside a chat and a dead end in an export: its ID travels + /// into the plugin unchanged, and on the machine which reads that plugin it points at nothing. + /// Only ERI sources describe something the whole organization can reach, which is why they are + /// also the only ones the app offers an export for.

+ /// IDs which match no configured source at all are left out. Those are covered by the note the + /// export writes above the data source IDs anyway, and the name to warn about is missing. + ///
+ /// The chat template about to be exported. + /// The data sources configured on this machine. + /// The names of the preselected local data sources, in the order they are configured in. + public static IReadOnlyList GetPreselectedLocalDataSourceNames(ChatTemplate chatTemplate, IEnumerable configuredDataSources) + { + if (chatTemplate.DataSourceOptions is not { PreselectedDataSourceIds.Count: > 0 } options) + return []; + + var preselectedIds = options.PreselectedDataSourceIds.ToHashSet(StringComparer.OrdinalIgnoreCase); + return configuredDataSources + .Where(source => source is IInternalDataSource && preselectedIds.Contains(source.Id)) + .Select(source => source.Name) + .ToList(); + } + public static bool TryParseChatTemplateTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject template) { template = NO_CHAT_TEMPLATE; @@ -121,6 +212,8 @@ public record ChatTemplate( ExampleConversation = ParseExampleConversation(idx, table), FileAttachments = fileAttachments, AllowProfileUsage = allowProfileUsage, + ToolIds = ParseToolIds(idx, table), + DataSourceOptions = ParseDataSourceOptions(idx, table), IsEnterpriseConfiguration = true, EnterpriseConfigurationPluginId = configPluginId, }; @@ -175,6 +268,89 @@ public record ChatTemplate( return exampleConversation; } + /// + /// A missing list and an empty one mean different things here, so an empty one must not fall + /// back to null: the template then states that it wants no tools. The assistant plugins reject + /// an empty list instead, because there it carries no meaning at all. + /// + private static HashSet? ParseToolIds(int idx, LuaTable table) + { + if (!table.TryGetValue("ToolIds", out var toolIdsValue) || !toolIdsValue.TryRead(out var toolIdsTable)) + return null; + + var toolIds = new HashSet(StringComparer.Ordinal); + var numToolIds = toolIdsTable.ArrayLength; + for (var toolNum = 1; toolNum <= numToolIds; toolNum++) + { + if (!toolIdsTable[toolNum].TryRead(out var toolId) || string.IsNullOrWhiteSpace(toolId)) + { + LOGGER.LogWarning("The ToolIds entry {ToolNum} in chat template {IdxChatTemplate} is not a valid tool ID and will be ignored.", toolNum, idx); + continue; + } + + toolIds.Add(toolId.Trim()); + } + + return toolIds; + } + + private static DataSourceOptions? ParseDataSourceOptions(int idx, LuaTable table) + { + if (!table.TryGetValue("DataSourceOptions", out var optionsValue) || !optionsValue.TryRead(out var optionsTable)) + return null; + + // + // Writing this table at all is already the statement that the template wants data sources, + // hence the switch starts enabled here. Everywhere else in the app, data sources start + // switched off. + // + var disableDataSources = false; + if (optionsTable.TryGetValue("DisableDataSources", out var disableValue) && disableValue.TryRead(out var disable)) + disableDataSources = disable; + + var automaticSelection = false; + if (optionsTable.TryGetValue("AutomaticDataSourceSelection", out var automaticSelectionValue) && automaticSelectionValue.TryRead(out var automaticSelectionFlag)) + automaticSelection = automaticSelectionFlag; + + var automaticValidation = false; + if (optionsTable.TryGetValue("AutomaticValidation", out var automaticValidationValue) && automaticValidationValue.TryRead(out var automaticValidationFlag)) + automaticValidation = automaticValidationFlag; + + return new DataSourceOptions + { + DisableDataSources = disableDataSources, + AutomaticDataSourceSelection = automaticSelection, + AutomaticValidation = automaticValidation, + PreselectedDataSourceIds = ParsePreselectedDataSourceIds(idx, optionsTable), + }; + } + + /// + /// The IDs stay strings instead of being parsed as GUIDs: a data source of another + /// configuration may carry an ID which is none, and rejecting it here would make it + /// unreferenceable for no gain. + /// + private static List ParsePreselectedDataSourceIds(int idx, LuaTable optionsTable) + { + var dataSourceIds = new List(); + if (!optionsTable.TryGetValue("PreselectedDataSourceIds", out var idsValue) || !idsValue.TryRead(out var idsTable)) + return dataSourceIds; + + var numIds = idsTable.ArrayLength; + for (var idNum = 1; idNum <= numIds; idNum++) + { + if (!idsTable[idNum].TryRead(out var dataSourceId) || string.IsNullOrWhiteSpace(dataSourceId)) + { + LOGGER.LogWarning("The PreselectedDataSourceIds entry {IdNum} in chat template {IdxChatTemplate} is not a valid data source ID and will be ignored.", idNum, idx); + continue; + } + + dataSourceIds.Add(dataSourceId.Trim()); + } + + return dataSourceIds; + } + private static List ParseFileAttachments(int idx, LuaTable table, string pluginPath) { var fileAttachments = new List(); @@ -258,15 +434,24 @@ public record ChatTemplate( { issue = string.Empty; var fileAttachmentsLua = this.BuildFileAttachmentsLua(fileAttachmentPaths); + + // + // Both of these may be absent entirely, because saying nothing about tools or data sources + // is a statement of its own. They therefore bring their own line break and indentation + // instead of sitting on a line of the template: + // + var toolIdsLua = this.BuildToolIdsLua(); + var dataSourceOptionsLua = this.BuildDataSourceOptionsLua(); + luaCode = $$""" - CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { + {{this.BuildDataSourceIdNote()}}CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { ["Id"] = "{{LuaTools.EscapeLuaString(exportId)}}", ["Name"] = {{LuaTools.ToLuaStringLiteral(this.Name)}}, ["SystemPrompt"] = {{LuaTools.ToLuaStringLiteral(this.SystemPrompt)}}, ["PredefinedUserPrompt"] = {{LuaTools.ToLuaStringLiteral(this.PredefinedUserPrompt)}}, ["AllowProfileUsage"] = {{this.AllowProfileUsage.ToString().ToLowerInvariant()}}, ["FileAttachments"] = {{fileAttachmentsLua}}, - ["ExampleConversation"] = {{exampleConversationLua}}, + ["ExampleConversation"] = {{exampleConversationLua}},{{toolIdsLua}}{{dataSourceOptionsLua}} } """; return true; @@ -376,6 +561,84 @@ public record ChatTemplate( return true; } + /// + /// An empty set is written out as an empty table rather than being left out: the two say + /// different things, and dropping the line would turn "no tools at all" into "whatever the + /// chat default is" on the machine which reads this back. + /// + private string BuildToolIdsLua() + { + if (this.ToolIds is null) + return string.Empty; + + var builder = new StringBuilder(); + builder.AppendLine(); + if (this.ToolIds.Count == 0) + { + builder.Append(""" ["ToolIds"] = {},"""); + return builder.ToString(); + } + + builder.AppendLine(""" ["ToolIds"] = {"""); + + // + // A set has no order of its own, so exporting the same template twice would otherwise + // produce two different files. Sorting keeps the plugin diffs readable: + // + foreach (var toolId in this.ToolIds.Order(StringComparer.Ordinal)) + builder.AppendLine($" {LuaTools.ToLuaStringLiteral(toolId)},"); + + builder.Append(" },"); + return builder.ToString(); + } + + private string BuildDataSourceOptionsLua() + { + if (this.DataSourceOptions is not { } options) + return string.Empty; + + var builder = new StringBuilder(); + builder.AppendLine(); + builder.AppendLine(""" ["DataSourceOptions"] = {"""); + builder.AppendLine($""" ["DisableDataSources"] = {options.DisableDataSources.ToString().ToLowerInvariant()},"""); + builder.AppendLine($""" ["AutomaticDataSourceSelection"] = {options.AutomaticDataSourceSelection.ToString().ToLowerInvariant()},"""); + builder.AppendLine($""" ["AutomaticValidation"] = {options.AutomaticValidation.ToString().ToLowerInvariant()},"""); + + if (options.PreselectedDataSourceIds.Count == 0) + builder.AppendLine(""" ["PreselectedDataSourceIds"] = {},"""); + else + { + builder.AppendLine(""" ["PreselectedDataSourceIds"] = {"""); + foreach (var dataSourceId in options.PreselectedDataSourceIds) + builder.AppendLine($" {LuaTools.ToLuaStringLiteral(dataSourceId)},"); + + builder.AppendLine(" },"); + } + + builder.Append(" },"); + return builder.ToString(); + } + + /// + /// The template itself gets a fresh ID on export, but the data source IDs must not: they point + /// at the sources of the organization and only work when both sides agree on them. Nobody can + /// see that from the exported code alone, hence this note. + /// + private string BuildDataSourceIdNote() + { + if (this.DataSourceOptions is not { PreselectedDataSourceIds.Count: > 0 }) + return string.Empty; + + // The empty line before the closing delimiter is what ends the last comment line. Without + // it, the assignment would continue that comment and the whole export would be one comment: + return """ + -- The data source IDs below are the ones of the machine this was exported from. + -- Please check them against your CONFIG["DATA_SOURCES"]: an ID which resolves to + -- nothing is ignored, and a chat with this template then starts without that source. + + """; + } + private string BuildFileAttachmentsLua(IReadOnlyList? fileAttachmentPaths) { var paths = fileAttachmentPaths ?? this.FileAttachments.Select(attachment => attachment.FilePath).ToList(); diff --git a/app/MindWork AI Studio/Tools/Services/DirectChatService.cs b/app/MindWork AI Studio/Tools/Services/DirectChatService.cs index b89f3cea..0bbd5ffe 100644 --- a/app/MindWork AI Studio/Tools/Services/DirectChatService.cs +++ b/app/MindWork AI Studio/Tools/Services/DirectChatService.cs @@ -45,7 +45,7 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc profile = Profile.NO_PROFILE; } - var dataSourceOptionsResult = await this.ResolveDataSourceOptionsAsync(providerResult.Provider, launchConfiguration.DataSourceIds); + var dataSourceOptionsResult = await this.ResolveDataSourceOptionsAsync(assistantPlugin, providerResult.Provider, chatTemplate, launchConfiguration.DataSourceIds); var dataSourceOptions = dataSourceOptionsResult.Options; if (dataSourceOptions is null) return new(null, dataSourceOptionsResult.ErrorMessage); @@ -75,15 +75,21 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc } } + var toolChoice = ChatTemplate.ChooseToolIds(chatTemplate, launchConfiguration.ToolIds); + if (toolChoice.LauncherChoiceDropped) + logger.LogWarning( + "Assistant plugin '{PluginName}' selects the tools '{LauncherToolIds}', but its chat template '{ChatTemplateName}' names tools of its own. The chat starts with the tools of that template.", + assistantPlugin.Name, string.Join(", ", launchConfiguration.ToolIds!), chatTemplate.GetSafeName()); + // - // Only the tools the user could have switched on themselves. A launcher may name one whose + // Only the tools the user could have switched on themselves. Either side may name one whose // settings are incomplete — an unconfigured web search, say — and starting the chat with it // enabled would show a state the user cannot produce by hand and cannot fix from the chat. // Null keeps the chat's own defaults, which is what a launcher without tools wants. // - var selectedToolIds = launchConfiguration.ToolIds is null + var selectedToolIds = toolChoice.ToolIds is null ? null - : await toolRegistry.FilterSelectableToolIdsAsync(Components.CHAT, launchConfiguration.ToolIds); + : await toolRegistry.FilterSelectableToolIdsAsync(Components.CHAT, toolChoice.ToolIds); var chatThread = new ChatThread { @@ -101,7 +107,12 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc Blocks = chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : chatTemplate.ExampleConversation.Select(block => block.DeepClone()).ToList(), }; - return new(new(chatThread, ApplySelectedChatTemplateToComposer: true, PreserveDataSourceOptions: launchConfiguration.DataSourceIds is not null), string.Empty); + // + // Whoever decided these options — the chat template or the launcher — decided them for this + // chat. Without saying so, the chat page would replace them with the chat defaults again: + // + var dataSourcesWereChosen = chatTemplate.DataSourceOptions is not null || launchConfiguration.DataSourceIds is not null; + return new(new(chatThread, ApplySelectedChatTemplateToComposer: true, PreserveDataSourceOptions: dataSourcesWereChosen), string.Empty); } private (ProviderSettings Provider, bool IsExplicit, string ErrorMessage) ResolveProvider(Guid? providerId) @@ -167,64 +178,119 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc : new(chatTemplate, string.Empty); } - private async Task<(DataSourceOptions? Options, string ErrorMessage)> ResolveDataSourceOptionsAsync(ProviderSettings provider, IReadOnlyList? dataSourceIds) + private async Task<(DataSourceOptions? Options, string ErrorMessage)> ResolveDataSourceOptionsAsync(PluginAssistants assistantPlugin, ProviderSettings provider, ChatTemplate chatTemplate, IReadOnlyList? launcherDataSourceIds) { - if (dataSourceIds is null) + // + // The launcher names data sources as plain IDs, and the options around them are always the + // same ones. Building them here turns its choice into the same kind of thing the chat + // template carries, which is what lets one rule decide between the two. + // + DataSourceOptions? launcherOptions = null; + if (launcherDataSourceIds is not null) + { + var standardOptions = settingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions; + launcherOptions = new DataSourceOptions + { + DisableDataSources = false, + AutomaticDataSourceSelection = false, + AutomaticValidation = standardOptions.AutomaticValidation, + PreselectedDataSourceIds = launcherDataSourceIds.Select(dataSourceId => dataSourceId.ToString()).ToList(), + }; + } + + var optionsChoice = ChatTemplate.ChooseDataSourceOptions(chatTemplate, launcherOptions); + if (optionsChoice.LauncherChoiceDropped) + logger.LogWarning( + "Assistant plugin '{PluginName}' selects the data sources '{LauncherDataSourceIds}', but its chat template '{ChatTemplateName}' brings data source options of its own. The chat starts with the data sources of that template.", + assistantPlugin.Name, string.Join(", ", launcherDataSourceIds!), chatTemplate.GetSafeName()); + + // Neither side says anything, so the chat starts the way it would start on its own: + if (optionsChoice.Options is not { } chosenOptions) return new(settingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy(), string.Empty); + return await this.CheckChosenDataSourcesAsync(provider, chosenOptions, chatTemplate.DataSourceOptions is null ? null : chatTemplate); + } + + /// + /// Checks that the chosen data sources exist and may be used with the provider of the chat. + /// + /// + /// Opening a launcher is one click, so a source which is gone or not permitted has to be said + /// out loud instead of being dropped quietly: nobody would see what the chat is missing. Which + /// of the two sides chose the sources changes nothing but the wording — and that wording is the + /// only place where the user learns which of them to go and fix. + /// + /// The provider the launched chat runs with. + /// The options the chat is about to start with. + /// The chat template the options came from, or null when the launcher named the sources itself. + /// The checked options, or null and a message saying why no chat was created. + private async Task<(DataSourceOptions? Options, string ErrorMessage)> CheckChosenDataSourcesAsync(ProviderSettings provider, DataSourceOptions chosenOptions, ChatTemplate? originChatTemplate) + { + // + // There is nothing to check when data sources are switched off, and nothing to check either + // when an agent picks them: that choice is made per message in the chat, exactly as it is + // for a chat template the user picks by hand. + // + if (chosenOptions.DisableDataSources || chosenOptions.AutomaticDataSourceSelection || chosenOptions.PreselectedDataSourceIds.Count == 0) + return new(chosenOptions, string.Empty); + // // Deciding which data sources are permitted needs an effective provider. Without one, // the check below would report every requested source as unavailable, which would hide // the actual cause from the user: // if (provider == ProviderSettings.NONE) - return new(null, TB("The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created.")); + return new(null, originChatTemplate is null + ? TB("The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created.") + : string.Format(TB("The chat template '{0}' selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created."), originChatTemplate.GetSafeName())); - var requestedDataSources = new List(dataSourceIds.Count); - foreach (var dataSourceId in dataSourceIds) + var requestedDataSources = new List(chosenOptions.PreselectedDataSourceIds.Count); + foreach (var dataSourceId in chosenOptions.PreselectedDataSourceIds) { // Data sources have no lookup helper in the settings manager, so we match their ids // the same way the rest of the app does: - var dataSourceIdText = dataSourceId.ToString(); var dataSource = settingsManager.ConfigurationData.DataSources.FirstOrDefault(candidate => - string.Equals(candidate.Id, dataSourceIdText, StringComparison.OrdinalIgnoreCase)); + string.Equals(candidate.Id, dataSourceId, StringComparison.OrdinalIgnoreCase)); if (dataSource is null) - return new(null, string.Format(TB("The assistant chat launcher references data source '{0}', but that data source does not exist."), dataSourceId)); + return new(null, originChatTemplate is null + ? string.Format(TB("The assistant chat launcher references data source '{0}', but that data source does not exist."), dataSourceId) + : string.Format(TB("The chat template '{0}' references data source '{1}', but that data source does not exist."), originChatTemplate.GetSafeName(), dataSourceId)); requestedDataSources.Add(dataSource); } // - // The options the launched chat will run under. We build them here already, because the - // data-source check depends on them: they decide which agent providers take part, and an - // agent with too little confidence makes a data source unavailable. + // The IDs are written back from the sources they resolved to: one of them may be spelled in + // another case than the source itself, and the chat matches its preselection literally. // - var standardOptions = settingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions; - var launchedDataSourceOptions = new DataSourceOptions - { - DisableDataSources = false, - AutomaticDataSourceSelection = false, - AutomaticValidation = standardOptions.AutomaticValidation, - PreselectedDataSourceIds = requestedDataSources.Select(source => source.Id).ToList(), - }; + chosenOptions.PreselectedDataSourceIds = requestedDataSources.Select(source => source.Id).ToList(); IReadOnlyList availableDataSources; try { - availableDataSources = await dataSourceService.GetAllowedDataSources(provider, launchedDataSourceOptions, requestedDataSources); + // + // The options the launched chat will run under are what this check runs against: they + // decide which agent providers take part, and an agent with too little confidence makes + // a data source unavailable. + // + availableDataSources = await dataSourceService.GetAllowedDataSources(provider, chosenOptions, requestedDataSources); } catch (Exception exception) { - logger.LogError(exception, "The data sources configured by an assistant chat launcher could not be checked."); - return new(null, TB("The data sources selected by the assistant chat launcher could not be checked. No chat was created.")); + logger.LogError(exception, "The data sources an assistant chat launcher would start its chat with could not be checked."); + return new(null, originChatTemplate is null + ? TB("The data sources selected by the assistant chat launcher could not be checked. No chat was created.") + : string.Format(TB("The data sources selected by the chat template '{0}' could not be checked. No chat was created."), originChatTemplate.GetSafeName())); } var availableSelectedIds = availableDataSources.Select(source => source.Id).ToHashSet(StringComparer.OrdinalIgnoreCase); var unavailableDataSources = requestedDataSources.Where(source => !availableSelectedIds.Contains(source.Id)).Select(source => source.Name).ToList(); if (unavailableDataSources.Count > 0) - return new(null, string.Format(TB("The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}"), string.Join(", ", unavailableDataSources))); + return new(null, originChatTemplate is null + ? string.Format(TB("The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}"), string.Join(", ", unavailableDataSources)) + : string.Format(TB("The following data sources selected by the chat template '{0}' are currently unavailable or not permitted for the selected provider: {1}"), originChatTemplate.GetSafeName(), string.Join(", ", unavailableDataSources))); - return new(launchedDataSourceOptions, string.Empty); + return new(chosenOptions, string.Empty); } } \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md index e0d15144..9f01643f 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md @@ -7,6 +7,8 @@ - Added tools to the policies of the Document Analysis assistant. A policy states which tools an analysis may use, and the AI uses exactly those — nobody has to pick them per document. AI Studio warns you beforehand when the provider you selected is not trusted enough for a tool the policy names. IT departments can roll policies out together with their tools. - Added tools to assistant plugins and direct-chat launchers. Plugin authors name them in the new `ToolIds` field, either as the tools an assistant runs with or as the tools a launcher preselects for the chat it opens; the example assistant plugin shows both. Which tools an assistant asks for is part of what you get to see before you enable it: its security card names them, and the security audit takes them into account. - Added tools to the Assistant Builder. For a direct-chat launcher you pick them yourself, alongside the workspace, provider, and data sources. For an assistant, the AI chooses from the tools installed here and says so in the draft, so you see the decision before the assistant is written. +- Added tools and data sources to your chat templates. A template can now decide which tools a chat starts with and which of your documents it may search, so a template such as "Research in our intranet" is complete on its own instead of leaving you to set the same things up by hand every time you switch to it. +- Added a way for IT departments to roll out chat templates that bring their own tools and data sources. You do not have to write any of it by hand: set the template up in the app, then export it as ready-made Lua code for your configuration plugin. - Added organization-wide management for tools. Among other options, IT departments can switch tools off entirely, disable individual ones, or define the provider trust a tool requires. You do not have to write any of it by hand: set a tool up in the app, then export its configuration as ready-made Lua code for your plugin, with encrypted API keys if you want them. - Added tool calling to the abilities you can state yourself in the expert provider settings. When you use a model AI Studio does not recognize as tool-capable, you can now declare that it is, the same way you already could for image input or reasoning. It works in the other direction as well: a model AI Studio has never heard of is offered tools, because most models can use them by now. Should one turn out not to be able to, AI Studio says so in plain words and points you to the same setting to switch the ability off again, instead of only passing the provider's error on. - Added support for OpenAI's GPT-6 Astra. @@ -43,6 +45,7 @@ - Improved the question AI Studio asks before you delete an embedding provider. It now names the data sources depending on that provider, together with what they can still do without it. - Improved what the AI is told when it answers from your own documents (RAG): it now learns which page a passage came from, so it can name the page an answer rests on. - Improved what happens when you ask for web content to be cleaned up and no model is available for it. AI Studio loads the page and tells you it arrived uncleaned, instead of quietly handing you the raw page with its navigation and advertising still in it. +- Changed what a tile that opens a chat directly starts with: when it uses a chat template that brings its own tools or data sources, that template decides them. The Assistant Builder says so while you build such a tile. - Changed which model a security check of an assistant plugin may fall back on. When you have set none aside for these checks, AI Studio uses the model you were working with, but only when that model meets the trust your organization requires for a check. One ruled out for that purpose no longer gets to read a plugin's code. - Changed how provider trust and provider confidence work together. Marking a provider as trustworthy in a configuration no longer also satisfies a required confidence level: one says who runs the provider, the other how confidential it is. Organizations raise a provider's level in their own confidence scheme instead. This applies beyond local data sources, for example, when a model reads a page from your intranet. - Fixed the abilities AI Studio assumed for many models. We checked the families against their documentation: some models gained image input, reasoning, or tool calling, others lost an ability they never had. diff --git a/app/Tests/Settings/ChatTemplateConfigurationTests.cs b/app/Tests/Settings/ChatTemplateConfigurationTests.cs new file mode 100644 index 00000000..a4186a28 --- /dev/null +++ b/app/Tests/Settings/ChatTemplateConfigurationTests.cs @@ -0,0 +1,299 @@ +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +using Lua; +using Lua.Standard; + +namespace AIStudio.Tests.Settings; + +/// +/// Checks the tools and data sources a chat template carries, across the two surfaces which have +/// to agree on them: the Lua a configuration plugin states, and the Lua the app exports. +/// +/// +/// The interesting part is not that a value survives, but that the difference between "this +/// template says nothing" and "this template says none" survives. Both end up as an empty +/// selection in the chat on a fresh installation, so a mistake here stays invisible until somebody +/// sets a default tool for their chats -- and then quietly hands out a tool the template ruled out. +/// The last tests cover what the export says out loud before it runs, for the same reason: a data +/// source which cannot be rolled out goes unnoticed on the machine reading the plugin. +/// +[TestFixture] +public sealed class ChatTemplateConfigurationTests +{ + private static readonly Guid PLUGIN_ID = new("22222222-2222-2222-2222-222222222222"); + + [Test] + public async Task WhatTheAppExportsIsWhatAConfigurationPluginCanReadBack() + { + var written = NewTemplate() with + { + ToolIds = ["read_web_page", "web_search"], + DataSourceOptions = new() + { + DisableDataSources = false, + AutomaticDataSourceSelection = false, + AutomaticValidation = true, + PreselectedDataSourceIds = ["11111111-1111-1111-1111-111111111111"], + }, + }; + + var read = await ExportAndReadBackAsync(written); + + Assert.Multiple(() => + { + Assert.That(read.ToolIds, Is.EquivalentTo(written.ToolIds!)); + Assert.That(read.DataSourceOptions, Is.Not.Null); + Assert.That(read.DataSourceOptions!.DisableDataSources, Is.False); + Assert.That(read.DataSourceOptions.AutomaticDataSourceSelection, Is.False); + Assert.That(read.DataSourceOptions.AutomaticValidation, Is.True); + Assert.That(read.DataSourceOptions.PreselectedDataSourceIds, Is.EqualTo(written.DataSourceOptions!.PreselectedDataSourceIds)); + }); + } + + [Test] + public async Task AnAgenticSelectionSurvivesTheExportAsWell() + { + var written = NewTemplate() with + { + DataSourceOptions = new() + { + DisableDataSources = false, + AutomaticDataSourceSelection = true, + AutomaticValidation = true, + PreselectedDataSourceIds = [], + }, + }; + + var read = await ExportAndReadBackAsync(written); + + Assert.Multiple(() => + { + Assert.That(read.DataSourceOptions, Is.Not.Null); + Assert.That(read.DataSourceOptions!.AutomaticDataSourceSelection, Is.True, "Letting an agent pick the sources is the one thing only a chat template can state, so it must not be lost on the way through Lua."); + Assert.That(read.DataSourceOptions.PreselectedDataSourceIds, Is.Empty); + Assert.That(read.ToolIds, Is.Null); + }); + } + + [Test] + public async Task ATemplateWhichSaysNothingExportsNeitherTable() + { + var written = NewTemplate(); + Assert.That(written.TryExportAsConfigurationSection(out var luaCode, out var issue), Is.True, issue); + + Assert.Multiple(() => + { + Assert.That(luaCode, Does.Not.Contain("ToolIds")); + Assert.That(luaCode, Does.Not.Contain("DataSourceOptions")); + }); + + var read = await ParseAsync(luaCode); + + Assert.Multiple(() => + { + Assert.That(read.ToolIds, Is.Null, "A template without a tool selection must stay without one, so the chat keeps using its own default."); + Assert.That(read.DataSourceOptions, Is.Null); + }); + } + + [Test] + public async Task ATemplateWhichRulesOutEveryToolStaysThatWay() + { + var written = NewTemplate() with { ToolIds = [] }; + var read = await ExportAndReadBackAsync(written); + + Assert.That(read.ToolIds, Is.Not.Null, "An empty selection is the statement that this template wants no tools. Reading it back as null would hand out the chat default instead."); + Assert.That(read.ToolIds, Is.Empty); + } + + [Test] + public async Task NamingTheDataSourceOptionsAtAllSwitchesDataSourcesOn() + { + var read = await ParseAsync(""" + CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { + ["Id"] = "33333333-3333-3333-3333-333333333333", + ["Name"] = "Intranet Research", + ["SystemPrompt"] = "You are a research assistant.", + ["DataSourceOptions"] = { + ["PreselectedDataSourceIds"] = { + "11111111-1111-1111-1111-111111111111", + }, + }, + } + """); + + Assert.That(read.DataSourceOptions, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(read.DataSourceOptions!.DisableDataSources, Is.False, "Writing this table is already the statement that the template wants data sources, so an omitted switch must not turn them off again."); + Assert.That(read.DataSourceOptions.AutomaticDataSourceSelection, Is.False); + Assert.That(read.DataSourceOptions.AutomaticValidation, Is.False); + Assert.That(read.DataSourceOptions.PreselectedDataSourceIds, Is.EqualTo(new[] { "11111111-1111-1111-1111-111111111111" })); + }); + } + + [Test] + public async Task AnUnusableEntryIsSkippedAndTheRestOfTheListSurvives() + { + var read = await ParseAsync(""" + CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { + ["Id"] = "33333333-3333-3333-3333-333333333333", + ["Name"] = "Intranet Research", + ["SystemPrompt"] = "You are a research assistant.", + ["ToolIds"] = { + "web_search", + "", + {}, + "read_web_page", + }, + ["DataSourceOptions"] = { + ["PreselectedDataSourceIds"] = { + "11111111-1111-1111-1111-111111111111", + " ", + }, + }, + } + """); + + Assert.Multiple(() => + { + Assert.That(read.ToolIds, Is.EquivalentTo(new[] { "web_search", "read_web_page" })); + Assert.That(read.DataSourceOptions!.PreselectedDataSourceIds, Is.EqualTo(new[] { "11111111-1111-1111-1111-111111111111" })); + }); + } + + [Test] + public void ExportedDataSourceIdsComeWithTheNoteThatTheyAreLocalOnes() + { + var withSources = NewTemplate() with + { + DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["11111111-1111-1111-1111-111111111111"] }, + }; + + var agenticOnly = NewTemplate() with + { + DataSourceOptions = new() { DisableDataSources = false, AutomaticDataSourceSelection = true }, + }; + + Assert.That(withSources.TryExportAsConfigurationSection(out var withSourcesLua, out var issue), Is.True, issue); + Assert.That(agenticOnly.TryExportAsConfigurationSection(out var agenticOnlyLua, out issue), Is.True, issue); + + Assert.Multiple(() => + { + Assert.That(withSourcesLua, Does.StartWith("--"), "Whoever pastes this into a plugin cannot see from the IDs alone that they belong to another machine."); + Assert.That(agenticOnlyLua, Does.Not.StartWith("--"), "Without IDs there is nothing to check, so the note would only be noise."); + }); + } + + [Test] + public void LocalDataSourcesOfATemplateAreNamedBeforeItIsExported() + { + var template = NewTemplate() with + { + DataSourceOptions = new() + { + DisableDataSources = false, + PreselectedDataSourceIds = ["11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222"], + }, + }; + + var localNames = ChatTemplate.GetPreselectedLocalDataSourceNames(template, ConfiguredDataSources()); + + Assert.That(localNames, Is.EqualTo(new[] { "Meeting notes" }), "Only the local source can be named: its ID means nothing on the machine which reads the exported plugin, while the ERI source points at something the whole organization reaches."); + } + + [Test] + public void ATemplateWithoutLocalDataSourcesIsExportedWithoutAQuestion() + { + var eriOnly = NewTemplate() with + { + DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["22222222-2222-2222-2222-222222222222"] }, + }; + + var agentic = NewTemplate() with + { + DataSourceOptions = new() { DisableDataSources = false, AutomaticDataSourceSelection = true }, + }; + + Assert.Multiple(() => + { + Assert.That(ChatTemplate.GetPreselectedLocalDataSourceNames(eriOnly, ConfiguredDataSources()), Is.Empty); + Assert.That(ChatTemplate.GetPreselectedLocalDataSourceNames(agentic, ConfiguredDataSources()), Is.Empty, "An agent picks the sources per message, so this template names none to begin with."); + Assert.That(ChatTemplate.GetPreselectedLocalDataSourceNames(NewTemplate(), ConfiguredDataSources()), Is.Empty); + }); + } + + [Test] + public void AnIdWhichMatchesNoDataSourceIsNotReportedAsALocalOne() + { + var template = NewTemplate() with + { + DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["99999999-9999-9999-9999-999999999999"] }, + }; + + Assert.That(ChatTemplate.GetPreselectedLocalDataSourceNames(template, ConfiguredDataSources()), Is.Empty, "There is no name to warn about, and the note above the exported IDs already tells the admin to check them."); + } + + /// + /// A template with the parts every export needs, and nothing said about tools or data sources. + /// + private static ChatTemplate NewTemplate() => new() + { + Num = 1, + Id = "33333333-3333-3333-3333-333333333333", + Name = "Intranet Research", + SystemPrompt = "You are a research assistant.", + PredefinedUserPrompt = string.Empty, + ExampleConversation = [], + FileAttachments = [], + AllowProfileUsage = true, + }; + + /// + /// One data source of each kind: a local one, which cannot be rolled out, and an ERI one, which can. + /// + private static IReadOnlyList ConfiguredDataSources() => + [ + new DataSourceLocalFile { Id = "11111111-1111-1111-1111-111111111111", Name = "Meeting notes", Type = DataSourceType.LOCAL_FILE }, + new DataSourceERI_V1 { Id = "22222222-2222-2222-2222-222222222222", Name = "Intranet", Type = DataSourceType.ERI_V1 }, + ]; + + private static async Task ExportAndReadBackAsync(ChatTemplate template) + { + Assert.That(template.TryExportAsConfigurationSection(out var luaCode, out var issue), Is.True, issue); + return await ParseAsync(luaCode); + } + + /// + /// Reads a chat template the way a configuration plugin states it. + /// + /// + /// Through a real Lua state rather than a table put together in C#, so that the exported code + /// has to be valid Lua before anything else is checked. + /// + /// The lines a plugin would contain, including the assignment itself. + /// The chat template read from it. + private static async Task ParseAsync(string luaCode) + { + var state = LuaState.Create(); + state.OpenBasicLibrary(); + state.OpenTableLibrary(); + + await state.DoStringAsync($$""" + CONFIG = {} + CONFIG["CHAT_TEMPLATES"] = {} + {{luaCode}} + """); + + if (!state.Environment["CONFIG"].TryRead(out var configTable) || + !configTable["CHAT_TEMPLATES"].TryRead(out var templatesTable) || + !templatesTable[1].TryRead(out var templateTable)) + throw new InvalidOperationException("The code of this test did not produce a chat template table."); + + if (!ChatTemplate.TryParseChatTemplateTable(1, templateTable, PLUGIN_ID, string.Empty, out var parsed) || parsed is not ChatTemplate chatTemplate) + throw new InvalidOperationException("The chat template of this test could not be read."); + + return chatTemplate; + } +} \ No newline at end of file diff --git a/app/Tests/Settings/ChatTemplatePrecedenceTests.cs b/app/Tests/Settings/ChatTemplatePrecedenceTests.cs new file mode 100644 index 00000000..282b53c6 --- /dev/null +++ b/app/Tests/Settings/ChatTemplatePrecedenceTests.cs @@ -0,0 +1,165 @@ +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +namespace AIStudio.Tests.Settings; + +/// +/// Checks who decides the tools and data sources when a direct chat launcher and the chat template +/// it opens its chat with both name some. +/// +/// +/// Either side can be filled in without knowing about the other, so all four combinations happen. +/// The rule is deliberately the same for tools and for data sources: the chat template wins as a +/// whole, because it is the only one of the two which can also leave the choice of sources to an +/// agent, and a field-by-field mix of both would be something neither of them asked for. +/// +[TestFixture] +public sealed class ChatTemplatePrecedenceTests +{ + [Test] + public void WhenNeitherSideSaysAnythingTheChatDefaultsStay() + { + var toolChoice = ChatTemplate.ChooseToolIds(NewTemplate(), null); + var optionsChoice = ChatTemplate.ChooseDataSourceOptions(NewTemplate(), null); + + Assert.Multiple(() => + { + Assert.That(toolChoice.ToolIds, Is.Null, "Nobody named any tool, so the chat has to keep using the tools of its own default."); + Assert.That(toolChoice.LauncherChoiceDropped, Is.False); + Assert.That(optionsChoice.Options, Is.Null, "Nobody named any data source, so the chat has to keep using its own default options."); + Assert.That(optionsChoice.LauncherChoiceDropped, Is.False); + }); + } + + [Test] + public void ALauncherAloneDecidesForItself() + { + var template = NewTemplate(); + var launcherOptions = NewLauncherOptions("11111111-1111-1111-1111-111111111111"); + + var toolChoice = ChatTemplate.ChooseToolIds(template, new[] { "web_search" }); + var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, launcherOptions); + + Assert.Multiple(() => + { + Assert.That(toolChoice.ToolIds, Is.EquivalentTo(new[] { "web_search" })); + Assert.That(toolChoice.LauncherChoiceDropped, Is.False, "Nothing was dropped here, so nothing may be reported as dropped either."); + Assert.That(optionsChoice.Options, Is.SameAs(launcherOptions)); + Assert.That(optionsChoice.LauncherChoiceDropped, Is.False); + }); + } + + [Test] + public void ATemplateAloneDecidesForItself() + { + var template = NewTemplate() with + { + ToolIds = ["read_web_page"], + DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["22222222-2222-2222-2222-222222222222"] }, + }; + + var toolChoice = ChatTemplate.ChooseToolIds(template, null); + var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, null); + + Assert.Multiple(() => + { + Assert.That(toolChoice.ToolIds, Is.EquivalentTo(new[] { "read_web_page" })); + Assert.That(toolChoice.LauncherChoiceDropped, Is.False); + Assert.That(optionsChoice.Options!.PreselectedDataSourceIds, Is.EqualTo(new[] { "22222222-2222-2222-2222-222222222222" })); + Assert.That(optionsChoice.LauncherChoiceDropped, Is.False); + }); + } + + [Test] + public void WhenBothSidesSpeakTheTemplateWinsAndTheLossIsReported() + { + var template = NewTemplate() with + { + ToolIds = ["read_web_page"], + DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["22222222-2222-2222-2222-222222222222"] }, + }; + + var toolChoice = ChatTemplate.ChooseToolIds(template, new[] { "web_search" }); + var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, NewLauncherOptions("11111111-1111-1111-1111-111111111111")); + + Assert.Multiple(() => + { + Assert.That(toolChoice.ToolIds, Is.EquivalentTo(new[] { "read_web_page" })); + Assert.That(toolChoice.LauncherChoiceDropped, Is.True, "The tools of the launcher are gone, and only this flag can make the log say so."); + Assert.That(optionsChoice.Options!.PreselectedDataSourceIds, Is.EqualTo(new[] { "22222222-2222-2222-2222-222222222222" })); + Assert.That(optionsChoice.LauncherChoiceDropped, Is.True); + }); + } + + [Test] + public void ATemplateWhichWantsNoToolsWinsJustTheSame() + { + var template = NewTemplate() with { ToolIds = [] }; + + var toolChoice = ChatTemplate.ChooseToolIds(template, new[] { "web_search" }); + + Assert.Multiple(() => + { + Assert.That(toolChoice.ToolIds, Is.Empty, "An empty selection is the statement that this template wants no tools, which is as much of a statement as naming one."); + Assert.That(toolChoice.LauncherChoiceDropped, Is.True); + }); + } + + [Test] + public void TheAgenticSelectionOfATemplateSurvivesALauncherWithItsOwnSources() + { + var template = NewTemplate() with + { + DataSourceOptions = new() { DisableDataSources = false, AutomaticDataSourceSelection = true, PreselectedDataSourceIds = [] }, + }; + + var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, NewLauncherOptions("11111111-1111-1111-1111-111111111111")); + + Assert.Multiple(() => + { + Assert.That(optionsChoice.Options!.AutomaticDataSourceSelection, Is.True, "Letting an agent pick the sources is the one thing a launcher cannot express, so it is exactly what must not be overwritten by one."); + Assert.That(optionsChoice.Options.PreselectedDataSourceIds, Is.Empty); + Assert.That(optionsChoice.LauncherChoiceDropped, Is.True); + }); + } + + [Test] + public void TheChosenOptionsAreACopyRatherThanTheOnesOfTheTemplate() + { + var template = NewTemplate() with + { + DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["22222222-2222-2222-2222-222222222222"] }, + }; + + var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, null); + optionsChoice.Options!.PreselectedDataSourceIds.Clear(); + + Assert.That(template.DataSourceOptions!.PreselectedDataSourceIds, Is.Not.Empty, "The launched chat goes on to change these options, and the template is a setting of the user which must not change with it."); + } + + /// + /// A template which says nothing about tools or data sources. + /// + private static ChatTemplate NewTemplate() => new() + { + Num = 1, + Id = "33333333-3333-3333-3333-333333333333", + Name = "Intranet Research", + SystemPrompt = "You are a research assistant.", + }; + + /// + /// The options a launcher which names data sources ends up with. + /// + /// + /// A launcher has no switches of its own: it names sources, and the rest is always this. Which + /// is the whole reason the chat template wins whenever both of them speak. + /// + private static DataSourceOptions NewLauncherOptions(params string[] dataSourceIds) => new() + { + DisableDataSources = false, + AutomaticDataSourceSelection = false, + AutomaticValidation = false, + PreselectedDataSourceIds = [..dataSourceIds], + }; +} \ No newline at end of file diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md index abd2d0c5..186d59ed 100644 --- a/documentation/Enterprise IT.md +++ b/documentation/Enterprise IT.md @@ -424,6 +424,9 @@ Currently, you can configure the following things: - Any number of LLM providers (self-hosted or cloud providers with encrypted API keys) - Any number of transcription providers for voice-to-text functionality - Any number of embedding providers for RAG +- Any number of ERI data sources for RAG +- Any number of profiles and chat templates, including the tools and data sources a template brings along +- Any number of policies for the Document Analysis assistant - Enterprise hash approvals for assistant plugins - Tool settings, encrypted tool API keys, and minimum provider confidence requirements - The update behavior of AI Studio @@ -589,6 +592,33 @@ A test configuration carries the rights of an organization configuration without The data directory belongs to the user account, so whoever can write there can approve assistant plugins in the name of your organization until the next restart. Treat write access to the data directory as equivalent to deploying a configuration, and protect it accordingly on managed devices. +## Exporting configurations from the app + +You do not have to write your configuration plugin by hand. Set something up in AI Studio, export it, and paste the Lua fragment into your plugin. + +Enable **Show administration settings** in the app settings once. It reveals the **Enterprise Administration** section and an **Export configuration** button next to each of these: + +| What you can export | Where the button sits | Offered for | +|---|---|---| +| LLM providers | the provider list in the app settings | providers you created yourself | +| Embedding providers | the provider list in the app settings | as above, and only while the RAG preview is enabled | +| Transcription providers | the provider list in the app settings | as above, and only while the speech-to-text preview is enabled | +| Profiles | the profile dialog in the app settings | profiles you created yourself | +| Chat templates | the chat template dialog in the app settings | templates you created yourself | +| ERI data sources | the data source list in the app settings | ERI sources only, and not the ones using Kerberos | +| Document analysis policies | the Document Analysis assistant itself | the policy you have selected | +| Tools | **Tool Settings** in the app settings | every tool | + +Anything your organization already manages has no export button: it came from a plugin to begin with. Local files and local directories have none either — such a data source exists on one machine only, so there is nothing to hand to your colleagues. + +The button copies the fragment to your clipboard. Paste it into your [configuration plugin](../app/MindWork%20AI%20Studio/Plugins/configuration/plugin.lua), after the initialization of the table it extends, such as `CONFIG["LLM_PROVIDERS"] = {}`. One export writes to disk as well: a chat template whose attachments you package copies those files into a folder of your plugin and puts the Lua into your clipboard as usual. + +**An export mints a new ID** for the exported object, so exporting the same provider or template twice deploys two of them to your colleagues. Once something is in your plugin, keep its ID and edit the rest around it. Document analysis policies are the exception: they keep the ID they have. + +Some exports ask a question first: a provider with an API key offers to include it encrypted (see [Encrypted API Keys](#encrypted-api-keys)), an ERI data source does the same for its token or its credentials, a chat template with file attachments asks whether to keep their paths or copy them into your plugin, and a tool opens a dialog for the areas and the kind of management you want (see [Exporting tool configurations](#exporting-tool-configurations)). + +Handing a whole plugin to a colleague is a different thing: that is the **Share** function on the plugins page, which writes a `.mwplugin` archive and is governed by its own organization setting rather than by the administration settings. + ## Encrypted API Keys You can include encrypted API keys in your configuration plugins for cloud providers (like OpenAI, Anthropic) or secured on-premise models. This feature provides obfuscation to prevent casual exposure of API keys in configuration files. @@ -601,7 +631,7 @@ You can include encrypted API keys in your configuration plugins for cloud provi ### Setting Up Encrypted API Keys 1. **Generate an encryption secret:** - In AI Studio, enable the "Show administration settings" toggle in the app settings. Then click the "Generate encryption secret and copy to clipboard" button in the "Enterprise Administration" section. This generates a cryptographically secure 256-bit key and copies it to your clipboard as a base64 string. + In AI Studio, click the "Generate encryption secret and copy to clipboard" button in the "Enterprise Administration" section of the app settings, which [Show administration settings](#exporting-configurations-from-the-app) reveals. This generates a cryptographically secure 256-bit key and copies it to your clipboard as a base64 string. 2. **Deploy the encryption secret:** Distribute the secret to all client machines using any supported enterprise source. The secret can be deployed on its own, even when no enterprise configuration IDs or server URLs are defined on that machine: @@ -612,11 +642,7 @@ You can include encrypted API keys in your configuration plugins for cloud provi You must also deploy the same secret on the machine where you will export the encrypted API keys (step 3). 3. **Export encrypted API keys from AI Studio:** - Once the encryption secret is deployed on your machine: - - Configure a provider with an API key in AI Studio's settings - - Click the export button for that provider - - If an API key is configured, you will be asked if you want to include the encrypted API key in the export - - The exported Lua code will contain the encrypted API key in the format `ENC:v1:` + Once the encryption secret is deployed on your machine, configure the provider with its API key and [export it](#exporting-configurations-from-the-app). AI Studio asks whether to include the key; the exported Lua code then contains it in the format `ENC:v1:`. 4. **Add encrypted keys to your configuration:** Copy the exported configuration (including the encrypted API key) into your configuration plugin. @@ -643,9 +669,9 @@ The API key will be automatically decrypted when the configuration is loaded and ## Exporting tool configurations -Start from the [example configuration plugin](../app/MindWork%20AI%20Studio/Plugins/configuration/plugin.lua). The export produces a Lua fragment to insert into that file; it assumes `CONFIG` and `CONFIG["SETTINGS"]` already exist. +A tool export is the one that asks the most before it writes anything. It assumes `CONFIG` and `CONFIG["SETTINGS"]` already exist in your plugin. -1. Enable **Show administration settings** in the app settings. In **Tool Settings**, configure the tool and save your changes, then click its **Export configuration** button next to the settings button. +1. In **Tool Settings**, configure the tool and save your changes, then [export it](#exporting-configurations-from-the-app). 2. Select the areas to export. All areas start selected. For Web Search, SearXNG, Staan, Tavily, and General are independent: selecting only Tavily does not include the search language, strategy, or preferred backend. Select General separately when you need those settings. 3. Choose **Locked settings** or **Editable defaults**. Locked settings go into `DataTools.LockedToolSettings` and cannot be changed by users. Editable defaults go into `DataTools.DefaultToolSettings`; a user's saved value takes precedence over them. 4. Optionally select **Include encrypted API keys and other secrets**, which starts off. The option is available only when the selected areas contain configured secrets and this machine has a valid enterprise encryption secret. Deploy the same secret to recipients as described in [Setting Up Encrypted API Keys](#setting-up-encrypted-api-keys). Secrets always go into `LockedToolSettings`, including when you choose editable defaults for the other fields. Managed tool secrets are used from the configuration without replacing the user's own keyring entries; removing the managed secret makes the user's own key available again. @@ -685,6 +711,23 @@ This does not change the SearXNG or Staan settings, the general Web Search setti You can combine both fragments in the same plugin: their table initializations preserve earlier entries, and only a later assignment to an identical key replaces its value. A later whole-table assignment such as `CONFIG["SETTINGS"]["DataTools.LockedToolSettings"] = { ... }` replaces those entries, so place exports after it or merge them manually. This behavior applies within one plugin; across separate configuration plugins, the winning plugin replaces the whole managed table as described in [Settings that hold a list or a table](#settings-that-hold-a-list-or-a-table). +## Chat templates with tools and data sources + +On top of its system prompt and the rest, a chat template decides the tools and the data sources a chat started with it begins with. The data source IDs in an exported template are **carried over unchanged**, unlike the template's own ID: they point at the data sources of your organization. Check them against your `CONFIG["DATA_SOURCES"]` -- an ID that resolves to nothing is ignored, and a chat with that template then starts without that source. + +Writing such a template by hand means knowing that saying nothing and saying none are two different statements: + +| What the template says | What a chat started with it does | +|---|---| +| no `ToolIds` at all | starts with the tools the user has set as their chat default | +| `ToolIds` present but empty | starts with no tool at all, whatever that default says | +| no `DataSourceOptions` at all | starts with the data source options the user has set as their chat default | +| `DataSourceOptions` present | starts with exactly those, including the choice to let an agent pick the sources | + +Writing the `DataSourceOptions` table at all is already the statement that this template wants data sources, so `DisableDataSources` starts at `false` inside it, unlike everywhere else in the app. + +When an [assistant plugin](../app/MindWork%20AI%20Studio/Plugins/assistants/README.md) opens a chat directly and its chat template names tools or data sources, that template decides them alone; what the launcher names is dropped with a warning in the log. Its README explains the rule and how such sources are checked. + ## Letting users provide their own API key Sometimes you want to hand out a preconfigured provider -- a fixed host, model, and instance name