diff --git a/.gitignore b/.gitignore index 543ff06f8..f5e67cafd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ startup.env libpdfium.dylib libpdfium.so libpdfium.dll +pdfium.dll # Ignore qdrant database: qdrant-aarch64-apple-darwin diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 9f5aa8399..c858ad949 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -4399,9 +4399,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1797236585"] = " -- Workspace name (Optional) UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1873204484"] = "Workspace name (Optional)" +-- The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1876510820"] = "The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats." + -- Use no profile UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2205839602"] = "Use no profile" +-- The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2344103917"] = "The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template." + -- Existing workspace (Optional) UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = "Existing workspace (Optional)" @@ -4423,6 +4429,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2901190527"] = " -- Data sources (Optional) UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3259309302"] = "Data sources (Optional)" +-- The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3419242912"] = "The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists." + -- Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = "Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings." @@ -6187,12 +6196,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "The cha -- 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." +-- Preselected provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1469984996"] = "Preselected provider" + -- Please enter a name for the chat template. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Please enter a name for the chat template." +-- A chat template may decide which provider, and with it which model, a chat starts with. Without such a decision, those chats start with the provider they would start with anyway. The provider stays changeable in the chat, and it must meet the confidence level required for chats. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T171066641"] = "A chat template may decide which provider, and with it which model, a chat starts with. Without such a decision, those chats start with the provider they would start with anyway. The provider stays changeable in the chat, and it must meet the confidence level required for chats." + -- 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" +-- Preselected profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T178965494"] = "Preselected profile" + -- Enter the full path of a file on this computer, or remove the attachment. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1798848159"] = "Enter the full path of a file on this computer, or remove the attachment." @@ -6202,12 +6220,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Load pr -- Update UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1847791252"] = "Update" +-- Yes, this template decides which profile a chat starts with +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1865005833"] = "Yes, this template decides which profile a chat starts with" + -- The chat template name must not exceed 40 characters. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T204496403"] = "The chat template name must not exceed 40 characters." -- Profile Usage UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2147062613"] = "Profile Usage" +-- Use no profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2205839602"] = "Use no profile" + +-- Yes, chats with this template go into a workspace +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2205963533"] = "Yes, chats with this template go into a workspace" + -- Relink attachment: {0} UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2234793579"] = "Relink attachment: {0}" @@ -6217,6 +6244,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2292424657"] = "Add mes -- File Attachments UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "File Attachments" +-- No, chats go where they would go anyway +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2355334446"] = "No, chats go where they would go anyway" + +-- Existing workspace (Optional) +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2364306588"] = "Existing workspace (Optional)" + -- Role UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Role" @@ -6229,6 +6262,12 @@ 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?" +-- Please enter the workspace for the chats of this template, or switch the preselection off. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2504276031"] = "Please enter the workspace for the chats of this template, or switch the preselection off." + +-- Does this chat template preselect a provider? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2541393632"] = "Does this chat template preselect a provider?" + -- Tell the AI your system prompt. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2552381261"] = "Tell the AI your system prompt." @@ -6238,6 +6277,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579080722"] = "No, pro -- You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579208700"] = "You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with." +-- Does this chat template preselect a profile? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2602340928"] = "Does this chat template preselect a profile?" + -- Predefined User Input UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2607897066"] = "Predefined User Input" @@ -6253,9 +6295,15 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2719480499"] = "The mes -- Chat Template Name UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T275026390"] = "Chat Template Name" +-- Workspaces are switched off in your workspace settings. While they are off, chats with this template belong to no workspace, and they do not move into a workspace when you switch workspaces on again. You can change this choice once workspaces are on. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2828180414"] = "Workspaces are switched off in your workspace settings. While they are off, chats with this template belong to no workspace, and they do not move into a workspace when you switch workspaces on again. You can change this choice once workspaces are on." + -- Please relink the attachment '{0}' to a file on this computer or remove it before you save. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2884684079"] = "Please relink the attachment '{0}' to a file on this computer or remove it before you save." +-- Workspace name +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T295876489"] = "Workspace name" + -- Commit Changes UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2996302517"] = "Commit Changes" @@ -6271,21 +6319,42 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3108503534"] = "You can -- Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats." +-- A chat template may also decide which profile a chat starts with, including starting without one. Without such a decision, those chats start with the profile they would start with anyway. The profile stays changeable in the chat. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T313529011"] = "A chat template may also decide which profile a chat starts with, including starting without one. Without such a decision, those chats start with the profile they would start with anyway. The profile stays changeable in the chat." + +-- The profile this chat template preselects does not exist on this computer. Chats started with it keep their usual profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3170335831"] = "The profile this chat template preselects does not exist on this computer. Chats started with it keep their usual profile." + -- 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." +-- Choose an existing workspace or enter a name that should be created when a chat with this template starts. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3256439227"] = "Choose an existing workspace or enter a name that should be created when a chat with this template starts." + -- 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." +-- A chat template may keep its chats in a workspace of its own, which is created when it does not exist yet. This applies whenever a chat would otherwise be a disappearing chat, for example, when you start a new chat outside the workspaces or pick this template in a new chat. A chat you start in a workspace on purpose, for example, from the workspace list, stays in that workspace. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3352941260"] = "A chat template may keep its chats in a workspace of its own, which is created when it does not exist yet. This applies whenever a chat would otherwise be a disappearing chat, for example, when you start a new chat outside the workspaces or pick this template in a new chat. A chat you start in a workspace on purpose, for example, from the workspace list, stays in that workspace." + -- Add a message UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message" +-- Yes, this template decides which provider a chat starts with +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3422337353"] = "Yes, this template decides which provider a chat starts with" + -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Close" +-- No, chats keep their usual profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3465925873"] = "No, chats keep their usual profile" + +-- The provider this chat template preselects does not exist on this computer or does not meet the confidence level required for chats. Chats started with it keep their usual provider. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T349281189"] = "The provider this chat template preselects does not exist on this computer or does not meet the confidence level required for chats. Chats started with it keep their usual provider." + -- Unsupported content type UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Unsupported content type" @@ -6322,12 +6391,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4051106111"] = "Use the -- Tell the AI your predefined user input. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4052406705"] = "Tell the AI your predefined user input." +-- Does this chat template keep its chats in a workspace? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4057825866"] = "Does this chat template keep its chats in a workspace?" + -- Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4194591607"] = "Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template." -- Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements." +-- Please choose the provider this chat template preselects, or switch the preselection off. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T442309864"] = "Please choose the provider this chat template preselects, or switch the preselection off." + -- Enter a message UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Enter a message" @@ -6340,15 +6415,24 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System P -- 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." +-- Workspace +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T658612054"] = "Workspace" + -- Remove this attachment UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T70791272"] = "Remove this attachment" -- 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." +-- No, chats keep their usual provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T787085105"] = "No, chats keep their usual provider" + -- 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?" +-- Provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900237532"] = "Provider" + -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel" @@ -12994,6 +13078,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECUR -- The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T186946778"] = "The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured." +-- The chat template preselects the provider '{0}', which does not meet the confidence level required for chats. Chats started with it use your usual provider instead. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T190892810"] = "The chat template preselects the provider '{0}', which does not meet the confidence level required for chats. Chats started with it use your usual provider instead." + -- Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T206419407"] = "Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself." @@ -13006,6 +13093,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T260 -- The chat template preselects tools which are not available on this computer: {0}. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2693686582"] = "The chat template preselects tools which are not available on this computer: {0}." +-- The chat template preselects a profile which does not exist on this computer: {0}. Chats started with it use your usual profile instead. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2735390326"] = "The chat template preselects a profile which does not exist on this computer: {0}. Chats started with it use your usual profile instead." + +-- The chat template preselects a provider which does not exist on this computer: {0}. Chats started with it use your usual provider instead. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3321789654"] = "The chat template preselects a provider which does not exist on this computer: {0}. Chats started with it use your usual provider instead." + -- The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3589070261"] = "The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider." @@ -13027,6 +13120,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T600 -- The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T639320638"] = "The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider." +-- The chat template preselects a profile but does not allow profiles. Chats started with it ignore that profile. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T769062029"] = "The chat template preselects a profile but does not allow profiles. Chats started with it ignore that profile." + -- Expected a quoted string at character {0}. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1235695745"] = "Expected a quoted string at character {0}." diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index 04834d572..209a50a92 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -901,6 +901,84 @@ public partial class ChatComponent : MSGComponentBase this.selectedToolIds = await this.ToolRegistry.FilterSelectableToolIdsAsync(Tools.Components.CHAT, templateToolIds); } + /// + /// Takes over the profile and provider which the chat template preselects, wherever it preselects one. + /// + /// + /// A preselection, not a limit — the user changes both in the chat as usual. A template which + /// says nothing, or names a provider which is gone or may not be used for chats, leaves the + /// current choice in place, so the chat starts as it would without the template. A template + /// whose profile is gone starts with the default profile of the chat settings instead. What the + /// previous template preselected is not carried over, see SettingsManager.ChooseChatTemplateProfile. + /// The caller tells the parent about a new provider. + /// + /// The chat template the current profile and provider were chosen with; the current one when the template does not change. + /// True when the provider changed. + private bool ApplyChatTemplateProfileAndProvider(ChatTemplate previousChatTemplate) + { + this.currentProfile = this.SettingsManager.ChooseChatTemplateProfile(this.currentChatTemplate, previousChatTemplate, this.currentProfile); + + var provider = this.SettingsManager.ChooseChatTemplateProvider(this.currentChatTemplate, previousChatTemplate, this.Provider); + if (provider == this.Provider) + return false; + + this.Provider = provider; + return true; + } + + /// + /// The workspace a chat which has no thread yet gets with its first content. + /// + /// + /// The first content is the first message, a media import, or a draft kept when the user leaves + /// the page. A chat without a thread belongs to no workspace, so the workspace of its chat + /// template fills in, opened or created by its name. That happens only here, when the chat gets + /// something to keep: merely opening the chat page or trying out a template creates no + /// workspace, and a workspace the user deleted stays gone until a chat with that template + /// actually starts. Without a usable workspace of the template, the chat stays without one. + /// + /// The ID of the workspace, or Guid.Empty for none. + private async Task ResolveWorkspaceOfFirstThreadAsync() + { + if (this.currentWorkspaceId != Guid.Empty) + return this.currentWorkspaceId; + + if (this.SettingsManager.GetChatTemplateWorkspaceName(this.currentChatTemplate) is not { } workspaceName) + return Guid.Empty; + + return await WorkspaceBehaviour.ResolveOrCreateChatTemplateWorkspaceAsync(workspaceName, this.currentChatTemplate.GetSafeName()); + } + + /// + /// Hands a thread which this component just created for a chat without one to the parent. + /// + /// + /// The thread is a parameter of this component, and the parent hands over its own whenever it + /// renders. So the parent hears about the new thread before anything else may render it, see + /// StartNewChat. Only afterward, the header shows the workspace of the thread, since showing a + /// workspace name renders the parent as well. + /// + private async Task PublishNewChatThreadAsync() + { + this.MarkCurrentChatAsLoadedParameter(); + await this.ChatThreadChanged.InvokeAsync(this.ChatThread); + + // The header shows the workspace of the new thread from now on, e.g., the one of its chat template: + if (this.ChatThread is not null && this.ChatThread.WorkspaceId != Guid.Empty) + await this.SyncWorkspaceHeaderWithChatThreadAsync(); + } + + /// + /// Whether the open chat is in the workspace which the given chat template names. + /// + /// + /// Such a workspace was the choice of the template rather than of the user, so a chat started + /// with another template does not have to stay there. + /// + private bool IsInWorkspaceOfChatTemplate(ChatTemplate chatTemplate) => this.currentWorkspaceId != Guid.Empty + && this.SettingsManager.GetChatTemplateWorkspaceName(chatTemplate) is { } workspaceName + && string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(this.currentWorkspaceName), workspaceName, StringComparison.OrdinalIgnoreCase); + /// /// The data source options a chat starts with: those of its chat template, or the chat defaults. /// @@ -1006,24 +1084,36 @@ public partial class ChatComponent : MSGComponentBase private async Task ChatTemplateWasChanged(ChatTemplate chatTemplate) { - this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(chatTemplate.Id); + var nextChatTemplate = this.SettingsManager.GetChatTemplateById(chatTemplate.Id); + if (this.ChatThread is not null) + { + // + // Starting the new chat is what hands the new template to it, with its text, attachments, + // and selections. Nothing changes here before, since the user may still decline to leave + // the open chat; it then stays as it was, with its template. The new chat stays in the + // workspace of the open one, unless that workspace is merely the one of the current + // template: then the new template decides where its chat goes. + // + await this.StartNewChat(useSameWorkspace: !this.IsInWorkspaceOfChatTemplate(this.currentChatTemplate), chatTemplate: nextChatTemplate); + return; + } + + var previousChatTemplate = this.currentChatTemplate; + this.currentChatTemplate = nextChatTemplate; if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt)) this.ComposerState.SetSystemInput(this.currentChatTemplate.PredefinedUserPrompt); // Apply template's file attachments (replaces existing): 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. + // applied right here. It travels into the thread which the first message creates, and that + // message also decides the workspace, see ResolveWorkspaceOfFirstThreadAsync. // + if (this.ApplyChatTemplateProfileAndProvider(previousChatTemplate)) + await this.ProviderChanged.InvokeAsync(this.Provider); + await this.ApplyChatTemplateToolSelectionAsync(); this.ApplyAutomaticDataSourceOptions(); } @@ -1047,6 +1137,19 @@ public partial class ChatComponent : MSGComponentBase { this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT); this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT); + + // + // A chat which has not started yet keeps up with its chat template. The provider follows + // only when the template now preselects another one, so that a provider the user picked + // by hand survives a change of some unrelated setting: + // + if (previousChatTemplate.Id != this.currentChatTemplate.Id || previousChatTemplate.ProviderId != this.currentChatTemplate.ProviderId) + { + if (this.ApplyChatTemplateProfileAndProvider(previousChatTemplate)) + await this.ProviderChanged.InvokeAsync(this.Provider); + } + else if (this.SettingsManager.GetChatTemplateStartProfile(this.currentChatTemplate) is { } templateProfile) + this.currentProfile = templateProfile; } else { @@ -1256,10 +1359,16 @@ public partial class ChatComponent : MSGComponentBase if (!this.ComposerState.HasVisibleUserDraft) return; + // + // The message moves out of the composer before anything below waits, so that leaving a + // second time meanwhile finds nothing left to keep. Only the workspace follows afterward, + // since finding or creating the one of the chat template may take a moment: + // this.RefreshCurrentProfileAndChatTemplate(); - chat = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput)); + chat = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput), Guid.Empty); this.MirrorComposerIntoDraft(chat); this.ComposerState.Clear(); + chat.WorkspaceId = await this.ResolveWorkspaceOfFirstThreadAsync(); } else if (unstoredChatBeingLeft is not null && (!unstoredChatBeingLeft.Draft.IsEmpty || this.MediaTranscriptionService.IsBusy(MediaImportOwner.ForChat(unstoredChatBeingLeft.ChatId)))) { @@ -1330,22 +1439,28 @@ public partial class ChatComponent : MSGComponentBase ? string.Format(T("Transcription: {0}"), Path.GetFileName(firstMediaPath)) : promptName; - this.ChatThread = this.NewChatThread(threadName); + var chat = this.NewChatThread(threadName, await this.ResolveWorkspaceOfFirstThreadAsync()); // A message the user started belongs to the new chat, so it is stored with it: if (this.ComposerState.HasUserDraft) - this.MirrorComposerIntoDraft(this.ChatThread); + this.MirrorComposerIntoDraft(chat); + + // + // The parent hears about the new thread before storing it waits, as with the first message. + // A render of the parent in between would otherwise hand back no thread at all, and the + // transcript would arrive in a chat which is nowhere: + // + this.ChatThread = chat; + await this.PublishNewChatThreadAsync(); // // Like saving, this goes through the workspace component whenever there is one, so its tree // lists the new chat right away, together with the progress of the transcription, instead // of only after the first message was sent: // - await this.StoreAndListChatAsync(this.ChatThread); - this.MarkCurrentChatAsLoadedParameter(); - await this.ChatThreadChanged.InvokeAsync(this.ChatThread); + await this.StoreAndListChatAsync(chat); await this.SyncForegroundChatAsync(); - return this.ChatThread; + return chat; } private async Task SendMessage(bool reuseLastUserPrompt = false) @@ -1377,9 +1492,8 @@ public partial class ChatComponent : MSGComponentBase // Create a new chat thread if necessary: if (this.ChatThread is null) { - this.ChatThread = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput)); - this.MarkCurrentChatAsLoadedParameter(); - await this.ChatThreadChanged.InvokeAsync(this.ChatThread); + this.ChatThread = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput), await this.ResolveWorkspaceOfFirstThreadAsync()); + await this.PublishNewChatThreadAsync(); } else { @@ -1576,12 +1690,17 @@ public partial class ChatComponent : MSGComponentBase /// Every way to start a new chat leads here: the buttons of the toolbar, a change of the chat /// template, and the workspace tree. So every new chat gets its chat template with the data /// sources, tools, and example conversation, its profile, and its provider in the same way, and - /// the user is asked about unsaved changes in the same way. + /// the user is asked about unsaved changes in the same way.

+ /// + /// A new chat template takes over only once the user agreed to leave the open chat and that + /// chat is stored or deleted. Declining leaves the open chat as it was, with its template, and + /// the template it runs with stays the one whose preselections the next chat must not carry over. /// - /// True starts the new chat with a thread right away, in the workspace of the open chat. False starts it outside any workspace, and its thread comes with the first message. + /// True starts the new chat with a thread right away, in the workspace of the open chat. False starts it outside any workspace, and its thread comes with the first message. A chat which would belong to no workspace either way, while its chat template names one, also gets its thread only with the first message, in the workspace of the template. /// Whether to delete the open chat first. /// The workspace for the new chat, when the workspace tree asks for one. When given, the new chat gets a thread in that workspace, whatever useSameWorkspace says. - private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false, Guid? targetWorkspaceId = null) + /// The chat template of the new chat, when the user picked another one. Without one, the new chat keeps the template of the open chat. + private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false, Guid? targetWorkspaceId = null, ChatTemplate? chatTemplate = null) { // // Where the new chat goes is decided right away, when it is asked for. Deleting the open chat @@ -1589,6 +1708,9 @@ public partial class ChatComponent : MSGComponentBase // var workspaceOfNewChat = targetWorkspaceId ?? this.currentWorkspaceId; + // The template the profile and the provider of the open chat were chosen with: + var previousChatTemplate = this.currentChatTemplate; + // // Want the user to manage the chat storage manually? In that case, we have to ask the user // about possible data loss: @@ -1650,8 +1772,13 @@ public partial class ChatComponent : MSGComponentBase // this.hasUnsavedChanges = false; this.ComposerState.Clear(); + + // The open chat is left now, so the new template takes over: + if (chatTemplate is not null) + this.currentChatTemplate = chatTemplate; + this.RefreshCurrentProfileAndChatTemplate(); - + // // Reset the LLM provider considering the user's settings: // @@ -1673,11 +1800,26 @@ public partial class ChatComponent : MSGComponentBase break; } + // What the chat template preselects comes last, so that it wins over the behavior above: + if (this.ApplyChatTemplateProfileAndProvider(previousChatTemplate)) + await this.ProviderChanged.InvokeAsync(this.Provider); + // // Reset the chat thread or create a new one. A target workspace always means a new chat in // that workspace, no matter what useSameWorkspace says: // var startsOutsideWorkspaces = !useSameWorkspace && targetWorkspaceId is null; + + // + // The workspace of the chat template fills in only where the new chat would belong to no + // workspace otherwise. A workspace asked for on purpose wins: the one of the workspace list, + // and the open one for the button that starts a new chat in it. Such a chat gets no thread + // here: its first message creates one in that workspace, so that the workspace is created + // only once there is something to keep, see ResolveWorkspaceOfFirstThreadAsync. + // + if (targetWorkspaceId is null && workspaceOfNewChat == Guid.Empty && this.SettingsManager.GetChatTemplateWorkspaceName(this.currentChatTemplate) is not null) + startsOutsideWorkspaces = true; + if (startsOutsideWorkspaces) { // @@ -1916,7 +2058,11 @@ public partial class ChatComponent : MSGComponentBase var chatChatTemplate = this.ChatThread?.SelectedChatTemplate; this.Provider = this.SettingsManager.GetChatProviderForLoadedChat(chatProvider); - + + // A chat without a thread has not started yet, so it starts with what its chat template preselects: + if (this.ChatThread is null) + this.ApplyChatTemplateProfileAndProvider(this.currentChatTemplate); + await this.ProviderChanged.InvokeAsync(this.Provider); // Try to select the profile: @@ -2054,7 +2200,7 @@ public partial class ChatComponent : MSGComponentBase // that, and it may even bring an example conversation along -- reporting nothing for all // of it would tell a person their window is empty while their first message is not. // - var thread = this.ChatThread ?? this.NewChatThread(string.Empty); + var thread = this.ChatThread ?? this.NewChatThread(string.Empty, this.currentWorkspaceId); var toolDefinitions = this.GetRunnableToolDefinitions(offersSemanticSearch); provider = this.Provider; parts = ConversationParts.Of(thread, this.BuildSystemPromptFor(thread, toolDefinitions), this.UserInput, this.ComposerState.FileAttachments, provider.SupportsImageInput(), toolDefinitions); @@ -2153,18 +2299,22 @@ public partial class ChatComponent : MSGComponentBase /// The thread a new chat starts with, as the selections made so far decide it. /// /// - /// In one place because three code paths used to write it out, and because the token count has - /// to measure the same thing they build. A count against a thread assembled differently from - /// the one which is then sent would be wrong in exactly the moment a person looks at it: before - /// they send their first message. + /// The only place which builds a new thread: every way to start a chat comes here, and so does + /// the token count, which has to measure the same thing they build. A count against a thread + /// assembled differently from the one which is then sent would be wrong in exactly the moment a + /// person looks at it: before they send their first message. + /// + /// The workspace is no default of its own but always named by the caller, since that is the one + /// part the ways to start a chat decide differently: StartNewChat decides it right away, and a + /// chat which gets its thread with its first content asks ResolveWorkspaceOfFirstThreadAsync. /// /// The data source options are those chosen before the chat had a thread. Starting a new chat in /// a workspace replaces them right afterward with those its chat template asks for. /// /// The name of the thread. - /// The workspace of the thread. Without one, the thread goes to the workspace of the open chat. + /// The workspace of the thread, or Guid.Empty for none. /// The new thread. - private ChatThread NewChatThread(string name, Guid? workspaceId = null) => new() + private ChatThread NewChatThread(string name, Guid workspaceId) => new() { DataSourceOptions = this.earlyDataSourceOptions, IncludeDateTime = true, @@ -2173,7 +2323,7 @@ public partial class ChatComponent : MSGComponentBase SelectedChatTemplate = this.currentChatTemplate.Id, SelectedToolIds = [..this.selectedToolIds], SystemPrompt = SystemPrompts.DEFAULT, - WorkspaceId = workspaceId ?? this.currentWorkspaceId, + WorkspaceId = workspaceId, ChatId = Guid.NewGuid(), Name = name, Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(block => block.DeepClone()).ToList(), diff --git a/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor b/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor index 2ccad9843..5f9dd321b 100644 --- a/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor +++ b/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor @@ -1,16 +1,7 @@ +@using AIStudio.Settings.DataModel @inherits MSGComponentBase -@if (this.availableWorkspaces.Count > 0) -{ - - @foreach (var workspace in this.availableWorkspaces) - { - @workspace.Name - } - -} - - + @* The tile behaves differently with and without a workspace, so the form says which of the two is chosen right now instead of only explaining that both are possible. *@ @@ -19,6 +10,12 @@ ? T("Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings.") : T("The tile opens its chat in this workspace and creates the workspace when it does not exist yet.")) +@if (this.OpensTemporaryChat && this.SettingsManager.GetChatTemplateWorkspaceName(this.SelectedChatTemplate) is { } templateWorkspaceName) +{ + + @string.Format(T("The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template."), templateWorkspaceName) + +} @T("Use chat default") @foreach (var provider in this.SettingsManager.GetConfidentProviders(Components.CHAT)) @@ -28,6 +25,12 @@ } +@if (this.SelectedChatTemplate.ProviderId is not null) +{ + + @T("The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats.") + +} @T("Use chat default") @T("Use no profile") @@ -36,6 +39,12 @@ @profile.GetSafeName() } +@if (this.SelectedChatTemplate is { AllowProfileUsage: true, ProfileId: not null }) +{ + + @T("The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists.") + +} @T("Use chat default") @T("Use no chat template") @@ -44,20 +53,25 @@ @chatTemplate.GetSafeName() } - - @foreach (var dataSource in this.SettingsManager.ConfigurationData.DataSources) - { - @dataSource.Name - } - - -@* 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) +@* Without the RAG preview, the chat uses no data sources, so there is nothing to choose here. A + selection made before stays with the launcher and counts once RAG is switched on again: *@ +@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) { - - @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.") - + + @foreach (var dataSource in this.SettingsManager.ConfigurationData.DataSources) + { + @dataSource.Name + } + + + @* 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.") + + } } diff --git a/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor.cs b/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor.cs index 902d39ee1..5a3954cdf 100644 --- a/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor.cs +++ b/app/MindWork AI Studio/Components/DirectChatLauncherForm.razor.cs @@ -96,37 +96,6 @@ public partial class DirectChatLauncherForm : MSGComponentBase ? this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT) : this.SettingsManager.GetChatTemplateById(this.ChatTemplateId); - private IReadOnlyList availableWorkspaces = []; - - private static readonly Dictionary USER_INPUT_ATTRIBUTES = new(); - - #region Overrides of MSGComponentBase - - protected override async Task OnInitializedAsync() - { - // Configure the spellchecking for the workspace name input: - this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); - - await base.OnInitializedAsync(); - - var workspaceSnapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); - this.availableWorkspaces = workspaceSnapshot.Workspaces; - } - - #endregion - - // - // Picking an existing workspace fills the name field. Clearing the select must not wipe a name - // the user typed, though, so an empty selection is ignored: - // - private async Task SelectExistingWorkspace(string workspaceName) - { - if (string.IsNullOrWhiteSpace(workspaceName)) - return; - - await this.SetWorkspaceName(workspaceName); - } - private async Task SetWorkspaceName(string workspaceName) { this.WorkspaceName = workspaceName; diff --git a/app/MindWork AI Studio/Components/WorkspaceNameField.razor b/app/MindWork AI Studio/Components/WorkspaceNameField.razor new file mode 100644 index 000000000..69542641c --- /dev/null +++ b/app/MindWork AI Studio/Components/WorkspaceNameField.razor @@ -0,0 +1,13 @@ +@inherits MSGComponentBase + +@if (this.availableWorkspaces.Count > 0 && !this.ReadOnly) +{ + + @foreach (var workspace in this.availableWorkspaces) + { + @workspace.Name + } + +} + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/WorkspaceNameField.razor.cs b/app/MindWork AI Studio/Components/WorkspaceNameField.razor.cs new file mode 100644 index 000000000..a6da1b1bb --- /dev/null +++ b/app/MindWork AI Studio/Components/WorkspaceNameField.razor.cs @@ -0,0 +1,107 @@ +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +/// +/// Names a workspace by its name: an existing one to pick from, or a new name to type. +/// +/// +/// Assistant chat launchers and chat templates both name their workspace this way, because a +/// workspace has an ID of its own on every computer, and both create it when it does not exist +/// yet. The field offers the existing workspaces only as a shortcut; the name it hands out is +/// whatever the text field says. All texts come from the form around it, which knows what the +/// name is used for. +/// +public partial class WorkspaceNameField : MSGComponentBase +{ + /// + /// The name of the workspace. An empty name is up to the form around the field, e.g., a + /// disappearing chat for an assistant chat launcher. + /// + [Parameter] + public string WorkspaceName { get; set; } = string.Empty; + + [Parameter] + public EventCallback WorkspaceNameChanged { get; set; } + + /// + /// The label of the selection of existing workspaces. + /// + [Parameter] + public string SelectLabel { get; set; } = string.Empty; + + /// + /// The label of the text field for the name. + /// + [Parameter] + public string Label { get; set; } = string.Empty; + + [Parameter] + public string HelperText { get; set; } = string.Empty; + + [Parameter] + public string AdornmentIcon { get; set; } = Icons.Material.Filled.CreateNewFolder; + + [Parameter] + public Size AdornmentIconSize { get; set; } = Size.Medium; + + /// + /// The validation of the name, or null for none. + /// + [Parameter] + public Func? Validation { get; set; } + + /// + /// Whether every keystroke counts as a change, e.g., so that a validation follows the typing. + /// + [Parameter] + public bool Immediate { get; set; } + + /// + /// Shows the name without letting the user change it. The selection of existing workspaces + /// is left out then, since picking from it would change the name. + /// + [Parameter] + public bool ReadOnly { get; set; } + + [Parameter] + public string SelectClass { get; set; } = "mb-3 rounded-lg"; + + [Parameter] + public string TextFieldClass { get; set; } = "mb-3"; + + private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); + + private IReadOnlyList availableWorkspaces = []; + + #region Overrides of MSGComponentBase + + protected override async Task OnInitializedAsync() + { + this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); + await base.OnInitializedAsync(); + + var workspaceSnapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); + this.availableWorkspaces = workspaceSnapshot.Workspaces; + } + + #endregion + + // + // Picking an existing workspace fills the name field. Clearing the select must not wipe a name + // the user typed, though, so an empty selection is ignored: + // + private async Task SelectExistingWorkspace(string workspaceName) + { + if (string.IsNullOrWhiteSpace(workspaceName)) + return; + + await this.SetWorkspaceName(workspaceName); + } + + private async Task SetWorkspaceName(string workspaceName) + { + this.WorkspaceName = workspaceName; + await this.WorkspaceNameChanged.InvokeAsync(workspaceName); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor index 40c1d07a6..74734082e 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 @using AIStudio.Settings.DataModel @inherits MSGComponentBase @@ -136,6 +137,73 @@ @T("Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.") + @if (this.AllowProfileUsage) + { + + @T("A chat template may also decide which profile a chat starts with, including starting without one. Without such a decision, those chats start with the profile they would start with anyway. The profile stays changeable in the chat.") + + + @if (this.preselectProfile) + { + + @T("Use no profile") + @foreach (var profile in this.SettingsManager.ConfigurationData.Profiles) + { + @profile.GetSafeName() + } + + @if (this.IsPreselectedProfileMissing) + { + + @T("The profile this chat template preselects does not exist on this computer. Chats started with it keep their usual profile.") + + } + } + } + + + @T("Provider") + + + @T("A chat template may decide which provider, and with it which model, a chat starts with. Without such a decision, those chats start with the provider they would start with anyway. The provider stays changeable in the chat, and it must meet the confidence level required for chats.") + + + @if (this.preselectProvider) + { + + @foreach (var provider in this.SettingsManager.GetConfidentProviders(Components.CHAT)) + { + + + + } + + @if (this.IsPreselectedProviderUnusable) + { + + @T("The provider this chat template preselects does not exist on this computer or does not meet the confidence level required for chats. Chats started with it keep their usual provider.") + + } + } + + + @T("Workspace") + + + @T("A chat template may keep its chats in a workspace of its own, which is created when it does not exist yet. This applies whenever a chat would otherwise be a disappearing chat, for example, when you start a new chat outside the workspaces or pick this template in a new chat. A chat you start in a workspace on purpose, for example, from the workspace list, stays in that workspace.") + + @* While workspaces are off, nothing goes into a workspace, so choosing one here would promise what no chat does: *@ + @if (this.AreWorkspacesDisabled) + { + + @T("Workspaces are switched off in your workspace settings. While they are off, chats with this template belong to no workspace, and they do not move into a workspace when you switch workspaces on again. You can change this choice once workspaces are on.") + + } + + @if (this.preselectWorkspace) + { + + } @T("Tools") diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs index 0d199bae7..8f479df08 100644 --- a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs @@ -79,6 +79,25 @@ public partial class ChatTemplateDialog : MSGComponentBase [Parameter] public DataSourceOptions? DataSourceOptions { get; set; } + /// + /// The profile this template preselects, the ID of the no-profile entry to start without one, + /// or null when it says nothing about profiles. + /// + [Parameter] + public string? ProfileId { get; set; } + + /// + /// The provider this template preselects, or null when it says nothing about providers. + /// + [Parameter] + public string? ProviderId { get; set; } + + /// + /// The name of the workspace this template keeps its chats in, or null when it says nothing about workspaces. + /// + [Parameter] + public string? WorkspaceName { get; set; } + [Parameter] public bool CreateFromExistingChatThread { get; set; } @@ -105,6 +124,12 @@ public partial class ChatTemplateDialog : MSGComponentBase private HashSet selectedToolIds = new(StringComparer.Ordinal); private bool preselectDataSources; private DataSourceOptions templateDataSourceOptions = new(); + private bool preselectProfile; + private string selectedProfileId = Profile.NO_PROFILE.Id; + private bool preselectProvider; + private string selectedProviderId = string.Empty; + private bool preselectWorkspace; + private string selectedWorkspaceName = string.Empty; private string[] dataIssues = []; private string dataEditingPreviousName = string.Empty; private bool isInlineEditOnGoing; @@ -138,6 +163,14 @@ public partial class ChatTemplateDialog : MSGComponentBase // them, so the switch inside the selection starts on instead of at its usual default: this.templateDataSourceOptions = this.DataSourceOptions?.CreateCopy() ?? new DataSourceOptions { DisableDataSources = false }; + // The profile and the provider follow the same pattern, with a switch each: + this.preselectProfile = this.ProfileId is not null; + this.selectedProfileId = this.ProfileId ?? Profile.NO_PROFILE.Id; + this.preselectProvider = this.ProviderId is not null; + this.selectedProviderId = this.ProviderId ?? string.Empty; + this.preselectWorkspace = this.WorkspaceName is not null; + this.selectedWorkspaceName = this.WorkspaceName ?? string.Empty; + // When editing, we need to load the data: if(this.IsEditing) this.dataEditingPreviousName = this.DataName.ToLowerInvariant(); @@ -183,6 +216,11 @@ public partial class ChatTemplateDialog : MSGComponentBase ToolIds = this.preselectTools ? new HashSet(this.selectedToolIds, StringComparer.Ordinal) : null, DataSourceOptions = this.preselectDataSources ? this.templateDataSourceOptions.CreateCopy() : null, + // A template which forbids profiles has no use for a profile, so none is kept for it: + ProfileId = this.AllowProfileUsage && this.preselectProfile ? this.selectedProfileId : null, + ProviderId = this.preselectProvider && !string.IsNullOrWhiteSpace(this.selectedProviderId) ? this.selectedProviderId : null, + WorkspaceName = this.preselectWorkspace && !string.IsNullOrWhiteSpace(this.selectedWorkspaceName) ? this.selectedWorkspaceName.Trim() : null, + EnterpriseConfigurationPluginId = Guid.Empty, IsEnterpriseConfiguration = false, }; @@ -205,6 +243,57 @@ public partial class ChatTemplateDialog : MSGComponentBase private void SetSelectedToolIds(HashSet toolIds) => this.selectedToolIds = toolIds; + /// + /// Whether the preselected profile is gone, so that chats started with this template keep their usual profile. + /// + private bool IsPreselectedProfileMissing => this.preselectProfile + && this.selectedProfileId != Profile.NO_PROFILE.Id + && this.SettingsManager.GetProfileById(this.selectedProfileId) == Profile.NO_PROFILE; + + /// + /// Whether the preselected provider is gone or may not be used for chats, so that chats started + /// with this template keep their usual provider. + /// + /// + /// The list to pick from holds only the providers which may be used for chats. A template from + /// another machine or from a configuration plugin may name another one, though, and the list + /// alone would then show nothing about it. + /// + private bool IsPreselectedProviderUnusable => this.preselectProvider + && !string.IsNullOrWhiteSpace(this.selectedProviderId) + && this.SettingsManager.GetChatTemplateProvider(this.selectedProviderId) == AIStudio.Settings.Provider.NONE; + + /// + /// Whether workspaces are switched off, so that a workspace of this template has no effect for now. + /// + private bool AreWorkspacesDisabled => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.DISABLE_WORKSPACES; + + /// + /// Whether the workspace of this template can be changed right now. + /// + /// + /// Not while workspaces are switched off: a choice made then would take effect for no chat, + /// and nothing in the chat would say so. A workspace the template already names stays as it + /// is, so it applies again once workspaces are back. + /// + private bool IsWorkspaceChoiceLocked => this.IsReadOnly || this.AreWorkspacesDisabled; + + private string? ValidateWorkspaceName(string workspaceName) + { + if (this.preselectWorkspace && string.IsNullOrWhiteSpace(workspaceName)) + return T("Please enter the workspace for the chats of this template, or switch the preselection off."); + + return null; + } + + private string? ValidatePreselectedProvider(string providerId) + { + if (this.preselectProvider && string.IsNullOrWhiteSpace(providerId)) + return T("Please choose the provider this chat template preselects, or switch the preselection off."); + + return null; + } + 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 c74b9b239..6e5b7d22a 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs @@ -114,6 +114,9 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase { x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage }, { x => x.ToolIds, chatTemplate.ToolIds }, { x => x.DataSourceOptions, chatTemplate.DataSourceOptions }, + { x => x.ProfileId, chatTemplate.ProfileId }, + { x => x.ProviderId, chatTemplate.ProviderId }, + { x => x.WorkspaceName, chatTemplate.WorkspaceName }, }; private async Task DeleteChatTemplate(ChatTemplate chatTemplate) diff --git a/app/MindWork AI Studio/Plugins/assistants/README.md b/app/MindWork AI Studio/Plugins/assistants/README.md index cd267985b..b21239593 100644 --- a/app/MindWork AI Studio/Plugins/assistants/README.md +++ b/app/MindWork AI Studio/Plugins/assistants/README.md @@ -153,6 +153,8 @@ ASSISTANT = { - 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. +- A chat template may also preselect a provider and a profile, and the same rule applies: they win over the launcher `ProviderId` and `ProfileId`, with a warning in the log. A provider of the template which is gone or does not meet the confidence level required for chats, and a profile of the template which is gone, are left out instead of stopping the launch; the launcher then decides as if the template named none. +- A chat template may name a workspace for its chats as well. Here the launcher wins when it names a workspace of its own, because that choice was made on purpose. A launcher that opens a disappearing chat opens it in the workspace of its template instead, which is created when it does not exist yet; if that fails, the chat stays a disappearing chat. - 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. diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 5b3e9ae1f..1876b4be6 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -1363,6 +1363,42 @@ CONFIG["CHAT_TEMPLATES"] = {} -- }, -- } +-- An example chat template which preselects a provider, a profile, and a workspace: +-- All three are optional and independent of each other, and leaving one out keeps what a +-- chat starts with anyway. They are a preselection, not a limit: users change the provider +-- and the profile in the chat as usual, and move the chat to another workspace. The IDs may +-- name a provider or profile of this configuration, which keeps its ID on every computer, or +-- one of another configuration of your organization. +-- CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { +-- ["Id"] = "00000000-0000-0000-0000-000000000003", +-- ["Name"] = "Mail Assistant", +-- ["SystemPrompt"] = "You help 's employees read, sort, and answer their mails.", +-- ["AllowProfileUsage"] = true, +-- +-- -- Optional: the provider, and with it the model, a chat with this template starts with. +-- -- Must be an ID from CONFIG["LLM_PROVIDERS"] or of a provider of another configuration. +-- -- The provider has to meet the confidence level required for chats. A provider which +-- -- does not, or an ID which resolves to nothing, is ignored, and the chat starts with +-- -- the provider it would start with anyway. +-- ["ProviderId"] = "11111111-1111-1111-1111-111111111111", +-- +-- -- Optional: the profile a chat with this template starts with. +-- -- Must be an ID from CONFIG["PROFILES"] or of a profile of another configuration. +-- -- "00000000-0000-0000-0000-000000000000" starts the chat without a profile, whatever +-- -- the user chose as their default. An ID which resolves to nothing is ignored, and the +-- -- chat starts with the default profile of the user. The whole field is ignored when +-- -- AllowProfileUsage is false. +-- ["ProfileId"] = "22222222-2222-2222-2222-222222222222", +-- +-- -- Optional: the workspace, by its name, that keeps the chats started with this template. +-- -- The workspace is created with the first message of such a chat, when it does not exist +-- -- yet. It only applies where a chat would otherwise be a disappearing chat: a chat users +-- -- start in a workspace on purpose, for example, from the workspace list, stays there, and +-- -- so does every chat while workspaces are switched off. An assistant chat launcher which +-- -- names a workspace keeps its own. +-- ["WorkspaceName"] = "Mails", +-- } + -- 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 424aeee7e..29bbfa25c 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 @@ -4401,9 +4401,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1797236585"] = " -- Workspace name (Optional) UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1873204484"] = "Name des Arbeitsbereichs (optional)" +-- The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1876510820"] = "Die ausgewählte Chat-Vorlage wählt einen eigenen Anbieter vorab aus; dieser hat Vorrang vor einer hier getroffenen Auswahl, solange er für Chats verwendet werden kann." + -- Use no profile UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2205839602"] = "Kein Profil verwenden" +-- The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2344103917"] = "Die ausgewählte Chat-Vorlage legt ihre Chats im Arbeitsbereich „{0}“ ab, daher öffnet die Kachel ihren Chat dort. Ein hier eingegebener Arbeitsbereich hat Vorrang vor dem der Vorlage." + -- Existing workspace (Optional) UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = "Vorhandener Arbeitsbereich (optional)" @@ -4425,6 +4431,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2901190527"] = " -- Data sources (Optional) UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3259309302"] = "Datenquellen (optional)" +-- The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3419242912"] = "Die ausgewählte Chat-Vorlage wählt ein eigenes Profil vorab aus; dieses hat Vorrang vor einer hier getroffenen Auswahl, solange das Profil existiert." + -- Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = "Ohne Arbeitsbereich öffnet die Kachel einen selbstlöschenden Chat: Er gehört keinem Arbeitsbereich an und wird gemäß den Wartungseinstellungen für Ihre Arbeitsbereiche gelöscht." @@ -6189,14 +6198,23 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "Der Nam -- 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." +-- Preselected provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1469984996"] = "Vorausgewählter Anbieter" + -- 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." +-- A chat template may decide which provider, and with it which model, a chat starts with. Without such a decision, those chats start with the provider they would start with anyway. The provider stays changeable in the chat, and it must meet the confidence level required for chats. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T171066641"] = "Eine Chat-Vorlage kann festlegen, mit welchem Anbieter – und damit mit welchem Modell – ein Chat startet. Ohne eine solche Festlegung starten diese Chats mit dem Anbieter, mit dem sie ohnehin starten würden. Der Anbieter lässt sich im Chat weiterhin ändern und muss das für Chats erforderliche Vertrauensniveau erfüllen." + -- 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" +-- Preselected profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T178965494"] = "Vorausgewähltes Profil" + -- Enter the full path of a file on this computer, or remove the attachment. -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1798848159"] = "Geben Sie den vollständigen Pfad zu einer Datei auf diesem Computer ein oder entfernen Sie den Anhang." +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1798848159"] = "Geben Sie den vollständigen Pfad zu einer Datei auf diesem Rechner ein oder entfernen Sie den Anhang." -- Load predefined user input from file UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Vordefinierte Benutzereingabe aus Datei laden" @@ -6204,12 +6222,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Vordefi -- Update UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1847791252"] = "Aktualisieren" +-- Yes, this template decides which profile a chat starts with +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1865005833"] = "Ja, diese Vorlage legt fest, mit welchem Profil ein Chat startet" + -- The chat template name must not exceed 40 characters. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T204496403"] = "Der Name der Chat-Vorlage darf maximal 40 Zeichen lang sein." -- Profile Usage UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2147062613"] = "Profilnutzung" +-- Use no profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2205839602"] = "Kein Profil verwenden" + +-- Yes, chats with this template go into a workspace +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2205963533"] = "Ja, Chats mit dieser Vorlage werden in einem Arbeitsbereich abgelegt" + -- Relink attachment: {0} UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2234793579"] = "Anhang erneut verknüpfen: {0}" @@ -6219,6 +6246,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2292424657"] = "Fügen -- File Attachments UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "Dateianhänge" +-- No, chats go where they would go anyway +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2355334446"] = "Nein, Chats werden dort abgelegt, wo sie ohnehin landen würden" + +-- Existing workspace (Optional) +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2364306588"] = "Vorhandener Arbeitsbereich (optional)" + -- Role UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Rolle" @@ -6231,6 +6264,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2499909372"] = "Werkzeu -- What predefined user input do you want to use? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2501284417"] = "Welche vordefinierte Benutzereingabe möchten Sie verwenden?" +-- Please enter the workspace for the chats of this template, or switch the preselection off. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2504276031"] = "Bitte geben Sie den Arbeitsbereich für die Chats dieser Vorlage ein oder deaktivieren Sie die Vorauswahl." + +-- Does this chat template preselect a provider? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2541393632"] = "Wählt diese Chat-Vorlage einen Anbieter vorab aus?" + -- Tell the AI your system prompt. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2552381261"] = "Teilen Sie der KI einen System-Prompt mit." @@ -6240,6 +6279,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579080722"] = "Nein, d -- You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579208700"] = "Sie können eine Nachricht vordefinieren, die als Benutzereingabe verwendet wird, wenn Sie diese Chat-Vorlage verwenden. Diese Nachricht könnte beispielsweise eine Vorlage für eine strukturierte Nachricht sein, für die diese Chat-Vorlage entwickelt wurde." +-- Does this chat template preselect a profile? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2602340928"] = "Wählt diese Chat-Vorlage ein Profil vorab aus?" + -- Predefined User Input UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2607897066"] = "Vordefinierte Benutzereingabe" @@ -6255,8 +6297,14 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2719480499"] = "Die Nac -- Chat Template Name UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T275026390"] = "Name der Chat-Vorlage" +-- Workspaces are switched off in your workspace settings. While they are off, chats with this template belong to no workspace, and they do not move into a workspace when you switch workspaces on again. You can change this choice once workspaces are on. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2828180414"] = "In Ihren Arbeitsbereichseinstellungen sind Arbeitsbereiche deaktiviert. Solange sie deaktiviert sind, gehören Chats mit dieser Vorlage zu keinem Arbeitsbereich und werden auch nicht in einen Arbeitsbereich verschoben, wenn Sie Arbeitsbereiche wieder aktivieren. Sie können diese Auswahl ändern, sobald Arbeitsbereiche aktiviert sind." + -- Please relink the attachment '{0}' to a file on this computer or remove it before you save. -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2884684079"] = "Bitte verknüpfen Sie den Anhang „{0}“ vor dem Speichern erneut mit einer Datei auf diesem Computer oder entfernen Sie ihn." +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2884684079"] = "Bitte verknüpfen Sie den Anhang „{0}“ vor dem Speichern erneut mit einer Datei auf diesem Rechner oder entfernen Sie ihn." + +-- Workspace name +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T295876489"] = "Name des Arbeitsbereichs" -- Commit Changes UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2996302517"] = "Änderungen übernehmen" @@ -6273,21 +6321,42 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3108503534"] = "Sie kö -- Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Sind Sie unsicher, welchen System-Prompt Sie verwenden sollen? Sie können mit dem Standard-System-Prompt beginnen, den AI Studio für alle Chats verwendet." +-- A chat template may also decide which profile a chat starts with, including starting without one. Without such a decision, those chats start with the profile they would start with anyway. The profile stays changeable in the chat. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T313529011"] = "Eine Chat-Vorlage kann auch festlegen, mit welchem Profil ein Chat startet, oder dass er ohne Profil startet. Ohne eine solche Festlegung starten diese Chats mit dem Profil, mit dem sie ohnehin starten würden. Das Profil lässt sich im Chat weiterhin ändern." + +-- The profile this chat template preselects does not exist on this computer. Chats started with it keep their usual profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3170335831"] = "Das Profil, das diese Chat-Vorlage vorab auswählt, ist auf diesem Rechner nicht vorhanden. Mit ihr gestartete Chats behalten ihr übliches Profil." + -- 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." +-- Choose an existing workspace or enter a name that should be created when a chat with this template starts. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3256439227"] = "Wählen Sie einen vorhandenen Arbeitsbereich aus oder geben Sie einen Namen ein, der erstellt werden soll, wenn ein Chat mit dieser Vorlage startet." + -- 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." +-- A chat template may keep its chats in a workspace of its own, which is created when it does not exist yet. This applies whenever a chat would otherwise be a disappearing chat, for example, when you start a new chat outside the workspaces or pick this template in a new chat. A chat you start in a workspace on purpose, for example, from the workspace list, stays in that workspace. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3352941260"] = "Eine Chat-Vorlage kann ihre Chats in einem eigenen Arbeitsbereich ablegen, der erstellt wird, falls er noch nicht existiert. Das gilt immer dann, wenn ein Chat sonst ein selbstlöschender Chat wäre, zum Beispiel wenn Sie einen neuen Chat außerhalb der Arbeitsbereiche starten oder diese Vorlage in einem neuen Chat auswählen. Ein Chat, den Sie gezielt in einem Arbeitsbereich starten, zum Beispiel über die Liste der Arbeitsbereiche, bleibt in diesem Arbeitsbereich." + -- Add a message UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Nachricht hinzufügen" +-- Yes, this template decides which provider a chat starts with +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3422337353"] = "Ja, diese Vorlage legt fest, mit welchem Anbieter ein Chat startet" + -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Schließen" +-- No, chats keep their usual profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3465925873"] = "Nein, Chats behalten ihr übliches Profil" + +-- The provider this chat template preselects does not exist on this computer or does not meet the confidence level required for chats. Chats started with it keep their usual provider. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T349281189"] = "Der Anbieter, den diese Chat-Vorlage vorab auswählt, ist auf diesem Rechner nicht vorhanden oder erfüllt das für Chats erforderliche Vertrauensniveau nicht. Mit ihr gestartete Chats behalten ihren üblichen Anbieter." + -- Unsupported content type UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Nicht unterstützter Inhaltstyp" @@ -6324,12 +6393,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4051106111"] = "Verwend -- Tell the AI your predefined user input. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4052406705"] = "Teilen Sie der KI Ihre vordefinierte Benutzereingabe mit." +-- Does this chat template keep its chats in a workspace? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4057825866"] = "Legt diese Chat-Vorlage ihre Chats in einem Arbeitsbereich ab?" + -- Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template. -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4194591607"] = "Einige Anhänge dieser Chat-Vorlage sind auf diesem Computer nicht vorhanden. Bitte verknüpfen Sie sie erneut oder entfernen Sie sie, bevor Sie die Vorlage hinzufügen." +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4194591607"] = "Einige Anhänge dieser Chat-Vorlage sind auf diesem Rechner nicht vorhanden. Bitte verknüpfen Sie sie erneut oder entfernen Sie sie, bevor Sie die Vorlage hinzufügen." -- Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Erstellen Sie Ihre eigene Chat-Vorlage, um das Verhalten des LLMs für bestimmte Aufgaben oder Bereiche anzupassen. Definieren Sie einen individuellen System-Prompt und geben Sie eine Beispiel-Konversation vor, um eine KI-Erfahrung zu gestalten, die genau auf Ihre Anforderungen zugeschnitten ist." +-- Please choose the provider this chat template preselects, or switch the preselection off. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T442309864"] = "Bitte wählen Sie den Anbieter aus, den diese Chat-Vorlage vorab auswählt, oder deaktivieren Sie die Vorauswahl." + -- Enter a message UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Nachricht eingeben" @@ -6342,15 +6417,24 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System-P -- 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." +-- Workspace +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T658612054"] = "Arbeitsbereich" + -- Remove this attachment UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T70791272"] = "Diesen Anhang entfernen" -- 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." +-- No, chats keep their usual provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T787085105"] = "Nein, Chats behalten ihren üblichen Anbieter" + -- 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?" +-- Provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900237532"] = "Anbieter" + -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Abbrechen" @@ -12994,41 +13078,53 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECUR UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T995107927"] = "Der Plugin-Code wurde nach der letzten Sicherheitsprüfung geändert. Das gespeicherte Ergebnis stimmt nicht mehr mit dem aktuellen Code überein, daher muss dieses Assistenten-Plugin erneut geprüft werden, bevor es aktiviert oder verwendet werden darf." -- The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T186946778"] = "Das Token oder Passwort der Datenquelle lässt sich auf diesem Computer nicht entschlüsseln. Datenquellen dieser Art können nur auf einem Computer importiert werden, auf dem das Verschlüsselungsgeheimnis Ihrer Organisation eingerichtet ist." +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T186946778"] = "Das Token oder Passwort der Datenquelle lässt sich auf diesem Rechner nicht entschlüsseln. Datenquellen dieser Art können nur auf einem Rechner importiert werden, auf dem das Verschlüsselungsgeheimnis Ihrer Organisation eingerichtet ist." + +-- The chat template preselects the provider '{0}', which does not meet the confidence level required for chats. Chats started with it use your usual provider instead. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T190892810"] = "Die Chat-Vorlage wählt den Anbieter „{0}“ vorab aus, der das für Chats erforderliche Vertrauensniveau nicht erfüllt. Mit ihr gestartete Chats verwenden dann Ihren üblichen Anbieter." -- Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T206419407"] = "Datenquellen, die Kerberos zur Anmeldung verwenden, können noch nicht importiert werden. Bitte fügen Sie sie selbst hinzu." -- The tokenizer '{0}' is not on this computer, so the provider is imported without it. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2556276619"] = "Der Tokenizer „{0}“ ist auf diesem Computer nicht vorhanden. Daher wird der Anbieter ohne ihn importiert." +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2556276619"] = "Der Tokenizer „{0}“ ist auf diesem Rechner nicht vorhanden. Daher wird der Anbieter ohne ihn importiert." -- The chat template preselects data sources which do not exist on this computer: {0}. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2608225446"] = "Die Chat-Vorlage wählt Datenquellen vorab aus, die auf diesem Computer nicht vorhanden sind: {0}." +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2608225446"] = "Die Chat-Vorlage wählt Datenquellen vorab aus, die auf diesem Rechner nicht vorhanden sind: {0}." -- The chat template preselects tools which are not available on this computer: {0}. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2693686582"] = "Die Chat-Vorlage wählt Werkzeuge vorab aus, die auf diesem Computer nicht verfügbar sind: {0}." +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2693686582"] = "Die Chat-Vorlage wählt Werkzeuge vorab aus, die auf diesem Rechner nicht verfügbar sind: {0}." + +-- The chat template preselects a profile which does not exist on this computer: {0}. Chats started with it use your usual profile instead. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2735390326"] = "Die Chat-Vorlage wählt ein Profil vorab aus, das auf diesem Rechner nicht vorhanden ist: {0}. Mit ihr gestartete Chats verwenden dann Ihr übliches Profil." + +-- The chat template preselects a provider which does not exist on this computer: {0}. Chats started with it use your usual provider instead. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3321789654"] = "Die Chat-Vorlage wählt einen Anbieter vorab aus, der auf diesem Rechner nicht vorhanden ist: {0}. Mit ihr gestartete Chats verwenden dann Ihren üblichen Anbieter." -- The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3589070261"] = "Der API-Schlüssel in der eingefügten Konfiguration kann auf diesem Computer nicht entschlüsselt werden. Bitte geben Sie Ihren API-Schlüssel ein, bevor Sie den Anbieter speichern." +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3589070261"] = "Der API-Schlüssel in der eingefügten Konfiguration kann auf diesem Rechner nicht entschlüsselt werden. Bitte geben Sie Ihren API-Schlüssel ein, bevor Sie den Anbieter speichern." -- The chat template attaches these files from this computer to every chat started with it: {0}. Please check that you want to share them. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3702015680"] = "Die Chat-Vorlage fügt jedem damit gestarteten Chat folgende Dateien von diesem Computer hinzu: {0}. Bitte vergewissern Sie sich, dass Sie diese Dateien teilen möchten." +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3702015680"] = "Die Chat-Vorlage fügt jedem damit gestarteten Chat folgende Dateien von diesem Rechner hinzu: {0}. Bitte vergewissern Sie sich, dass Sie diese Dateien teilen möchten." -- The policy preselects a provider which does not exist on this computer: {0}. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3849646238"] = "Das Regelwerk wählt einen Anbieter vorab aus, der auf diesem Computer nicht vorhanden ist: {0}." +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3849646238"] = "Das Regelwerk wählt einen Anbieter vorab aus, der auf diesem Rechner nicht vorhanden ist: {0}." -- The pasted configuration is not a valid configuration of the type '{0}'. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T4231596123"] = "Die eingefügte Konfiguration ist keine gültige Konfiguration vom Typ „{0}“." -- The policy permits tools which are not available on this computer: {0}. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T582643438"] = "Das Regelwerk erlaubt Werkzeuge, die auf diesem Computer nicht verfügbar sind: {0}." +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T582643438"] = "Das Regelwerk erlaubt Werkzeuge, die auf diesem Rechner nicht verfügbar sind: {0}." -- The policy preselects a profile which does not exist on this computer: {0}. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T600255824"] = "Das Regelwerk wählt ein Profil vorab aus, das auf diesem Computer nicht vorhanden ist: {0}." +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T600255824"] = "Das Regelwerk wählt ein Profil vorab aus, das auf diesem Rechner nicht vorhanden ist: {0}." -- The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T639320638"] = "Die eingefügte Konfiguration enthält einen unverschlüsselten API-Schlüssel. Dieser wurde daher ausgelassen. Bitte geben Sie Ihren API-Schlüssel ein, bevor Sie den Anbieter speichern." +-- The chat template preselects a profile but does not allow profiles. Chats started with it ignore that profile. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T769062029"] = "Die Chat-Vorlage wählt vorab ein Profil aus, erlaubt aber keine Profile. Mit ihr gestartete Chats ignorieren dieses Profil." + -- Expected a quoted string at character {0}. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1235695745"] = "An Position {0} wurde ein Text in Anführungszeichen erwartet." 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 d3210b17e..d28906ed2 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 @@ -4401,9 +4401,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1797236585"] = " -- Workspace name (Optional) UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1873204484"] = "Workspace name (Optional)" +-- The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1876510820"] = "The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats." + -- Use no profile UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2205839602"] = "Use no profile" +-- The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2344103917"] = "The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template." + -- Existing workspace (Optional) UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = "Existing workspace (Optional)" @@ -4425,6 +4431,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2901190527"] = " -- Data sources (Optional) UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3259309302"] = "Data sources (Optional)" +-- The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3419242912"] = "The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists." + -- Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = "Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings." @@ -6189,12 +6198,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "The cha -- 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." +-- Preselected provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1469984996"] = "Preselected provider" + -- Please enter a name for the chat template. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Please enter a name for the chat template." +-- A chat template may decide which provider, and with it which model, a chat starts with. Without such a decision, those chats start with the provider they would start with anyway. The provider stays changeable in the chat, and it must meet the confidence level required for chats. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T171066641"] = "A chat template may decide which provider, and with it which model, a chat starts with. Without such a decision, those chats start with the provider they would start with anyway. The provider stays changeable in the chat, and it must meet the confidence level required for chats." + -- 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" +-- Preselected profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T178965494"] = "Preselected profile" + -- Enter the full path of a file on this computer, or remove the attachment. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1798848159"] = "Enter the full path of a file on this computer, or remove the attachment." @@ -6204,12 +6222,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Load pr -- Update UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1847791252"] = "Update" +-- Yes, this template decides which profile a chat starts with +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1865005833"] = "Yes, this template decides which profile a chat starts with" + -- The chat template name must not exceed 40 characters. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T204496403"] = "The chat template name must not exceed 40 characters." -- Profile Usage UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2147062613"] = "Profile Usage" +-- Use no profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2205839602"] = "Use no profile" + +-- Yes, chats with this template go into a workspace +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2205963533"] = "Yes, chats with this template go into a workspace" + -- Relink attachment: {0} UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2234793579"] = "Relink attachment: {0}" @@ -6219,6 +6246,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2292424657"] = "Add mes -- File Attachments UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "File Attachments" +-- No, chats go where they would go anyway +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2355334446"] = "No, chats go where they would go anyway" + +-- Existing workspace (Optional) +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2364306588"] = "Existing workspace (Optional)" + -- Role UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Role" @@ -6231,6 +6264,12 @@ 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?" +-- Please enter the workspace for the chats of this template, or switch the preselection off. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2504276031"] = "Please enter the workspace for the chats of this template, or switch the preselection off." + +-- Does this chat template preselect a provider? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2541393632"] = "Does this chat template preselect a provider?" + -- Tell the AI your system prompt. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2552381261"] = "Tell the AI your system prompt." @@ -6240,6 +6279,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579080722"] = "No, pro -- You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579208700"] = "You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with." +-- Does this chat template preselect a profile? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2602340928"] = "Does this chat template preselect a profile?" + -- Predefined User Input UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2607897066"] = "Predefined User Input" @@ -6255,9 +6297,15 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2719480499"] = "The mes -- Chat Template Name UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T275026390"] = "Chat Template Name" +-- Workspaces are switched off in your workspace settings. While they are off, chats with this template belong to no workspace, and they do not move into a workspace when you switch workspaces on again. You can change this choice once workspaces are on. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2828180414"] = "Workspaces are switched off in your workspace settings. While they are off, chats with this template belong to no workspace, and they do not move into a workspace when you switch workspaces on again. You can change this choice once workspaces are on." + -- Please relink the attachment '{0}' to a file on this computer or remove it before you save. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2884684079"] = "Please relink the attachment '{0}' to a file on this computer or remove it before you save." +-- Workspace name +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T295876489"] = "Workspace name" + -- Commit Changes UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2996302517"] = "Commit Changes" @@ -6273,21 +6321,42 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3108503534"] = "You can -- Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats." +-- A chat template may also decide which profile a chat starts with, including starting without one. Without such a decision, those chats start with the profile they would start with anyway. The profile stays changeable in the chat. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T313529011"] = "A chat template may also decide which profile a chat starts with, including starting without one. Without such a decision, those chats start with the profile they would start with anyway. The profile stays changeable in the chat." + +-- The profile this chat template preselects does not exist on this computer. Chats started with it keep their usual profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3170335831"] = "The profile this chat template preselects does not exist on this computer. Chats started with it keep their usual profile." + -- 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." +-- Choose an existing workspace or enter a name that should be created when a chat with this template starts. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3256439227"] = "Choose an existing workspace or enter a name that should be created when a chat with this template starts." + -- 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." +-- A chat template may keep its chats in a workspace of its own, which is created when it does not exist yet. This applies whenever a chat would otherwise be a disappearing chat, for example, when you start a new chat outside the workspaces or pick this template in a new chat. A chat you start in a workspace on purpose, for example, from the workspace list, stays in that workspace. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3352941260"] = "A chat template may keep its chats in a workspace of its own, which is created when it does not exist yet. This applies whenever a chat would otherwise be a disappearing chat, for example, when you start a new chat outside the workspaces or pick this template in a new chat. A chat you start in a workspace on purpose, for example, from the workspace list, stays in that workspace." + -- Add a message UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message" +-- Yes, this template decides which provider a chat starts with +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3422337353"] = "Yes, this template decides which provider a chat starts with" + -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Close" +-- No, chats keep their usual profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3465925873"] = "No, chats keep their usual profile" + +-- The provider this chat template preselects does not exist on this computer or does not meet the confidence level required for chats. Chats started with it keep their usual provider. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T349281189"] = "The provider this chat template preselects does not exist on this computer or does not meet the confidence level required for chats. Chats started with it keep their usual provider." + -- Unsupported content type UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Unsupported content type" @@ -6324,12 +6393,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4051106111"] = "Use the -- Tell the AI your predefined user input. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4052406705"] = "Tell the AI your predefined user input." +-- Does this chat template keep its chats in a workspace? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4057825866"] = "Does this chat template keep its chats in a workspace?" + -- Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4194591607"] = "Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template." -- Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements." +-- Please choose the provider this chat template preselects, or switch the preselection off. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T442309864"] = "Please choose the provider this chat template preselects, or switch the preselection off." + -- Enter a message UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Enter a message" @@ -6342,15 +6417,24 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System P -- 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." +-- Workspace +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T658612054"] = "Workspace" + -- Remove this attachment UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T70791272"] = "Remove this attachment" -- 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." +-- No, chats keep their usual provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T787085105"] = "No, chats keep their usual provider" + -- 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?" +-- Provider +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900237532"] = "Provider" + -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel" @@ -12996,6 +13080,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECUR -- The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T186946778"] = "The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured." +-- The chat template preselects the provider '{0}', which does not meet the confidence level required for chats. Chats started with it use your usual provider instead. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T190892810"] = "The chat template preselects the provider '{0}', which does not meet the confidence level required for chats. Chats started with it use your usual provider instead." + -- Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T206419407"] = "Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself." @@ -13008,6 +13095,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T260 -- The chat template preselects tools which are not available on this computer: {0}. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2693686582"] = "The chat template preselects tools which are not available on this computer: {0}." +-- The chat template preselects a profile which does not exist on this computer: {0}. Chats started with it use your usual profile instead. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2735390326"] = "The chat template preselects a profile which does not exist on this computer: {0}. Chats started with it use your usual profile instead." + +-- The chat template preselects a provider which does not exist on this computer: {0}. Chats started with it use your usual provider instead. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3321789654"] = "The chat template preselects a provider which does not exist on this computer: {0}. Chats started with it use your usual provider instead." + -- The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3589070261"] = "The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider." @@ -13029,6 +13122,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T600 -- The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T639320638"] = "The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider." +-- The chat template preselects a profile but does not allow profiles. Chats started with it ignore that profile. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T769062029"] = "The chat template preselects a profile but does not allow profiles. Chats started with it ignore that profile." + -- Expected a quoted string at character {0}. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1235695745"] = "Expected a quoted string at character {0}." diff --git a/app/MindWork AI Studio/Settings/ChatTemplate.cs b/app/MindWork AI Studio/Settings/ChatTemplate.cs index ebe52bec8..2e3b836e7 100644 --- a/app/MindWork AI Studio/Settings/ChatTemplate.cs +++ b/app/MindWork AI Studio/Settings/ChatTemplate.cs @@ -50,7 +50,47 @@ public record ChatTemplate( /// whether the retrieved data is validated. /// public DataSourceOptions? DataSourceOptions { get; init; } - + + /// + /// The profile this template preselects for a chat started with it. + /// + /// + /// Null means the template says nothing about profiles, so the chat keeps the profile it would + /// start with anyway: the default of the chat settings, or the one the user picked. The ID of + /// Profile.NO_PROFILE is a statement of its own: such chats start without a profile, whatever + /// that default says.

+ /// A preselection, not a limit: the user picks another profile in the chat as usual. It means + /// nothing for a template which does not allow profiles, and a profile which is gone leaves the + /// chat default in place. + ///
+ public string? ProfileId { get; init; } + + /// + /// The provider, and with it the model, this template preselects for a chat started with it. + /// + /// + /// Null means the template says nothing, so the chat starts with the provider it would start + /// with anyway. A preselection, not a limit, and no way around the confidence levels either: a + /// provider which is gone, or which may not be used for chats, leaves the chat to start as if + /// the template said nothing. + /// + public string? ProviderId { get; init; } + + /// + /// The name of the workspace a chat started with this template is kept in. + /// + /// + /// Null means the template says nothing, so the chat goes where it would go anyway. A name + /// rather than an ID: a workspace has an ID of its own on every computer, so a configuration + /// plugin has no other way to name one. Like for an assistant chat launcher, the workspace is + /// created when it does not exist yet; on the chat page only with the first message of the chat, + /// so that trying out a template creates no workspace.

+ /// It only fills in where a chat would belong to no workspace otherwise. A chat started in a + /// workspace on purpose, for example from the workspace list, stays there, and so does every + /// chat while workspaces are switched off. + ///
+ public string? WorkspaceName { get; init; } + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatTemplate).Namespace, nameof(ChatTemplate)); private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); @@ -67,6 +107,9 @@ public record ChatTemplate( AllowProfileUsage = true, ToolIds = null, DataSourceOptions = null, + ProfileId = null, + ProviderId = null, + WorkspaceName = null, EnterpriseConfigurationPluginId = Guid.Empty, IsEnterpriseConfiguration = false, }; @@ -81,6 +124,16 @@ public record ChatTemplate( #endregion + /// + /// Whether this template starts its chats without a profile, whatever the default says. + /// + /// + /// That is a statement of its own, made with the ID of the no-profile entry, see ProfileId. It + /// names no profile which could be missing on another computer. A method rather than a property, + /// so that it does not end up in the stored settings. + /// + public bool StartsWithoutProfile() => string.Equals(this.ProfileId, Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase); + /// /// Gets the name of this chat template. If it is the NO_CHAT_TEMPLATE, it returns a localized string. /// @@ -117,12 +170,12 @@ public record ChatTemplate( /// 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) + public static ChatTemplateToolChoice ChooseToolIds(ChatTemplate chatTemplate, IReadOnlyCollection? launcherToolIds) { if (chatTemplate.ToolIds is not { } templateToolIds) - return (launcherToolIds, false); + return new(launcherToolIds, false); - return (templateToolIds, launcherToolIds is not null); + return new(templateToolIds, launcherToolIds is not null); } /// @@ -137,12 +190,12 @@ public record ChatTemplate( /// 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) + public static ChatTemplateDataSourceChoice ChooseDataSourceOptions(ChatTemplate chatTemplate, DataSourceOptions? launcherOptions) { if (chatTemplate.DataSourceOptions is not { } templateOptions) - return (launcherOptions, false); + return new(launcherOptions, false); - return (templateOptions.CreateCopy(), launcherOptions is not null); + return new(templateOptions.CreateCopy(), launcherOptions is not null); } /// @@ -202,6 +255,10 @@ public record ChatTemplate( var fileAttachments = ParseFileAttachments(idx, table, pluginPath); + var profileId = ParseOptionalId(idx, table, "ProfileId", allowEmpty: true); + if (profileId is not null && !allowProfileUsage) + LOGGER.LogWarning("The chat template {IdxChatTemplate} preselects a profile but does not allow profiles. Chats started with it ignore that profile.", idx); + template = new ChatTemplate { Num = 0, // will be set later by the PluginConfigurationObject @@ -214,6 +271,9 @@ public record ChatTemplate( AllowProfileUsage = allowProfileUsage, ToolIds = ParseToolIds(idx, table), DataSourceOptions = ParseDataSourceOptions(idx, table), + ProfileId = profileId, + ProviderId = ParseOptionalId(idx, table, "ProviderId", allowEmpty: false), + WorkspaceName = ParseWorkspaceName(idx, table), IsEnterpriseConfiguration = true, EnterpriseConfigurationPluginId = configPluginId, }; @@ -294,6 +354,43 @@ public record ChatTemplate( return toolIds; } + /// + /// The empty GUID is a statement for a profile — start without one — but names no provider at + /// all, hence the switch. Anything which is no GUID cannot point at a provider or profile and is + /// dropped, so that the template still works, with the chat defaults in its place. + /// + private static string? ParseOptionalId(int idx, LuaTable table, string fieldName, bool allowEmpty) + { + if (!table.TryGetValue(fieldName, out var idValue)) + return null; + + if (!idValue.TryRead(out var idText) || !Guid.TryParse(idText, out var id) || (!allowEmpty && id == Guid.Empty)) + { + LOGGER.LogWarning("The {FieldName} of chat template {IdxChatTemplate} is not a valid ID and will be ignored.", fieldName, idx); + return null; + } + + return id.ToString(); + } + + /// + /// An empty name would mean "no workspace", which is what a template says by leaving the field + /// out. So it is dropped, with a warning, as is anything which is no text. + /// + private static string? ParseWorkspaceName(int idx, LuaTable table) + { + if (!table.TryGetValue("WorkspaceName", out var workspaceNameValue)) + return null; + + if (!workspaceNameValue.TryRead(out var workspaceName) || string.IsNullOrWhiteSpace(workspaceName)) + { + LOGGER.LogWarning("The WorkspaceName of chat template {IdxChatTemplate} is not a valid name and will be ignored.", idx); + return null; + } + + return workspaceName.Trim(); + } + private static DataSourceOptions? ParseDataSourceOptions(int idx, LuaTable table) { if (!table.TryGetValue("DataSourceOptions", out var optionsValue) || !optionsValue.TryRead(out var optionsTable)) @@ -449,22 +546,25 @@ public record ChatTemplate( 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: + // These may be absent entirely, because saying nothing about the profile, the provider, the + // workspace, the tools, or the 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 profileIdLua = BuildOptionalStringLua("ProfileId", this.ProfileId); + var providerIdLua = BuildOptionalStringLua("ProviderId", this.ProviderId); + var workspaceNameLua = BuildOptionalStringLua("WorkspaceName", this.WorkspaceName); var toolIdsLua = this.BuildToolIdsLua(); var dataSourceOptionsLua = this.BuildDataSourceOptionsLua(); luaCode = $$""" - {{this.BuildDataSourceIdNote()}}CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { + {{this.BuildProviderAndProfileIdNote()}}{{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}},{{toolIdsLua}}{{dataSourceOptionsLua}} + ["ExampleConversation"] = {{exampleConversationLua}},{{profileIdLua}}{{providerIdLua}}{{workspaceNameLua}}{{toolIdsLua}}{{dataSourceOptionsLua}} } """; return true; @@ -574,6 +674,10 @@ public record ChatTemplate( return true; } + private static string BuildOptionalStringLua(string fieldName, string? value) => value is null + ? string.Empty + : $"{Environment.NewLine} [\"{fieldName}\"] = {LuaTools.ToLuaStringLiteral(value)},"; + /// /// 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 @@ -653,6 +757,26 @@ public record ChatTemplate( """; } + /// + /// The same holds for the provider and the profile: a configuration plugin reaches only those + /// which it or another plugin defines, and the ones of this machine are mostly the user's own. + /// Starting without a profile names nothing, so it needs no note. + /// + private string BuildProviderAndProfileIdNote() + { + var namesProfile = this.ProfileId is not null && !this.StartsWithoutProfile(); + if (this.ProviderId is null && !namesProfile) + return string.Empty; + + return """ + -- The provider and profile IDs below are the ones of the machine this was exported from. + -- Please check them against your CONFIG["LLM_PROVIDERS"] and CONFIG["PROFILES"]: an ID + -- which resolves to nothing is ignored, and a chat with this template then starts with + -- the provider or profile it would start with anyway. + + """; + } + private string BuildFileAttachmentsLua(IReadOnlyList? fileAttachmentPaths) { var paths = fileAttachmentPaths ?? this.FileAttachments.Select(attachment => attachment.FilePath).ToList(); diff --git a/app/MindWork AI Studio/Settings/ChatTemplateDataSourceChoice.cs b/app/MindWork AI Studio/Settings/ChatTemplateDataSourceChoice.cs new file mode 100644 index 000000000..02968c0be --- /dev/null +++ b/app/MindWork AI Studio/Settings/ChatTemplateDataSourceChoice.cs @@ -0,0 +1,16 @@ +using AIStudio.Settings.DataModel; + +namespace AIStudio.Settings; + +/// +/// Whose data source options a chat started by a launcher begins with, decided by ChatTemplate.ChooseDataSourceOptions. +/// +/// The options to start with, or null when neither side names any, which leaves the chat default in place. +/// Whether the data sources the launcher names itself were dropped for the options of the chat template. +public readonly record struct ChatTemplateDataSourceChoice(DataSourceOptions? Options, bool LauncherChoiceDropped) +{ + /// + /// Neither side names any data sources, so the chat keeps its default options. + /// + public static readonly ChatTemplateDataSourceChoice NONE = new(null, false); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ChatTemplateToolChoice.cs b/app/MindWork AI Studio/Settings/ChatTemplateToolChoice.cs new file mode 100644 index 000000000..eb44db657 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ChatTemplateToolChoice.cs @@ -0,0 +1,14 @@ +namespace AIStudio.Settings; + +/// +/// Whose tools a chat started by a launcher begins with, decided by ChatTemplate.ChooseToolIds. +/// +/// The tools to start with, or null when neither side names any, which leaves the chat default in place. +/// Whether the tools the launcher names itself were dropped for those of the chat template. +public readonly record struct ChatTemplateToolChoice(IReadOnlyCollection? ToolIds, bool LauncherChoiceDropped) +{ + /// + /// Neither side names any tools, so the chat keeps its default. + /// + public static readonly ChatTemplateToolChoice NONE = new(null, false); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index ead36738b..15010d697 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -877,6 +877,149 @@ public sealed class SettingsManager return this.GetChatTemplateById(this.ConfigurationData.App.PreselectedChatTemplate); } + /// + /// Returns the profile a chat template preselects for the chats started with it. + /// + /// + /// Null whenever the template has nothing to say which could be used: it names no profile, it + /// does not allow profiles, or the profile it names is gone. The chat then keeps the profile it + /// would have without the template. Starting without a profile is an answer of its own, and it + /// comes back as Profile.NO_PROFILE. + /// + /// The chat template of the chat about to start. + /// The profile to start with, or null to keep the usual one. + public Profile? GetChatTemplateProfile(ChatTemplate chatTemplate) + { + if (!chatTemplate.AllowProfileUsage || chatTemplate.ProfileId is null) + return null; + + if (chatTemplate.StartsWithoutProfile()) + return Profile.NO_PROFILE; + + var profile = this.GetProfileById(chatTemplate.ProfileId); + return profile == Profile.NO_PROFILE ? null : profile; + } + + /// + /// Returns the profile a chat started with the given chat template begins with. + /// + /// + /// Null when the template says nothing about profiles, so the chat keeps the profile it has. + /// A template whose profile is gone starts its chats with the default profile of the chat + /// settings, which may be none — not with whatever profile the previous chat happened to use, + /// which would be a choice nobody made for this chat. + /// + /// The chat template of the chat about to start. + /// The profile to start with, or null to keep the current one. + public Profile? GetChatTemplateStartProfile(ChatTemplate chatTemplate) + { + if (!chatTemplate.AllowProfileUsage || chatTemplate.ProfileId is null) + return null; + + return this.GetChatTemplateProfile(chatTemplate) ?? this.GetPreselectedProfile(Tools.Components.CHAT); + } + + /// + /// Returns the provider a chat template preselects for the chats started with it. + /// + /// + /// The template is no way around the confidence levels: a provider which may not be used for + /// chats comes back as Provider.NONE, just like one which is gone or one the + /// template does not name at all. The chat then keeps the provider it would have without the + /// template. + /// + /// The chat template of the chat about to start. + /// The provider to start with, or Provider.NONE to keep the usual one. + public Provider GetChatTemplateProvider(ChatTemplate chatTemplate) => this.GetChatTemplateProvider(chatTemplate.ProviderId); + + /// + /// Returns the provider a chat template may preselect under the given ID. + /// + /// + /// The one rule for the provider of a chat template: it exists and meets the confidence level + /// required for chats. The chat, assistant chat launchers, the chat template dialog, and the + /// import of a chat template all ask here, so that none of them accepts a provider another + /// one would leave out. + /// + /// The ID the chat template names, or null when it names none. + /// The provider, or Provider.NONE when the ID names no provider a chat may use. + public Provider GetChatTemplateProvider(string? providerId) + { + if (string.IsNullOrWhiteSpace(providerId)) + return Provider.NONE; + + var provider = this.GetProviderById(providerId); + return provider != Provider.NONE && this.IsProviderConfident(provider, Tools.Components.CHAT) ? provider : Provider.NONE; + } + + /// + /// Returns the profile a chat begins with when it starts with the given chat template now. + /// + /// + /// A template which names a profile decides, see GetChatTemplateStartProfile. One + /// which says nothing keeps the current profile, with one exception: when that profile is the one + /// the previous template preselected, the chat starts with the default profile of the chat + /// settings instead. Keeping it would carry the choice of another template over into this chat. + /// + /// The chat template of the chat about to start. + /// The chat template the current profile was chosen with; the same one when the template does not change. + /// The profile the chat has right now. + /// The profile to start with. + public Profile ChooseChatTemplateProfile(ChatTemplate chatTemplate, ChatTemplate previousChatTemplate, Profile currentProfile) + { + if (this.GetChatTemplateStartProfile(chatTemplate) is { } templateProfile) + return templateProfile; + + if (this.GetChatTemplateStartProfile(previousChatTemplate) is { } previousTemplateProfile && previousTemplateProfile.Id == currentProfile.Id) + return this.GetPreselectedProfile(Tools.Components.CHAT); + + return currentProfile; + } + + /// + /// Returns the provider a chat begins with when it starts with the given chat template now. + /// + /// + /// The same as for the profile, see ChooseChatTemplateProfile: a template which + /// names a usable provider decides, and the provider of the previous template is not carried + /// over. In its place comes the default provider of the chat settings. Without such a default, + /// the current provider stays, so that the chat does not lose its provider altogether. + /// + /// The chat template of the chat about to start. + /// The chat template the current provider was chosen with; the same one when the template does not change. + /// The provider the chat has right now. + /// The provider to start with. + public Provider ChooseChatTemplateProvider(ChatTemplate chatTemplate, ChatTemplate previousChatTemplate, Provider currentProvider) + { + var templateProvider = this.GetChatTemplateProvider(chatTemplate); + if (templateProvider != Provider.NONE) + return templateProvider; + + var previousTemplateProvider = this.GetChatTemplateProvider(previousChatTemplate); + if (previousTemplateProvider == Provider.NONE || previousTemplateProvider.Id != currentProvider.Id) + return currentProvider; + + var defaultProvider = this.GetPreselectedProvider(Tools.Components.CHAT); + return defaultProvider == Provider.NONE ? currentProvider : defaultProvider; + } + + /// + /// Returns the name of the workspace a chat template keeps the chats started with it in. + /// + /// + /// Null when the template names none, and also while workspaces are switched off: a chat then + /// belongs to no workspace, whatever its template says. + /// + /// The chat template of the chat about to start. + /// The trimmed name of the workspace, or null to keep the chat where it would go anyway. + public string? GetChatTemplateWorkspaceName(ChatTemplate chatTemplate) + { + if (string.IsNullOrWhiteSpace(chatTemplate.WorkspaceName) || this.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.DISABLE_WORKSPACES) + return null; + + return chatTemplate.WorkspaceName.Trim(); + } + public Profile GetProfileById(string? profileId) { if (string.IsNullOrWhiteSpace(profileId)) diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationSnippetImport.cs b/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationSnippetImport.cs index a3e5dca60..832dbedf9 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationSnippetImport.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationSnippetImport.cs @@ -148,6 +148,20 @@ public static class ConfigurationSnippetImport if (missingChatToolIds.Count > 0) hints.Add(string.Format(TB("The chat template preselects tools which are not available on this computer: {0}."), string.Join(", ", missingChatToolIds))); + if (template.ProviderId is not null) + { + var templateProvider = settingsManager.GetProviderById(template.ProviderId); + if (templateProvider == Settings.Provider.NONE) + hints.Add(string.Format(TB("The chat template preselects a provider which does not exist on this computer: {0}. Chats started with it use your usual provider instead."), template.ProviderId)); + else if (settingsManager.GetChatTemplateProvider(template.ProviderId) == Settings.Provider.NONE) + hints.Add(string.Format(TB("The chat template preselects the provider '{0}', which does not meet the confidence level required for chats. Chats started with it use your usual provider instead."), templateProvider.InstanceName)); + } + + if (template.ProfileId is not null && !template.AllowProfileUsage) + hints.Add(TB("The chat template preselects a profile but does not allow profiles. Chats started with it ignore that profile.")); + else if (template.ProfileId is not null && !template.StartsWithoutProfile() && settingsManager.GetProfileById(template.ProfileId) == Profile.NO_PROFILE) + hints.Add(string.Format(TB("The chat template preselects a profile which does not exist on this computer: {0}. Chats started with it use your usual profile instead."), template.ProfileId)); + return result with { ConfigurationObject = template with { ToolIds = templateToolIds }, Hints = hints }; case DataDocumentAnalysisPolicy policy: diff --git a/app/MindWork AI Studio/Tools/Services/ChatLauncherDataSourcesResult.cs b/app/MindWork AI Studio/Tools/Services/ChatLauncherDataSourcesResult.cs new file mode 100644 index 000000000..843654453 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/ChatLauncherDataSourcesResult.cs @@ -0,0 +1,10 @@ +using AIStudio.Settings.DataModel; + +namespace AIStudio.Tools.Services; + +/// +/// The data source options a chat started by an assistant chat launcher begins with. +/// +/// The checked options of the chat, or null when the chosen data sources cannot be used. +/// Why no chat can be created with the chosen data sources, or an empty string when it can. +internal readonly record struct ChatLauncherDataSourcesResult(DataSourceOptions? Options, string ErrorMessage); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/ChatLauncherProfileResult.cs b/app/MindWork AI Studio/Tools/Services/ChatLauncherProfileResult.cs new file mode 100644 index 000000000..35171008b --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/ChatLauncherProfileResult.cs @@ -0,0 +1,10 @@ +using AIStudio.Settings; + +namespace AIStudio.Tools.Services; + +/// +/// The profile a chat started by an assistant chat launcher begins with. +/// +/// The profile of the chat, or null when the profile the launcher names does not exist. +/// Why no profile could be chosen, or an empty string when one was. +internal readonly record struct ChatLauncherProfileResult(Profile? Profile, string ErrorMessage); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/ChatLauncherProviderResult.cs b/app/MindWork AI Studio/Tools/Services/ChatLauncherProviderResult.cs new file mode 100644 index 000000000..906611477 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/ChatLauncherProviderResult.cs @@ -0,0 +1,11 @@ +using ProviderSettings = AIStudio.Settings.Provider; + +namespace AIStudio.Tools.Services; + +/// +/// The provider a chat started by an assistant chat launcher begins with. +/// +/// The provider of the chat, or NONE when there is none to start with. +/// Whether the chat template or the launcher named this provider, as opposed to the chat defaults. +/// Why the named provider cannot be used, or an empty string when it can. +internal readonly record struct ChatLauncherProviderResult(ProviderSettings Provider, bool IsExplicit, string ErrorMessage); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/ChatLauncherTemplateResult.cs b/app/MindWork AI Studio/Tools/Services/ChatLauncherTemplateResult.cs new file mode 100644 index 000000000..344fb29f5 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/ChatLauncherTemplateResult.cs @@ -0,0 +1,10 @@ +using AIStudio.Settings; + +namespace AIStudio.Tools.Services; + +/// +/// The chat template a chat started by an assistant chat launcher begins with. +/// +/// The chat template of the chat, or null when the template the launcher names does not exist. +/// Why no chat template could be chosen, or an empty string when one was. +internal readonly record struct ChatLauncherTemplateResult(ChatTemplate? ChatTemplate, string ErrorMessage); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/DirectChatService.cs b/app/MindWork AI Studio/Tools/Services/DirectChatService.cs index 45c1c1a42..b660874f2 100644 --- a/app/MindWork AI Studio/Tools/Services/DirectChatService.cs +++ b/app/MindWork AI Studio/Tools/Services/DirectChatService.cs @@ -17,20 +17,25 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc if (assistantPlugin.ChatLaunchConfiguration is not { } launchConfiguration) return new(null, TB("The assistant plugin does not contain a valid chat launch configuration.")); - var providerResult = this.ResolveProvider(launchConfiguration.ProviderId); - if (providerResult.IsExplicit && providerResult.Provider == ProviderSettings.NONE) - return new(null, providerResult.ErrorMessage); - - var profileResult = this.ResolveProfile(launchConfiguration.ProfileId); - var profile = profileResult.Profile; - if (profile is null) - return new(null, profileResult.ErrorMessage); - + // + // The chat template comes first, because it may preselect the provider and the profile as + // well. When it does, it wins over the launcher — the same rule as for the tools and the + // data sources, so that nobody has to remember two of them: + // var chatTemplateResult = this.ResolveChatTemplate(launchConfiguration.ChatTemplateId); var chatTemplate = chatTemplateResult.ChatTemplate; if (chatTemplate is null) return new(null, chatTemplateResult.ErrorMessage); + var providerResult = this.ResolveProvider(assistantPlugin.Name, chatTemplate, launchConfiguration.ProviderId); + if (providerResult.IsExplicit && providerResult.Provider == ProviderSettings.NONE) + return new(null, providerResult.ErrorMessage); + + var profileResult = this.ResolveProfile(assistantPlugin.Name, chatTemplate, launchConfiguration.ProfileId); + var profile = profileResult.Profile; + if (profile is null) + return new(null, profileResult.ErrorMessage); + // // A chat template that forbids profiles wins over a configured profile: the chat disables // its profile selection for such templates, so keeping the profile would pin one that the @@ -52,8 +57,10 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc // // A launcher that names no workspace wants the same chat the chat page starts on its own: - // one that belongs nowhere, is kept among the temporary chats, and disappears with them. The - // empty workspace ID is what says so, here as everywhere else in the app. + // one that belongs nowhere, is kept among the temporary chats, and disappears with them — + // unless its chat template names a workspace, which then fills in, just like on the chat + // page. A workspace the launcher names itself was chosen on purpose and wins. The empty + // workspace ID is what says "no workspace", here as everywhere else in the app. // var workspaceId = Guid.Empty; if (!launchConfiguration.OpensTemporaryChat) @@ -74,6 +81,11 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc return new(null, string.Format(TB("The workspace '{0}' could not be opened or created."), launchConfiguration.WorkspaceName)); } } + else if (settingsManager.GetChatTemplateWorkspaceName(chatTemplate) is { } templateWorkspaceName) + { + // A workspace of the template which cannot be used does not stop the launch, the same as on the chat page: + workspaceId = await WorkspaceBehaviour.ResolveOrCreateChatTemplateWorkspaceAsync(templateWorkspaceName, chatTemplate.GetSafeName()); + } var toolChoice = ChatTemplate.ChooseToolIds(chatTemplate, launchConfiguration.ToolIds); if (toolChoice.LauncherChoiceDropped) @@ -115,7 +127,34 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc return new(new(chatThread, ApplySelectedChatTemplateToComposer: true, PreserveDataSourceOptions: dataSourcesWereChosen), string.Empty); } - private (ProviderSettings Provider, bool IsExplicit, string ErrorMessage) ResolveProvider(Guid? providerId) + /// + /// A provider of the chat template which is gone, or which may not be used for chats, does not + /// stop the launch: the chat then starts as if the template named none, so the launcher decides + /// after all. Only what the launcher names itself is an error when it cannot be used, because + /// that is the one choice its author can fix in the launcher. + /// + internal ChatLauncherProviderResult ResolveProvider(string pluginName, ChatTemplate chatTemplate, Guid? launcherProviderId) + { + var templateProvider = settingsManager.GetChatTemplateProvider(chatTemplate); + if (templateProvider != ProviderSettings.NONE) + { + if (launcherProviderId is not null) + logger.LogWarning( + "Assistant plugin '{PluginName}' selects the provider '{LauncherProviderId}', but its chat template '{ChatTemplateName}' preselects a provider of its own. The chat starts with the provider of that template.", + pluginName, launcherProviderId, chatTemplate.GetSafeName()); + + return new(templateProvider, true, string.Empty); + } + + if (chatTemplate.ProviderId is not null) + logger.LogWarning( + "The chat template '{ChatTemplateName}' preselects the provider '{TemplateProviderId}', which does not exist or may not be used for chats. The chat starts as if the template named no provider.", + chatTemplate.GetSafeName(), chatTemplate.ProviderId); + + return this.ResolveLauncherProvider(launcherProviderId); + } + + private ChatLauncherProviderResult ResolveLauncherProvider(Guid? providerId) { // // The launcher does not name a provider, so it wants the chat defaults. We resolve them @@ -140,7 +179,32 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc return new(provider, true, string.Empty); } - private (Profile? Profile, string ErrorMessage) ResolveProfile(Guid? profileId) + /// + /// The same as for the provider: a profile of the chat template which is gone leaves the choice + /// to the launcher. A template which does not allow profiles names none here, and the launch + /// below drops the profile of the launcher for it as well. + /// + internal ChatLauncherProfileResult ResolveProfile(string pluginName, ChatTemplate chatTemplate, Guid? launcherProfileId) + { + if (settingsManager.GetChatTemplateProfile(chatTemplate) is { } templateProfile) + { + if (launcherProfileId is not null) + logger.LogWarning( + "Assistant plugin '{PluginName}' selects the profile '{LauncherProfileId}', but its chat template '{ChatTemplateName}' preselects a profile of its own. The chat starts with the profile of that template.", + pluginName, launcherProfileId, chatTemplate.GetSafeName()); + + return new(templateProfile, string.Empty); + } + + if (chatTemplate.AllowProfileUsage && chatTemplate.ProfileId is not null) + logger.LogWarning( + "The chat template '{ChatTemplateName}' preselects the profile '{TemplateProfileId}', which does not exist. The chat starts as if the template named no profile.", + chatTemplate.GetSafeName(), chatTemplate.ProfileId); + + return this.ResolveLauncherProfile(launcherProfileId); + } + + private ChatLauncherProfileResult ResolveLauncherProfile(Guid? profileId) { if (profileId is null) return new(settingsManager.GetPreselectedProfile(Components.CHAT), string.Empty); @@ -159,7 +223,7 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc : new(profile, string.Empty); } - private (ChatTemplate? ChatTemplate, string ErrorMessage) ResolveChatTemplate(Guid? chatTemplateId) + private ChatLauncherTemplateResult ResolveChatTemplate(Guid? chatTemplateId) { if (chatTemplateId is null) return new(settingsManager.GetPreselectedChatTemplate(Components.CHAT), string.Empty); @@ -178,7 +242,7 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc : new(chatTemplate, string.Empty); } - private async Task<(DataSourceOptions? Options, string ErrorMessage)> ResolveDataSourceOptionsAsync(PluginAssistants assistantPlugin, ProviderSettings provider, ChatTemplate chatTemplate, IReadOnlyList? launcherDataSourceIds) + private async Task ResolveDataSourceOptionsAsync(PluginAssistants assistantPlugin, ProviderSettings provider, ChatTemplate chatTemplate, IReadOnlyList? launcherDataSourceIds) { // // The launcher names data sources as plain IDs, and the options around them are always the @@ -225,8 +289,17 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc /// 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) + internal async Task CheckChosenDataSourcesAsync(ProviderSettings provider, DataSourceOptions chosenOptions, ChatTemplate? originChatTemplate) { + // + // Without the RAG preview, the chat uses no data sources at all, so a source which is gone + // or not permitted takes nothing from it. Stopping the launch for it would block a launcher + // over something the user can neither see nor change. The options stay in the chat as they + // are, just like a chat template keeps them, and count once RAG is switched on again. + // + if (!PreviewFeatures.PRE_RAG_2024.IsEnabled(settingsManager)) + return new(chosenOptions, string.Empty); + // // 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 diff --git a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs index 02920f974..4f18223ec 100644 --- a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs +++ b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs @@ -737,6 +737,36 @@ public static partial class WorkspaceBehaviour } } + /// + /// Opens or creates the workspace a chat template keeps its chats in, by its name. + /// + /// + /// Both the chat page and assistant chat launchers come here, so that a chat finds the workspace + /// of its template the same way wherever it starts. Unlike a workspace a launcher names itself, + /// a failure stops nothing: like everything else a chat template names, a workspace which cannot + /// be used leaves the chat to start as it would without the template, i.e., without a workspace. + /// The failure is logged here, once, so the callers need no handling of their own. + /// + /// The name of the workspace, as the chat template names it. + /// The name of the chat template, for the log. + /// The ID of the workspace, or Guid.Empty when it can be neither opened nor created. + public static async Task ResolveOrCreateChatTemplateWorkspaceAsync(string workspaceName, string chatTemplateName) + { + try + { + var workspaceId = await ResolveOrCreateWorkspaceIdByNameAsync(workspaceName); + if (workspaceId == Guid.Empty) + LOG.LogWarning("The workspace '{WorkspaceName}' of the chat template '{ChatTemplateName}' could not be opened or created. The chat starts without a workspace.", workspaceName, chatTemplateName); + + return workspaceId; + } + catch (Exception exception) + { + LOG.LogError(exception, "The workspace '{WorkspaceName}' of the chat template '{ChatTemplateName}' could not be opened or created. The chat starts without a workspace.", workspaceName, chatTemplateName); + return Guid.Empty; + } + } + public static async Task TryCreateWorkspaceAsync(string workspaceName) { var normalizedWorkspaceName = NormalizeWorkspaceName(workspaceName); diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md index d4217e936..46cb24bb9 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md @@ -24,6 +24,8 @@ - Added a choice of how AI Studio encrypts API keys, tokens, and passwords when you export providers, data sources, or tool settings for your organization. The new protected format needs AI Studio v26.10.1 or newer on every computer that reads it, and your encryption secret stays the same. If you used AI Studio before, your exports keep the previous format until you switch, so your organization can update all computers first. You find the choice in the app settings, under Enterprise Administration. - Added organization-wide management for the format in which exports encrypt API keys, tokens, and passwords. IT departments can set it for all their administrators, either fixed or as a default which users may change. - Added a way for IT departments to ask you for your personal API key on the start page. When the configuration of your organization brings providers that need your own key, the start page explains how to get it, checks the key with the provider, and stores it for all of these providers at once. Thanks, Dominic Neuburg (`donework`), for this contribution. +- Added providers, profiles, and workspaces to your chat templates. A template can now decide which provider, and with it which model, and which profile a chat starts with, and keep its chats in a workspace of its own. A template for your mails, for example, can start every chat with a provider that meets the confidence level your mailbox requires. You can still pick another provider or profile in the chat. +- Added a way for IT departments to roll out chat templates that bring their own provider, profile, and workspace. Set the template up in the app, then export it as ready-made Lua code for your configuration plugin. - Improved the Read Web Page tool. When you have not allowed the AI to choose web addresses freely, the tool now refuses every address that does not appear in your chat or in what the tools returned, instead of only asking the AI not to read it. - Improved the installation on macOS: AI Studio is now signed and checked by Apple, so it opens like any other app. You no longer need the Terminal. - Improved the installation on Windows: AI Studio is now signed, so Windows shows who published it. For a while, Windows may still ask you to confirm the installation until it knows the app better. @@ -95,6 +97,8 @@ - Fixed a deleted workspace coming back as "Unnamed workspace" when one of its chats was still open or being answered. AI Studio now deletes a workspace only when none of its chats is being answered or transcribed, and it closes the chat you had open in it. - Fixed a chat you started by attaching an audio or video file getting a German name, regardless of the language you use AI Studio in. - Fixed AI Studio sometimes warning about unsaved changes when you left a chat in which you had only selected a chat template, without writing anything yourself. +- Fixed canceling the question about unsaved changes after you picked another chat template: the open chat stayed, but it showed the new template, and your message held the text and attachments of that template. Now everything stays as it was. +- Fixed a tile that opens a chat directly refusing to open because of a missing data source while the RAG preview is switched off. Without RAG, a chat uses no data sources, so the tile now opens anyway, and the Assistant Builder no longer offers data sources for it. - Fixed regenerating an answer clearing the message you had started to write. - Fixed AI Studio waiting for an answer you had removed while it was still being written, so you could not send another message. While an answer is being written, the messages, attachments, provider, profile, and data sources of the chat now stay as they are until you stop the answer or it is complete. Thanks, Bernhard, for reporting this issue. - Fixed the warning about unsaved changes in a chat appearing only by chance when you save your chats manually or have turned off workspaces. AI Studio now reliably asks before you open another chat, create a new one, or leave the chat page, so you no longer lose your changes by accident. diff --git a/app/Tests/Settings/ChatTemplateConfigurationTests.cs b/app/Tests/Settings/ChatTemplateConfigurationTests.cs index ebe1170a0..60a1b2cf1 100644 --- a/app/Tests/Settings/ChatTemplateConfigurationTests.cs +++ b/app/Tests/Settings/ChatTemplateConfigurationTests.cs @@ -7,8 +7,9 @@ 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. +/// Checks the profile, provider, workspace, 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 @@ -88,6 +89,9 @@ public sealed class ChatTemplateConfigurationTests { Assert.That(luaCode, Does.Not.Contain("ToolIds")); Assert.That(luaCode, Does.Not.Contain("DataSourceOptions")); + Assert.That(luaCode, Does.Not.Contain("ProviderId")); + Assert.That(luaCode, Does.Not.Contain("ProfileId")); + Assert.That(luaCode, Does.Not.Contain("WorkspaceName")); }); var read = await ParseAsync(luaCode); @@ -189,6 +193,140 @@ public sealed class ChatTemplateConfigurationTests }); } + [Test] + public async Task APreselectedProviderAndProfileSurviveTheExport() + { + var written = NewTemplate() with + { + ProviderId = "44444444-4444-4444-4444-444444444444", + ProfileId = "55555555-5555-5555-5555-555555555555", + }; + + var read = await ExportAndReadBackAsync(written); + + Assert.Multiple(() => + { + Assert.That(read.ProviderId, Is.EqualTo(written.ProviderId)); + Assert.That(read.ProfileId, Is.EqualTo(written.ProfileId)); + }); + } + + [Test] + public async Task StartingWithoutAProfileIsAStatementOfItsOwn() + { + var read = await ExportAndReadBackAsync(NewTemplate() with { ProfileId = Profile.NO_PROFILE.Id }); + var silent = await ExportAndReadBackAsync(NewTemplate()); + + Assert.Multiple(() => + { + Assert.That(read.ProfileId, Is.EqualTo(Profile.NO_PROFILE.Id), "Reading this back as null would hand out the default profile the template ruled out."); + Assert.That(silent.ProfileId, Is.Null); + Assert.That(silent.ProviderId, Is.Null); + }); + } + + [Test] + public async Task APluginTemplateMayNameTheProviderAndProfileOfItsPlugin() + { + var read = await ParseAsync(""" + CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { + ["Id"] = "33333333-3333-3333-3333-333333333333", + ["Name"] = "Mail", + ["SystemPrompt"] = "You help with mails.", + ["AllowProfileUsage"] = true, + ["ProviderId"] = "44444444-4444-4444-4444-444444444444", + ["ProfileId"] = "55555555-5555-5555-5555-555555555555", + } + """); + + Assert.Multiple(() => + { + Assert.That(read.ProviderId, Is.EqualTo("44444444-4444-4444-4444-444444444444")); + Assert.That(read.ProfileId, Is.EqualTo("55555555-5555-5555-5555-555555555555")); + }); + } + + [TestCase("\"\"")] + [TestCase("\"no GUID\"")] + [TestCase("42")] + [TestCase("\"00000000-0000-0000-0000-000000000000\"")] + public async Task AProviderIdWhichNamesNoProviderIsDropped(string luaValue) + { + var read = await ParseAsync($$""" + CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { + ["Id"] = "33333333-3333-3333-3333-333333333333", + ["Name"] = "Mail", + ["SystemPrompt"] = "You help with mails.", + ["ProviderId"] = {{luaValue}}, + } + """); + + Assert.That(read.ProviderId, Is.Null, "The template still works then, with the provider its chats would start with anyway."); + } + + [Test] + public async Task AWorkspaceNameSurvivesTheExport() + { + var written = NewTemplate() with { WorkspaceName = "Mails \"inbox\"" }; + var read = await ExportAndReadBackAsync(written); + var silent = await ExportAndReadBackAsync(NewTemplate()); + + Assert.Multiple(() => + { + Assert.That(read.WorkspaceName, Is.EqualTo(written.WorkspaceName)); + Assert.That(silent.WorkspaceName, Is.Null, "A template without a workspace must keep leaving the chat where it would go anyway."); + }); + } + + [TestCase("\"\"")] + [TestCase("\" \"")] + [TestCase("42")] + [TestCase("{}")] + public async Task AWorkspaceNameWhichNamesNothingIsDropped(string luaValue) + { + var read = await ParseAsync($$""" + CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { + ["Id"] = "33333333-3333-3333-3333-333333333333", + ["Name"] = "Mail", + ["SystemPrompt"] = "You help with mails.", + ["WorkspaceName"] = {{luaValue}}, + } + """); + + Assert.That(read.WorkspaceName, Is.Null); + } + + [Test] + public async Task APluginTemplateNamesItsWorkspaceWithoutTheSpacesAround() + { + var read = await ParseAsync(""" + CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { + ["Id"] = "33333333-3333-3333-3333-333333333333", + ["Name"] = "Mail", + ["SystemPrompt"] = "You help with mails.", + ["WorkspaceName"] = " Mails ", + } + """); + + Assert.That(read.WorkspaceName, Is.EqualTo("Mails"), "Workspaces are matched by their trimmed name, so the spaces would only make the field look different from the name in the list."); + } + + [Test] + public void ExportedProviderAndProfileIdsComeWithTheNoteThatTheyAreLocalOnes() + { + var withProvider = NewTemplate() with { ProviderId = "44444444-4444-4444-4444-444444444444" }; + var withoutProfile = NewTemplate() with { ProfileId = Profile.NO_PROFILE.Id }; + + Assert.That(withProvider.TryExportAsConfigurationSection(out var withProviderLua, out var issue), Is.True, issue); + Assert.That(withoutProfile.TryExportAsConfigurationSection(out var withoutProfileLua, out issue), Is.True, issue); + + Assert.Multiple(() => + { + Assert.That(withProviderLua, Does.StartWith("--"), "A provider of this machine is mostly the user's own, which no configuration plugin knows."); + Assert.That(withoutProfileLua, Does.Not.StartWith("--"), "Starting without a profile names nothing which could be missing."); + }); + } + [Test] public void ExportedDataSourceIdsComeWithTheNoteThatTheyAreLocalOnes() { diff --git a/app/Tests/Settings/ChatTemplateLauncherSelectionTests.cs b/app/Tests/Settings/ChatTemplateLauncherSelectionTests.cs new file mode 100644 index 000000000..a549644a1 --- /dev/null +++ b/app/Tests/Settings/ChatTemplateLauncherSelectionTests.cs @@ -0,0 +1,154 @@ +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Settings.DataModel; +using AIStudio.Tests.Tools.ToolCalling; +using AIStudio.Tools.Services; + +using Microsoft.Extensions.Logging.Abstractions; + +namespace AIStudio.Tests.Settings; + +/// +/// Checks who decides the provider and the profile when an assistant chat launcher and the chat +/// template it opens its chat with both name one. +/// +/// +/// The rule is the one for the tools and the data sources: the chat template wins. Whatever the +/// template names and cannot be used must not stop the launch, though, because the author of the +/// launcher could not fix it in the launcher. The launcher then decides as if the template named none. +/// The last tests cover the data sources, which only stop a launch while the chat can use them. +/// +[TestFixture] +[NonParallelizable] +public sealed class ChatTemplateLauncherSelectionTests : ToolRegistryTestBase +{ + private const string PLUGIN_NAME = "Mail assistant"; + private const string TEMPLATE_PROFILE_ID = "55555555-5555-5555-5555-555555555555"; + private static readonly Guid LAUNCHER_PROVIDER_ID = Guid.Parse("11111111-1111-1111-1111-111111111111"); + private static readonly Guid LAUNCHER_PROFILE_ID = Guid.Parse("22222222-2222-2222-2222-222222222222"); + + [Test] + public void TheTemplateWinsOverTheLauncher() + { + var templateProvider = ToolCapableProvider(); + this.SettingsManager.ConfigurationData.Providers.Add(templateProvider); + this.SettingsManager.ConfigurationData.Providers.Add(LauncherProvider()); + var templateProfile = this.AddProfile(TEMPLATE_PROFILE_ID); + this.AddProfile(LAUNCHER_PROFILE_ID.ToString()); + + var template = NewTemplate() with { ProviderId = templateProvider.Id, ProfileId = TEMPLATE_PROFILE_ID }; + var service = this.CreateService(); + var providerResult = service.ResolveProvider(PLUGIN_NAME, template, LAUNCHER_PROVIDER_ID); + var profileResult = service.ResolveProfile(PLUGIN_NAME, template, LAUNCHER_PROFILE_ID); + + Assert.Multiple(() => + { + Assert.That(providerResult.Provider, Is.EqualTo(templateProvider)); + Assert.That(providerResult.IsExplicit, Is.True); + Assert.That(profileResult.Profile, Is.EqualTo(templateProfile)); + }); + } + + [Test] + public void WhatTheTemplateNamesAndIsGoneLeavesTheChoiceToTheLauncher() + { + var launcherProvider = LauncherProvider(); + this.SettingsManager.ConfigurationData.Providers.Add(launcherProvider); + var launcherProfile = this.AddProfile(LAUNCHER_PROFILE_ID.ToString()); + + var template = NewTemplate() with { ProviderId = "66666666-6666-6666-6666-666666666666", ProfileId = TEMPLATE_PROFILE_ID }; + var service = this.CreateService(); + var providerResult = service.ResolveProvider(PLUGIN_NAME, template, LAUNCHER_PROVIDER_ID); + var profileResult = service.ResolveProfile(PLUGIN_NAME, template, LAUNCHER_PROFILE_ID); + + Assert.Multiple(() => + { + Assert.That(providerResult.Provider, Is.EqualTo(launcherProvider)); + Assert.That(providerResult.ErrorMessage, Is.Empty); + Assert.That(profileResult.Profile, Is.EqualTo(launcherProfile)); + Assert.That(profileResult.ErrorMessage, Is.Empty); + }); + } + + [Test] + public void ATemplateProviderBelowTheConfidenceOfTheChatDoesNotStopTheLaunch() + { + var templateProvider = LessTrustedProvider(); + this.SettingsManager.ConfigurationData.Providers.Add(templateProvider); + this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence = true; + this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence = ConfidenceLevel.HIGH; + + var template = NewTemplate() with { ProviderId = templateProvider.Id }; + var providerResult = this.CreateService().ResolveProvider(PLUGIN_NAME, template, null); + + Assert.Multiple(() => + { + Assert.That(providerResult.Provider, Is.Not.EqualTo(templateProvider), "A template must not open the way to a provider the user could not pick by hand."); + Assert.That(providerResult.IsExplicit, Is.False, "The launcher names no provider, so the chat starts with its defaults instead of failing."); + Assert.That(providerResult.ErrorMessage, Is.Empty); + }); + } + + [Test] + public void ATemplateWhichForbidsProfilesLeavesTheProfileToTheLauncher() + { + this.AddProfile(TEMPLATE_PROFILE_ID); + var launcherProfile = this.AddProfile(LAUNCHER_PROFILE_ID.ToString()); + + var template = NewTemplate() with { AllowProfileUsage = false, ProfileId = TEMPLATE_PROFILE_ID }; + var profileResult = this.CreateService().ResolveProfile(PLUGIN_NAME, template, LAUNCHER_PROFILE_ID); + + Assert.That(profileResult.Profile, Is.EqualTo(launcherProfile), "The launch drops the profile afterward for such a template; picking the one of the template here would only hide that."); + } + + [Test] + public async Task WithoutRagAMissingDataSourceDoesNotStopTheLaunch() + { + var options = new DataSourceOptions { DisableDataSources = false, PreselectedDataSourceIds = ["99999999-9999-9999-9999-999999999999"] }; + var result = await this.CreateService().CheckChosenDataSourcesAsync(ToolCapableProvider(), options, originChatTemplate: null); + + Assert.Multiple(() => + { + Assert.That(result.Options, Is.SameAs(options), "The chat uses no data sources without RAG, so the options travel along unchecked and count once RAG is back."); + Assert.That(result.ErrorMessage, Is.Empty); + }); + } + + [Test] + public async Task WithRagAMissingDataSourceStillStopsTheLaunch() + { + this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Add(PreviewFeatures.PRE_RAG_2024); + var options = new DataSourceOptions { DisableDataSources = false, PreselectedDataSourceIds = ["99999999-9999-9999-9999-999999999999"] }; + var result = await this.CreateService().CheckChosenDataSourcesAsync(ToolCapableProvider(), options, originChatTemplate: null); + + Assert.Multiple(() => + { + Assert.That(result.Options, Is.Null, "Opening a launcher is one click, so a missing source has to be said out loud."); + Assert.That(result.ErrorMessage, Is.Not.Empty); + }); + } + + // Data sources are not resolved by these tests beyond their lookup, so the service needs no data source service: + private DirectChatService CreateService() => new(this.SettingsManager, dataSourceService: null!, toolRegistry: this.CreateRegistry(), NullLogger.Instance); + + private static AIStudio.Settings.Provider LauncherProvider() => LessTrustedProvider() with { Id = LAUNCHER_PROVIDER_ID.ToString() }; + + private Profile AddProfile(string profileId) + { + var profile = new Profile((uint)this.SettingsManager.ConfigurationData.Profiles.Count + 1, profileId, $"Profile {profileId[..4]}", "I read a lot of mails.", "Answer briefly."); + this.SettingsManager.ConfigurationData.Profiles.Add(profile); + return profile; + } + + private static ChatTemplate NewTemplate() => new() + { + Num = 1, + Id = "33333333-3333-3333-3333-333333333333", + Name = "Mail", + SystemPrompt = "You help with mails.", + PredefinedUserPrompt = string.Empty, + ExampleConversation = [], + FileAttachments = [], + AllowProfileUsage = true, + }; +} \ No newline at end of file diff --git a/app/Tests/Settings/ChatTemplatePrecedenceTests.cs b/app/Tests/Settings/ChatTemplatePrecedenceTests.cs index 282b53c68..ea855756e 100644 --- a/app/Tests/Settings/ChatTemplatePrecedenceTests.cs +++ b/app/Tests/Settings/ChatTemplatePrecedenceTests.cs @@ -24,10 +24,8 @@ public sealed class ChatTemplatePrecedenceTests 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); + Assert.That(toolChoice, Is.EqualTo(ChatTemplateToolChoice.NONE), "Nobody named any tool, so the chat has to keep using the tools of its own default."); + Assert.That(optionsChoice, Is.EqualTo(ChatTemplateDataSourceChoice.NONE), "Nobody named any data source, so the chat has to keep using its own default options."); }); } diff --git a/app/Tests/Settings/ChatTemplateStartSelectionTests.cs b/app/Tests/Settings/ChatTemplateStartSelectionTests.cs new file mode 100644 index 000000000..ad3879141 --- /dev/null +++ b/app/Tests/Settings/ChatTemplateStartSelectionTests.cs @@ -0,0 +1,268 @@ +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Settings.DataModel; +using AIStudio.Tests.Tools.ToolCalling; + +namespace AIStudio.Tests.Settings; + +/// +/// Checks which profile and provider of a chat template a chat actually starts with. +/// +/// +/// A preselection must never get past what the user would be allowed to pick by hand: a profile the +/// template forbids, or a provider below the confidence level of the chat. And it must never stop a +/// chat either: whatever the template names and cannot be used leaves the chat to start as if the +/// template said nothing. +/// +[TestFixture] +[NonParallelizable] +public sealed class ChatTemplateStartSelectionTests : ToolRegistryTestBase +{ + private const string PROFILE_ID = "55555555-5555-5555-5555-555555555555"; + private const string DEFAULT_PROFILE_ID = "77777777-7777-7777-7777-777777777777"; + private const string OTHER_TEMPLATE_ID = "88888888-8888-8888-8888-888888888888"; + + [Test] + public void ATemplateWhichSaysNothingLeavesBothInPlace() + { + this.AddProfile(); + this.SettingsManager.ConfigurationData.Providers.Add(ToolCapableProvider()); + + Assert.Multiple(() => + { + Assert.That(this.SettingsManager.GetChatTemplateProfile(NewTemplate()), Is.Null); + Assert.That(this.SettingsManager.GetChatTemplateProvider(NewTemplate()), Is.EqualTo(AIStudio.Settings.Provider.NONE)); + }); + } + + [Test] + public void ATemplateHandsOutWhatItNames() + { + var profile = this.AddProfile(); + var provider = ToolCapableProvider(); + this.SettingsManager.ConfigurationData.Providers.Add(provider); + var template = NewTemplate() with { ProfileId = PROFILE_ID, ProviderId = provider.Id }; + + Assert.Multiple(() => + { + Assert.That(this.SettingsManager.GetChatTemplateProfile(template), Is.EqualTo(profile)); + Assert.That(this.SettingsManager.GetChatTemplateProvider(template), Is.EqualTo(provider)); + }); + } + + [Test] + public void StartingWithoutAProfileIsAnAnswerOfItsOwn() + { + var template = NewTemplate() with { ProfileId = Profile.NO_PROFILE.Id }; + + Assert.That(this.SettingsManager.GetChatTemplateProfile(template), Is.EqualTo(Profile.NO_PROFILE), "Null would keep the default profile, which is exactly what this template rules out."); + } + + [Test] + public void WhatIsGoneLeavesTheChatAsIfTheTemplateSaidNothing() + { + var template = NewTemplate() with { ProfileId = PROFILE_ID, ProviderId = "66666666-6666-6666-6666-666666666666" }; + + Assert.Multiple(() => + { + Assert.That(this.SettingsManager.GetChatTemplateProfile(template), Is.Null); + Assert.That(this.SettingsManager.GetChatTemplateProvider(template), Is.EqualTo(AIStudio.Settings.Provider.NONE)); + }); + } + + [Test] + public void AChatWhoseTemplateProfileIsGoneStartsWithTheDefaultProfile() + { + var template = NewTemplate() with { ProfileId = PROFILE_ID }; + + var withoutDefault = this.SettingsManager.GetChatTemplateStartProfile(template); + + var defaultProfile = new Profile(2, "77777777-7777-7777-7777-777777777777", "Default", "I am the default.", string.Empty); + this.SettingsManager.ConfigurationData.Profiles.Add(defaultProfile); + this.SettingsManager.ConfigurationData.Chat.PreselectOptions = true; + this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = defaultProfile.Id; + var withDefault = this.SettingsManager.GetChatTemplateStartProfile(template); + + Assert.Multiple(() => + { + Assert.That(withoutDefault, Is.EqualTo(Profile.NO_PROFILE), "Keeping the profile the chat had would hand out one the previous chat happened to use, a choice nobody made for this one."); + Assert.That(withDefault, Is.EqualTo(defaultProfile)); + }); + } + + [Test] + public void ATemplateWhichSaysNothingKeepsTheProfileAChatStartsWith() + { + this.AddProfile(); + + Assert.Multiple(() => + { + Assert.That(this.SettingsManager.GetChatTemplateStartProfile(NewTemplate()), Is.Null); + Assert.That(this.SettingsManager.GetChatTemplateStartProfile(NewTemplate() with { AllowProfileUsage = false, ProfileId = PROFILE_ID }), Is.Null); + Assert.That(this.SettingsManager.GetChatTemplateStartProfile(NewTemplate() with { ProfileId = PROFILE_ID })!.Id, Is.EqualTo(PROFILE_ID)); + }); + } + + [Test] + public void ATemplateWhichForbidsProfilesHandsOutNone() + { + this.AddProfile(); + var template = NewTemplate() with { AllowProfileUsage = false, ProfileId = PROFILE_ID }; + + Assert.That(this.SettingsManager.GetChatTemplateProfile(template), Is.Null, "The chat locks the profile selection for such a template, so a profile handed out here could neither be seen nor changed."); + } + + [Test] + public void AProviderBelowTheConfidenceOfTheChatIsNotHandedOut() + { + var provider = LessTrustedProvider(); + this.SettingsManager.ConfigurationData.Providers.Add(provider); + this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence = true; + this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence = ConfidenceLevel.HIGH; + + var template = NewTemplate() with { ProviderId = provider.Id }; + + Assert.That(this.SettingsManager.GetChatTemplateProvider(template), Is.EqualTo(AIStudio.Settings.Provider.NONE), "A template must not open the way to a provider the user could not pick by hand."); + } + + [Test] + public void AProviderIdFollowsTheSameRuleAsTheTemplateWhichNamesIt() + { + var trustedProvider = ToolCapableProvider(); + var lessTrustedProvider = LessTrustedProvider(); + this.SettingsManager.ConfigurationData.Providers.Add(trustedProvider); + this.SettingsManager.ConfigurationData.Providers.Add(lessTrustedProvider); + this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence = true; + this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence = ConfidenceLevel.HIGH; + + Assert.Multiple(() => + { + Assert.That(this.SettingsManager.GetChatTemplateProvider(trustedProvider.Id), Is.EqualTo(trustedProvider)); + Assert.That(this.SettingsManager.GetChatTemplateProvider(lessTrustedProvider.Id), Is.EqualTo(AIStudio.Settings.Provider.NONE), "The chat template dialog and the import warn about exactly the providers a chat leaves out."); + Assert.That(this.SettingsManager.GetChatTemplateProvider("66666666-6666-6666-6666-666666666666"), Is.EqualTo(AIStudio.Settings.Provider.NONE)); + Assert.That(this.SettingsManager.GetChatTemplateProvider((string?)null), Is.EqualTo(AIStudio.Settings.Provider.NONE)); + }); + } + + [Test] + public void ATemplateNamesItsWorkspaceOnlyWhileWorkspacesAreUsed() + { + var template = NewTemplate() with { WorkspaceName = " Mails " }; + + this.SettingsManager.ConfigurationData.Workspace.StorageBehavior = WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY; + var whileUsed = this.SettingsManager.GetChatTemplateWorkspaceName(template); + + this.SettingsManager.ConfigurationData.Workspace.StorageBehavior = WorkspaceStorageBehavior.DISABLE_WORKSPACES; + var whileSwitchedOff = this.SettingsManager.GetChatTemplateWorkspaceName(template); + + Assert.Multiple(() => + { + Assert.That(whileUsed, Is.EqualTo("Mails")); + Assert.That(whileSwitchedOff, Is.Null, "Without workspaces, a template must not bring one back for its chats."); + Assert.That(this.SettingsManager.GetChatTemplateWorkspaceName(NewTemplate()), Is.Null); + }); + } + + [Test] + public void WhatThePreviousTemplatePreselectedIsNotCarriedOver() + { + var templateProfile = this.AddProfile(); + var defaultProfile = this.AddDefaultProfile(); + var templateProvider = ToolCapableProvider(); + var defaultProvider = LessTrustedProvider(); + this.SettingsManager.ConfigurationData.Providers.Add(templateProvider); + this.SettingsManager.ConfigurationData.Providers.Add(defaultProvider); + this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = defaultProvider.Id; + + var previousTemplate = NewTemplate() with { ProfileId = PROFILE_ID, ProviderId = templateProvider.Id }; + var nextTemplate = NewTemplate() with { Id = OTHER_TEMPLATE_ID }; + + Assert.Multiple(() => + { + Assert.That(this.SettingsManager.ChooseChatTemplateProfile(nextTemplate, previousTemplate, templateProfile), Is.EqualTo(defaultProfile), "The profile was the choice of another template, not one made for this chat."); + Assert.That(this.SettingsManager.ChooseChatTemplateProvider(nextTemplate, previousTemplate, templateProvider), Is.EqualTo(defaultProvider), "The provider was the choice of another template, not one made for this chat."); + }); + } + + [Test] + public void AChoiceMadeByHandStaysWhenTheTemplateSaysNothing() + { + var handPickedProfile = this.AddProfile(); + var defaultProfile = this.AddDefaultProfile(); + var templateProvider = ToolCapableProvider(); + var handPickedProvider = LessTrustedProvider(); + this.SettingsManager.ConfigurationData.Providers.Add(templateProvider); + this.SettingsManager.ConfigurationData.Providers.Add(handPickedProvider); + + var previousTemplate = NewTemplate() with { ProfileId = defaultProfile.Id, ProviderId = templateProvider.Id }; + var nextTemplate = NewTemplate() with { Id = OTHER_TEMPLATE_ID }; + + Assert.Multiple(() => + { + Assert.That(this.SettingsManager.ChooseChatTemplateProfile(nextTemplate, previousTemplate, handPickedProfile), Is.EqualTo(handPickedProfile)); + Assert.That(this.SettingsManager.ChooseChatTemplateProvider(nextTemplate, previousTemplate, handPickedProvider), Is.EqualTo(handPickedProvider)); + Assert.That(this.SettingsManager.ChooseChatTemplateProfile(NewTemplate(), NewTemplate(), handPickedProfile), Is.EqualTo(handPickedProfile), "Without any preselection, nothing changes."); + }); + } + + [Test] + public void TheNewTemplateDecidesWhenItNamesBoth() + { + var templateProfile = this.AddProfile(); + var otherProfile = this.AddDefaultProfile(); + var templateProvider = ToolCapableProvider(); + var otherProvider = LessTrustedProvider(); + this.SettingsManager.ConfigurationData.Providers.Add(templateProvider); + this.SettingsManager.ConfigurationData.Providers.Add(otherProvider); + + var previousTemplate = NewTemplate() with { Id = OTHER_TEMPLATE_ID, ProfileId = otherProfile.Id, ProviderId = otherProvider.Id }; + var nextTemplate = NewTemplate() with { ProfileId = PROFILE_ID, ProviderId = templateProvider.Id }; + + Assert.Multiple(() => + { + Assert.That(this.SettingsManager.ChooseChatTemplateProfile(nextTemplate, previousTemplate, otherProfile), Is.EqualTo(templateProfile)); + Assert.That(this.SettingsManager.ChooseChatTemplateProvider(nextTemplate, previousTemplate, otherProvider), Is.EqualTo(templateProvider)); + }); + } + + [Test] + public void WithoutADefaultProviderTheChatKeepsItsProvider() + { + var templateProvider = ToolCapableProvider(); + this.SettingsManager.ConfigurationData.Providers.Add(templateProvider); + this.SettingsManager.ConfigurationData.Providers.Add(LessTrustedProvider()); + + var previousTemplate = NewTemplate() with { ProviderId = templateProvider.Id }; + var nextTemplate = NewTemplate() with { Id = OTHER_TEMPLATE_ID }; + + Assert.That(this.SettingsManager.ChooseChatTemplateProvider(nextTemplate, previousTemplate, templateProvider), Is.EqualTo(templateProvider), "A chat without any provider could not answer at all."); + } + + private Profile AddDefaultProfile() + { + var defaultProfile = new Profile(2, DEFAULT_PROFILE_ID, "Default", "I am the default.", string.Empty); + this.SettingsManager.ConfigurationData.Profiles.Add(defaultProfile); + this.SettingsManager.ConfigurationData.Chat.PreselectOptions = true; + this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = defaultProfile.Id; + return defaultProfile; + } + + private Profile AddProfile() + { + var profile = new Profile(1, PROFILE_ID, "Mail", "I read a lot of mails.", "Answer briefly."); + this.SettingsManager.ConfigurationData.Profiles.Add(profile); + return profile; + } + + private static ChatTemplate NewTemplate() => new() + { + Num = 1, + Id = "33333333-3333-3333-3333-333333333333", + Name = "Mail", + SystemPrompt = "You help with mails.", + PredefinedUserPrompt = string.Empty, + ExampleConversation = [], + FileAttachments = [], + AllowProfileUsage = true, + }; +} \ No newline at end of file diff --git a/app/Tests/Tools/PluginSystem/ConfigurationSnippetImportTests.cs b/app/Tests/Tools/PluginSystem/ConfigurationSnippetImportTests.cs index 7cb9a80e8..09db7ad9a 100644 --- a/app/Tests/Tools/PluginSystem/ConfigurationSnippetImportTests.cs +++ b/app/Tests/Tools/PluginSystem/ConfigurationSnippetImportTests.cs @@ -118,6 +118,9 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase Assert.That(imported.DataSourceOptions!.AutomaticDataSourceSelection, Is.EqualTo(template.DataSourceOptions!.AutomaticDataSourceSelection)); Assert.That(imported.DataSourceOptions.AutomaticValidation, Is.EqualTo(template.DataSourceOptions.AutomaticValidation)); Assert.That(imported.DataSourceOptions.PreselectedDataSourceIds, Is.EqualTo(template.DataSourceOptions.PreselectedDataSourceIds)); + Assert.That(imported.ProviderId, Is.EqualTo(template.ProviderId)); + Assert.That(imported.ProfileId, Is.EqualTo(template.ProfileId)); + Assert.That(imported.WorkspaceName, Is.EqualTo(template.WorkspaceName)); }); } @@ -417,14 +420,66 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase public async Task MissingReferencesOfAChatTemplateAreNamed() { var missingDataSourceId = Guid.NewGuid().ToString(); - var snippet = ExportChatTemplate([], toolIds: [TOOL_ID, "missing_tool"], dataSourceIds: [missingDataSourceId]); + var missingProviderId = Guid.NewGuid().ToString(); + var missingProfileId = Guid.NewGuid().ToString(); + var snippet = ExportChatTemplate([], toolIds: [TOOL_ID, "missing_tool"], dataSourceIds: [missingDataSourceId], providerId: missingProviderId, profileId: missingProfileId); var result = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(snippet, PluginConfigurationObjectType.CHAT_TEMPLATE), this.SettingsManager, this.CreateRegistry(new TestTool(Definition()))); Assert.Multiple(() => { - Assert.That(result.Hints, Has.Count.EqualTo(2), "One sentence for each kind of reference."); + Assert.That(result.Hints, Has.Count.EqualTo(4), "One sentence for each kind of reference."); Assert.That(result.Hints, Has.Exactly(1).Contains(missingDataSourceId)); Assert.That(result.Hints, Has.Exactly(1).Contains("missing_tool")); + Assert.That(result.Hints, Has.Exactly(1).Contains(missingProviderId)); + Assert.That(result.Hints, Has.Exactly(1).Contains(missingProfileId)); + }); + } + + [Test] + public async Task AChatTemplateWhichStartsWithoutAProfileNamesNothingMissing() + { + var snippet = ExportChatTemplate([], profileId: Profile.NO_PROFILE.Id); + var result = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(snippet, PluginConfigurationObjectType.CHAT_TEMPLATE), this.SettingsManager, this.CreateRegistry(new TestTool(Definition()))); + + Assert.Multiple(() => + { + Assert.That(result.Hints, Is.Empty, "Starting without a profile is a statement, not a reference to a profile which could be missing."); + Assert.That(((ChatTemplate)result.ConfigurationObject).ProfileId, Is.EqualTo(Profile.NO_PROFILE.Id)); + }); + } + + [Test] + public async Task AProviderBelowTheConfidenceOfTheChatIsNamed() + { + // A chat template names its provider by a GUID, as every provider of the app has one: + var provider = LessTrustedProvider() with { Id = Guid.NewGuid().ToString() }; + this.SettingsManager.ConfigurationData.Providers.Add(provider); + this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence = true; + this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence = ConfidenceLevel.HIGH; + + var snippet = ExportChatTemplate([], providerId: provider.Id); + var result = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(snippet, PluginConfigurationObjectType.CHAT_TEMPLATE), this.SettingsManager, this.CreateRegistry(new TestTool(Definition()))); + + Assert.Multiple(() => + { + Assert.That(result.Hints, Has.Count.EqualTo(1), "The provider exists, but chats started with the template would not use it."); + Assert.That(result.Hints, Has.Exactly(1).Contains(provider.InstanceName)); + }); + } + + [Test] + public async Task AProfileOfATemplateWhichForbidsProfilesIsNamed() + { + var missingProfileId = Guid.NewGuid().ToString(); + var template = ChatTemplateWith([], null, null, profileId: missingProfileId) with { AllowProfileUsage = false }; + Assert.That(template.TryExportAsConfigurationSection(out var snippet, out var issue), Is.True, issue); + + var result = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(snippet, PluginConfigurationObjectType.CHAT_TEMPLATE), this.SettingsManager, this.CreateRegistry(new TestTool(Definition()))); + + Assert.Multiple(() => + { + Assert.That(result.Hints, Has.Count.EqualTo(1), "Whether the profile exists does not matter, since chats ignore it anyway."); + Assert.That(result.Hints, Has.None.Contains(missingProfileId)); }); } @@ -440,7 +495,7 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase var registry = this.CreateRegistry(new TestTool(Definition())); var policy = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(ExportPolicy(provider.Id, profile.Id, [TOOL_ID]), PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY), this.SettingsManager, registry); - var template = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(ExportChatTemplate([], [TOOL_ID], [dataSource.Id]), PluginConfigurationObjectType.CHAT_TEMPLATE), this.SettingsManager, registry); + var template = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(ExportChatTemplate([], [TOOL_ID], [dataSource.Id], provider.Id, profile.Id), PluginConfigurationObjectType.CHAT_TEMPLATE), this.SettingsManager, registry); Assert.Multiple(() => { @@ -589,7 +644,7 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase return (profile, profile.ExportAsConfigurationSection()); case PluginConfigurationObjectType.CHAT_TEMPLATE: - var template = ChatTemplateWith([], ["web_search"], [Guid.NewGuid().ToString()]); + var template = ChatTemplateWith([], ["web_search"], [Guid.NewGuid().ToString()], Guid.NewGuid().ToString(), Guid.NewGuid().ToString()) with { WorkspaceName = "Contracts" }; Assert.That(template.TryExportAsConfigurationSection(out var luaCode, out var issue), Is.True, issue); return (template, luaCode); @@ -677,14 +732,14 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase return (dataSource, dataSource.ExportAsConfigurationSection(encryptedSecret, usernamePasswordMode)); } - private static string ExportChatTemplate(IReadOnlyList attachmentPaths, IReadOnlyList? toolIds = null, IReadOnlyList? dataSourceIds = null) + private static string ExportChatTemplate(IReadOnlyList attachmentPaths, IReadOnlyList? toolIds = null, IReadOnlyList? dataSourceIds = null, string? providerId = null, string? profileId = null) { - var template = ChatTemplateWith(attachmentPaths, toolIds, dataSourceIds); + var template = ChatTemplateWith(attachmentPaths, toolIds, dataSourceIds, providerId, profileId); Assert.That(template.TryExportAsConfigurationSection(out var luaCode, out var issue), Is.True, issue); return luaCode; } - private static ChatTemplate ChatTemplateWith(IReadOnlyList attachmentPaths, IReadOnlyList? toolIds, IReadOnlyList? dataSourceIds) => new() + private static ChatTemplate ChatTemplateWith(IReadOnlyList attachmentPaths, IReadOnlyList? toolIds, IReadOnlyList? dataSourceIds, string? providerId = null, string? profileId = null) => new() { Num = 1, Id = Guid.NewGuid().ToString(), @@ -700,6 +755,8 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase ], ToolIds = toolIds?.ToHashSet(StringComparer.Ordinal), DataSourceOptions = dataSourceIds is null ? null : new() { DisableDataSources = false, AutomaticValidation = true, PreselectedDataSourceIds = [..dataSourceIds] }, + ProviderId = providerId, + ProfileId = profileId, }; private static string ExportPolicy(string preselectedProvider = "", string preselectedProfile = "", IReadOnlyList? allowedToolIds = null) => PolicyWith(preselectedProvider, preselectedProfile, allowedToolIds ?? []).ExportAsConfigurationSection(); diff --git a/app/Tests/Tools/PluginSystem/ConfigurationSnippetParserTests.cs b/app/Tests/Tools/PluginSystem/ConfigurationSnippetParserTests.cs index d1adc1b3b..b97296e1f 100644 --- a/app/Tests/Tools/PluginSystem/ConfigurationSnippetParserTests.cs +++ b/app/Tests/Tools/PluginSystem/ConfigurationSnippetParserTests.cs @@ -270,8 +270,12 @@ public sealed class ConfigurationSnippetParserTests ], ToolIds = ["web_search"], - // Preselected data sources make the export start with a comment: + // Preselected data sources make the export start with a comment, and so do a preselected + // provider and profile, with a comment of their own: DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = [Guid.NewGuid().ToString()] }, + ProviderId = Guid.NewGuid().ToString(), + ProfileId = Guid.NewGuid().ToString(), + WorkspaceName = QUOTED_TEXT, }; Assert.That(template.TryExportAsConfigurationSection(out var luaCode, out var issue), Is.True, issue); diff --git a/app/Tests/Tools/WorkspaceChatTemplateResolutionTests.cs b/app/Tests/Tools/WorkspaceChatTemplateResolutionTests.cs new file mode 100644 index 000000000..0f1f3e11d --- /dev/null +++ b/app/Tests/Tools/WorkspaceChatTemplateResolutionTests.cs @@ -0,0 +1,100 @@ +using AIStudio.Settings; +using AIStudio.Tools; + +namespace AIStudio.Tests.Tools; + +/// +/// Checks how a chat finds the workspace its chat template names. +/// +/// +/// A chat template names its workspace by name, because a workspace has an ID of its own on every +/// computer. The chat page and assistant chat launchers both go through the same method, so it has +/// to find the workspace the user sees in the list under that name, create it only when it is +/// missing, and never stop a chat over a name which names nothing. +/// +[TestFixture] +[NonParallelizable] +public sealed class WorkspaceChatTemplateResolutionTests +{ + private const string CHAT_TEMPLATE_NAME = "Mail"; + + private string? previousDataDirectory; + private string testDirectory = string.Empty; + + [SetUp] + public async Task PrepareDataDirectory() + { + // + // The data directory and the workspace cache are static state of the whole application, + // which is why this fixture does not run alongside others, see WorkspaceTreeCacheTests: + // + this.previousDataDirectory = SettingsManager.DataDirectory; + this.testDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-workspace-template-{Guid.NewGuid():N}"); + Directory.CreateDirectory(this.testDirectory); + SettingsManager.DataDirectory = this.testDirectory; + WorkspaceBehaviour.TestDataDirectory = this.testDirectory; + + await WorkspaceBehaviour.ForceReloadWorkspaceTreeAsync(); + } + + [TearDown] + public void RemoveDataDirectory() + { + SettingsManager.DataDirectory = this.previousDataDirectory; + WorkspaceBehaviour.TestDataDirectory = null; + + // Whoever reads the cache next must not get the workspaces of this test: + WorkspaceBehaviour.InvalidateWorkspaceTreeCache(); + + try + { + Directory.Delete(this.testDirectory, true); + } + catch (IOException) + { + // A temporary directory we could not remove says nothing about the code under test. + } + } + + [Test] + public async Task AnExistingWorkspaceIsFoundByItsNameWithoutSpacesInAnyCase() + { + var created = await WorkspaceBehaviour.TryCreateWorkspaceAsync("Mails"); + Assert.That(created.Success, Is.True); + + var workspaceId = await WorkspaceBehaviour.ResolveOrCreateChatTemplateWorkspaceAsync(" mails ", CHAT_TEMPLATE_NAME); + var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); + + Assert.Multiple(() => + { + Assert.That(workspaceId, Is.EqualTo(created.Workspace.WorkspaceId), "The template names the workspace the user sees in the list under that name."); + Assert.That(snapshot.Workspaces, Has.Count.EqualTo(1), "A second workspace of the same name would split the chats of the template."); + }); + } + + [Test] + public async Task AMissingWorkspaceIsCreated() + { + var workspaceId = await WorkspaceBehaviour.ResolveOrCreateChatTemplateWorkspaceAsync("Contracts", CHAT_TEMPLATE_NAME); + var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); + + Assert.Multiple(() => + { + Assert.That(workspaceId, Is.Not.EqualTo(Guid.Empty)); + Assert.That(snapshot.Workspaces.Single(workspace => workspace.WorkspaceId == workspaceId).Name, Is.EqualTo("Contracts")); + }); + } + + [Test] + public async Task ANameWhichNamesNothingGivesNoWorkspace() + { + var workspaceId = await WorkspaceBehaviour.ResolveOrCreateChatTemplateWorkspaceAsync(" ", CHAT_TEMPLATE_NAME); + var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); + + Assert.Multiple(() => + { + Assert.That(workspaceId, Is.EqualTo(Guid.Empty), "The chat then starts without a workspace, as it would without the template."); + Assert.That(snapshot.Workspaces, Is.Empty, "A workspace without a name would only show up as an unnamed one in the list."); + }); + } +} \ No newline at end of file diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md index 5e0c7bc00..2b1c325f9 100644 --- a/documentation/Enterprise IT.md +++ b/documentation/Enterprise IT.md @@ -795,7 +795,31 @@ Writing the `DataSourceOptions` table at all is already the statement that this Semantic search only works where `semantic_search` can be offered: the model has to be able to call tools, neither `DataTools.EnableTools` nor `DataTools.DisabledToolIds` may switch it off, and the provider has to meet a minimum confidence you set for it in `DataTools.MinimumProviderConfidenceByToolId`. Otherwise, the chat searches with every message instead. A template that leaves `RetrievalMode` out gets `SEMANTIC_SEARCH`, not the chat default. That chat default is the setting `DataChat.PreselectedDataSourcesRetrievalMode`, with the same two values. -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. +When an [assistant plugin](../app/MindWork%20AI%20Studio/Plugins/assistants/README.md) opens a chat directly and its chat template names tools, data sources, a provider, or a profile, 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. + +## Chat templates with a provider, a profile, and a workspace + +A chat template may also decide the provider, and with it the model, and the profile a chat started with it begins with, and the workspace that keeps such chats. Use case: a template for working with mails preselects the mailbox as its data source, the provider whose confidence level the mailbox requires, a profile that suits mail work, and a workspace for these chats, so that users no longer set all of this up by hand for every chat. + +| What the template says | What a chat started with it does | +|---|---| +| no `ProviderId` at all | starts with the provider it would start with anyway | +| `ProviderId` present | starts with that provider, if it exists and meets the confidence level required for chats | +| no `ProfileId` at all | starts with the profile it would start with anyway | +| `ProfileId` = `00000000-0000-0000-0000-000000000000` | starts without a profile, whatever that default says | +| `ProfileId` present | starts with that profile, if it exists | +| no `WorkspaceName` at all | goes where it would go anyway | +| `WorkspaceName` present | goes into that workspace, which is created when it does not exist yet, wherever the chat would otherwise be a disappearing chat | + +All three are a preselection, not a lock: users change the provider and the profile in the chat as usual, and move the chat to another workspace. A provider that does not exist or is below the confidence level of chats is ignored, and the chat starts with the provider it would start with anyway. A profile that does not exist is ignored as well, and the chat starts with the default profile of the user's chat settings, which may be none. A `ProfileId` means nothing when `AllowProfileUsage` is `false`. The data sources a template preselects still have to be permitted for the provider in use. + +What one template preselects does not carry over to the next one. When users switch from a template with a provider or a profile to one without, the chat starts with the default provider and the default profile of their chat settings, unless they had picked another one by hand in the meantime. + +The IDs may name a provider from `CONFIG["LLM_PROVIDERS"]` or a profile from `CONFIG["PROFILES"]` of the same configuration, whose IDs are the same on every computer, or one of another configuration of your organization. A chat template exported from the app carries the provider and profile IDs of the computer it was exported from, mostly the user's own, and the export starts with a comment saying so. Replace them with IDs from your configuration. + +The workspace goes by its name, because a workspace has an ID of its own on every computer. It fills in only where a chat would otherwise be a disappearing chat: the first chat when users open the chat page, a new chat they start outside the workspaces, and picking the template in a new chat. A chat users start in a workspace on purpose, for example, from the workspace list, stays there, and so does a chat of an assistant chat launcher that names a workspace of its own. A chat in the workspace of its template is no such choice: when users pick another template there, the new chat goes where that template sends it. While users have switched workspaces off in their workspace settings, the template names no workspace. + +On the chat page, the chat moves into the workspace with its first message, and only then is the workspace created when it does not exist yet. Opening the chat page or trying out a template creates no workspace, so one that users deleted stays gone until they actually start a chat with the template. An assistant chat launcher creates the workspace right away, because it opens its chat at once. ## Mailboxes