mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 02:53:38 +00:00
Merge branch 'main' into rollback_chat_copy_chat
This commit is contained in:
commit
7695863201
@ -753,9 +753,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
await this.AssistantSessionService.ClearAsync(this.assistantSessionKey);
|
||||
this.MediaTranscriptionService.ClearOwnerState(this.CurrentMediaImportOwner);
|
||||
this.assistantSessionId = null;
|
||||
this.ChatThread = null;
|
||||
this.LastUserPrompt = null;
|
||||
this.ResultingContentBlock = null;
|
||||
this.ClearConversationState();
|
||||
this.ProviderSettings = Settings.Provider.NONE;
|
||||
|
||||
await this.JsRuntime.ClearDiv(BEFORE_RESULT_DIV_ID);
|
||||
|
||||
@ -34,4 +34,18 @@ public abstract class AssistantLowerBase : MSGComponentBase
|
||||
protected ContentBlock? ResultingContentBlock;
|
||||
protected string[] InputIssues = [];
|
||||
protected bool IsProcessing;
|
||||
|
||||
/// <summary>
|
||||
/// Clears everything one assistant run has produced.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Assistants call this whenever the previous run must not carry over: a follow-up run would
|
||||
/// otherwise append to the old chat thread, and the old result would stay on screen.
|
||||
/// </remarks>
|
||||
protected void ClearConversationState()
|
||||
{
|
||||
this.ChatThread = null;
|
||||
this.LastUserPrompt = null;
|
||||
this.ResultingContentBlock = null;
|
||||
}
|
||||
}
|
||||
@ -2191,6 +2191,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER
|
||||
-- View
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T1582017048"] = "View"
|
||||
|
||||
-- Improve further
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T1582753277"] = "Improve further"
|
||||
|
||||
-- Separate context, task, constraints, and output format with headings or markers.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T1626024580"] = "Separate context, task, constraints, and output format with headings or markers."
|
||||
|
||||
@ -2287,6 +2290,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER
|
||||
-- Use sequential steps
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T487578804"] = "Use sequential steps"
|
||||
|
||||
-- Moves the optimized prompt into the prompt field so you can optimize it again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T502438377"] = "Moves the optimized prompt into the prompt field so you can optimize it again."
|
||||
|
||||
-- Use clear, explicit instructions and directly state quality expectations.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T596557540"] = "Use clear, explicit instructions and directly state quality expectations."
|
||||
|
||||
@ -3601,6 +3607,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2036185364"] = "Code"
|
||||
-- plus {0} image(s), which is more than the {1} this model accepts
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2059172343"] = "plus {0} image(s), which is more than the {1} this model accepts"
|
||||
|
||||
-- Are you sure you want to start a new chat? All unsaved changes will be lost.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2111282488"] = "Are you sure you want to start a new chat? All unsaved changes will be lost."
|
||||
|
||||
-- Unsaved Changes
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2123670756"] = "Unsaved Changes"
|
||||
|
||||
-- Start New Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2310454789"] = "Start New Chat"
|
||||
|
||||
-- Italic
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2377171085"] = "Italic"
|
||||
|
||||
@ -4003,6 +4018,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = "
|
||||
-- Chat profile
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2412069346"] = "Chat profile"
|
||||
|
||||
-- The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2545184598"] = "The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own."
|
||||
|
||||
-- {0} data source(s) selected
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2777836629"] = "{0} data source(s) selected"
|
||||
|
||||
@ -4021,6 +4039,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = "
|
||||
-- Use the normal chat data source defaults
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3898572329"] = "Use the normal chat data source defaults"
|
||||
|
||||
-- The chosen chat template brings tools of its own, and those win over a selection made here.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T4038774259"] = "The chosen chat template brings tools of its own, and those win over a selection made here."
|
||||
|
||||
-- Use no chat template
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T4258819635"] = "Use no chat template"
|
||||
|
||||
@ -5287,9 +5308,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T588743762"] = "An error o
|
||||
-- The transcription result is empty.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T974954792"] = "The transcription result is empty."
|
||||
|
||||
-- Are you sure you want to delete the chat '{0}' in the workspace '{1}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1016188706"] = "Are you sure you want to delete the chat '{0}' in the workspace '{1}'?"
|
||||
|
||||
-- Do you want to copy this chat? Your unsaved changes move into the copy, and the original chat keeps the state it was last saved with.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1047391993"] = "Do you want to copy this chat? Your unsaved changes move into the copy, and the original chat keeps the state it was last saved with."
|
||||
|
||||
@ -5344,9 +5362,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2151341762"] = "Are you sure
|
||||
-- Are you sure you want to create a another chat? All unsaved changes will be lost.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2237618267"] = "Are you sure you want to create a another chat? All unsaved changes will be lost."
|
||||
|
||||
-- Delete Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2244038752"] = "Delete Chat"
|
||||
|
||||
-- Please enter a chat name.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2301651387"] = "Please enter a chat name."
|
||||
|
||||
@ -5356,9 +5371,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2446263209"] = "Workspace Na
|
||||
-- Move to workspace
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2509305748"] = "Move to workspace"
|
||||
|
||||
-- Are you sure you want to delete the temporary chat '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3043761007"] = "Are you sure you want to delete the temporary chat '{0}'?"
|
||||
|
||||
-- Move Chat to Workspace
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3045856778"] = "Move Chat to Workspace"
|
||||
|
||||
@ -5692,12 +5704,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1352914344"] = "Only te
|
||||
-- Please enter a message for the example conversation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1362948628"] = "Please enter a message for the example conversation."
|
||||
|
||||
-- No, chats keep the tools from your chat options
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1363645855"] = "No, chats keep the tools from your chat options"
|
||||
|
||||
-- The chat template name must be unique; the chosen name is already in use.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "The chat template name must be unique; the chosen name is already in use."
|
||||
|
||||
-- The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1442266827"] = "The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options."
|
||||
|
||||
-- Please enter a name for the chat template.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Please enter a name for the chat template."
|
||||
|
||||
-- Yes, this template decides which data a chat starts with
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T17861006"] = "Yes, this template decides which data a chat starts with"
|
||||
|
||||
-- Load predefined user input from file
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Load predefined user input from file"
|
||||
|
||||
@ -5719,6 +5740,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "File At
|
||||
-- Role
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Role"
|
||||
|
||||
-- Yes, this template decides which tools a chat starts with
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2494694135"] = "Yes, this template decides which tools a chat starts with"
|
||||
|
||||
-- Tools
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2499909372"] = "Tools"
|
||||
|
||||
-- What predefined user input do you want to use?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2501284417"] = "What predefined user input do you want to use?"
|
||||
|
||||
@ -5764,6 +5791,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Are you
|
||||
-- Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here."
|
||||
|
||||
-- No, chats keep the data source options from your chat options
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "No, chats keep the data source options from your chat options"
|
||||
|
||||
-- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says."
|
||||
|
||||
-- Add a message
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message"
|
||||
|
||||
@ -5782,6 +5815,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3675108201"] = "Yes, al
|
||||
-- Add a new message below
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3757779731"] = "Add a new message below"
|
||||
|
||||
-- Does this chat template preselect data sources?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3779813414"] = "Does this chat template preselect data sources?"
|
||||
|
||||
-- Example Conversation
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T380891852"] = "Example Conversation"
|
||||
|
||||
@ -5794,6 +5830,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3883091650"] = "Load sy
|
||||
-- Messages per page
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3893704289"] = "Messages per page"
|
||||
|
||||
-- Does this chat template preselect tools?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T399377711"] = "Does this chat template preselect tools?"
|
||||
|
||||
-- Use the default system prompt
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4051106111"] = "Use the default system prompt"
|
||||
|
||||
@ -5806,15 +5845,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Create
|
||||
-- Enter a message
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Enter a message"
|
||||
|
||||
-- Data Sources
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T558345131"] = "Data Sources"
|
||||
|
||||
-- System Prompt
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System Prompt"
|
||||
|
||||
-- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself."
|
||||
|
||||
-- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use."
|
||||
|
||||
-- Allow the use of profiles together with this chat template?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Allow the use of profiles together with this chat template?"
|
||||
|
||||
-- Cancel
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel"
|
||||
|
||||
-- Preselected tools
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T975962532"] = "Preselected tools"
|
||||
|
||||
-- {0} LLM providers
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T121235760"] = "{0} LLM providers"
|
||||
|
||||
@ -7810,6 +7861,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T20545
|
||||
-- No chat templates configured yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "No chat templates configured yet."
|
||||
|
||||
-- This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2563631533"] = "This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway?"
|
||||
|
||||
-- Chat Template Name
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T275026390"] = "Chat Template Name"
|
||||
|
||||
@ -10111,6 +10165,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T265391888"] = "The selected
|
||||
-- The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2819996431"] = "The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again."
|
||||
|
||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). The provider turned the request down with the status code {2} and would turn it down again, so we stopped trying. The provider message is: '{3}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2993640453"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The provider turned the request down with the status code {2} and would turn it down again, so we stopped trying. The provider message is: '{3}'"
|
||||
|
||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). Something was not found. The provider message is: '{2}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3014737766"] = "We tried to communicate with the LLM provider '{0}' (type={1}). Something was not found. The provider message is: '{2}'"
|
||||
|
||||
@ -12160,12 +12217,21 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DATASOURCELOCALRETRIEVALSERVICE::T93
|
||||
-- The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T103791004"] = "The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}"
|
||||
|
||||
-- The following data sources selected by the chat template '{0}' are currently unavailable or not permitted for the selected provider: {1}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T1164564929"] = "The following data sources selected by the chat template '{0}' are currently unavailable or not permitted for the selected provider: {1}"
|
||||
|
||||
-- The chat template '{0}' references data source '{1}', but that data source does not exist.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T1951110186"] = "The chat template '{0}' references data source '{1}', but that data source does not exist."
|
||||
|
||||
-- The assistant chat launcher references profile '{0}', but that profile does not exist.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T2466659933"] = "The assistant chat launcher references profile '{0}', but that profile does not exist."
|
||||
|
||||
-- The assistant chat launcher references data source '{0}', but that data source does not exist.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T289191545"] = "The assistant chat launcher references data source '{0}', but that data source does not exist."
|
||||
|
||||
-- The chat template '{0}' selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3082876173"] = "The chat template '{0}' selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created."
|
||||
|
||||
-- The data sources selected by the assistant chat launcher could not be checked. No chat was created.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3232401465"] = "The data sources selected by the assistant chat launcher could not be checked. No chat was created."
|
||||
|
||||
@ -12175,6 +12241,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3242713584"] = "
|
||||
-- The provider '{0}' selected by the assistant chat launcher is not permitted for chats at the required confidence level.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3491209726"] = "The provider '{0}' selected by the assistant chat launcher is not permitted for chats at the required confidence level."
|
||||
|
||||
-- The data sources selected by the chat template '{0}' could not be checked. No chat was created.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T361525913"] = "The data sources selected by the chat template '{0}' could not be checked. No chat was created."
|
||||
|
||||
-- The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3780395901"] = "The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created."
|
||||
|
||||
|
||||
@ -101,6 +101,15 @@ public partial class AssistantPromptOptimizer : AssistantBaseCore<SettingsDialog
|
||||
|
||||
protected override IReadOnlyList<IButtonData> FooterButtons =>
|
||||
[
|
||||
new ButtonData
|
||||
{
|
||||
Text = T("Improve further"),
|
||||
Tooltip = T("Moves the optimized prompt into the prompt field so you can optimize it again."),
|
||||
Icon = Icons.Material.Filled.Input,
|
||||
Color = Color.Default,
|
||||
AsyncAction = this.UseOptimizedPromptAsInput,
|
||||
DisabledActionParam = () => !this.CanImproveFurther,
|
||||
},
|
||||
new SendToButton
|
||||
{
|
||||
Self = Tools.Components.PROMPT_OPTIMIZER_ASSISTANT,
|
||||
@ -245,6 +254,7 @@ public partial class AssistantPromptOptimizer : AssistantBaseCore<SettingsDialog
|
||||
|
||||
private bool ShowUpdatedPromptGuidelinesIndicator => !this.useCustomPromptGuide && this.hasUpdatedDefaultRecommendations;
|
||||
private bool CanPreviewCustomPromptGuide => this.useCustomPromptGuide && this.customPromptGuideFiles.Count > 0;
|
||||
private bool CanImproveFurther => !this.IsProcessing && !string.IsNullOrWhiteSpace(this.optimizedPrompt);
|
||||
private string CustomPromptGuideFileName => this.customPromptGuideFiles.Count switch
|
||||
{
|
||||
0 => T("No file selected"),
|
||||
@ -464,6 +474,27 @@ public partial class AssistantPromptOptimizer : AssistantBaseCore<SettingsDialog
|
||||
this.optimizedPrompt = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the optimized prompt into the input field so the user can optimize it once more.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The finished run is dropped along the way. Keeping it would append the next optimization to
|
||||
/// the chat thread of the previous one, and the earlier proposal would stay on screen next to
|
||||
/// the prompt it was already turned into. The recommendations and every selection stay as they
|
||||
/// are, though: they are what the user works with while refining the prompt.
|
||||
/// </remarks>
|
||||
private Task UseOptimizedPromptAsInput()
|
||||
{
|
||||
if (!this.CanImproveFurther)
|
||||
return Task.CompletedTask;
|
||||
|
||||
this.inputPrompt = this.optimizedPrompt;
|
||||
this.ResetOutput();
|
||||
this.ClearConversationState();
|
||||
this.ClearInputIssues();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ResetGuidelineSummaryToDefault()
|
||||
{
|
||||
this.recClarityDirectness = T("Use clear, explicit instructions and directly state quality expectations.");
|
||||
|
||||
@ -94,7 +94,7 @@
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
<MudTooltip Text="@T("Delete this chat & start a new one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="@(() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true))" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.DeleteSweep" Color="Color.Error" OnClick="@(() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true))" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
|
||||
@ -74,7 +74,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
private DataSourceSelection? dataSourceSelectionComponent;
|
||||
private DataSourceOptions earlyDataSourceOptions = new();
|
||||
private DataSourceOptions lastAppliedStandardDataSourceOptions = new();
|
||||
private DataSourceOptions lastAppliedAutomaticDataSourceOptions = new();
|
||||
private Profile currentProfile = Profile.NO_PROFILE;
|
||||
private ChatTemplate currentChatTemplate = ChatTemplate.NO_CHAT_TEMPLATE;
|
||||
private bool hasUnsavedChanges;
|
||||
@ -253,9 +253,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
|
||||
if (!this.ComposerState.HasUserDraft && !this.ComposerState.HasComposerContent)
|
||||
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
|
||||
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT));
|
||||
|
||||
this.lastAppliedStandardDataSourceOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy();
|
||||
await this.ApplyChatTemplateToolSelectionAsync();
|
||||
this.lastAppliedAutomaticDataSourceOptions = this.GetAutomaticDataSourceOptions();
|
||||
|
||||
var deferredInput = MessageBus.INSTANCE.TakeDeferredMessages<string>(Event.SEND_TO_CHAT_INPUT).LastOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(deferredInput))
|
||||
@ -343,7 +343,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
//
|
||||
// No, the user did not send an assistant result to the chat.
|
||||
//
|
||||
this.ApplyStandardDataSourceOptions();
|
||||
this.ApplyAutomaticDataSourceOptions();
|
||||
}
|
||||
|
||||
//
|
||||
@ -694,35 +694,75 @@ public partial class ChatComponent : MSGComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyStandardDataSourceOptions()
|
||||
/// <summary>
|
||||
/// Picks the tools a chat starts with: those of its chat template, or the chat defaults.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A preselection, not a limit — the user changes it in the chat as usual. A template without a
|
||||
/// tool selection says nothing about tools and therefore leaves the chat default in place,
|
||||
/// which is a different statement from a template that selects no tool at all.
|
||||
/// </remarks>
|
||||
private async Task ApplyChatTemplateToolSelectionAsync()
|
||||
{
|
||||
var chatDefaultOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy();
|
||||
this.lastAppliedStandardDataSourceOptions = chatDefaultOptions.CreateCopy();
|
||||
this.earlyDataSourceOptions = chatDefaultOptions;
|
||||
if(this.ChatThread is not null)
|
||||
this.ChatThread.DataSourceOptions = chatDefaultOptions;
|
||||
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(chatDefaultOptions);
|
||||
if (this.currentChatTemplate.ToolIds is not { } templateToolIds)
|
||||
{
|
||||
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT));
|
||||
return;
|
||||
}
|
||||
|
||||
private async Task ApplyUpdatedStandardDataSourceOptionsAfterConfigurationChange()
|
||||
//
|
||||
// Only the tools the user could have switched on themselves: a template may name one whose
|
||||
// settings are incomplete — an unconfigured web search, say — and starting with it enabled
|
||||
// would show a state the user cannot produce by hand and cannot fix from the chat.
|
||||
//
|
||||
this.selectedToolIds = await this.ToolRegistry.FilterSelectableToolIdsAsync(Tools.Components.CHAT, templateToolIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The data source options a chat starts with: those of its chat template, or the chat defaults.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// As with the tools, a template which carries no options says nothing and leaves the chat
|
||||
/// defaults in place. A template which carries them answers more than which sources to search:
|
||||
/// whether data sources are used at all, and whether an agent picks them for each message.
|
||||
/// </remarks>
|
||||
private DataSourceOptions GetAutomaticDataSourceOptions() =>
|
||||
this.currentChatTemplate.DataSourceOptions?.CreateCopy() ?? this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy();
|
||||
|
||||
private void ApplyAutomaticDataSourceOptions()
|
||||
{
|
||||
var updatedStandardOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy();
|
||||
var previousStandardOptions = this.lastAppliedStandardDataSourceOptions;
|
||||
this.lastAppliedStandardDataSourceOptions = updatedStandardOptions.CreateCopy();
|
||||
var automaticOptions = this.GetAutomaticDataSourceOptions();
|
||||
this.lastAppliedAutomaticDataSourceOptions = automaticOptions.CreateCopy();
|
||||
this.earlyDataSourceOptions = automaticOptions;
|
||||
if(this.ChatThread is not null)
|
||||
this.ChatThread.DataSourceOptions = automaticOptions;
|
||||
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(automaticOptions);
|
||||
}
|
||||
|
||||
private async Task ApplyUpdatedAutomaticDataSourceOptionsAfterConfigurationChange()
|
||||
{
|
||||
//
|
||||
// What a chat would start with right now. The chat template is asked first, so that editing
|
||||
// the template of the current chat reaches it — a change of the chat defaults it does not
|
||||
// use would say nothing about it.
|
||||
//
|
||||
var updatedAutomaticOptions = this.GetAutomaticDataSourceOptions();
|
||||
var previousAutomaticOptions = this.lastAppliedAutomaticDataSourceOptions;
|
||||
this.lastAppliedAutomaticDataSourceOptions = updatedAutomaticOptions.CreateCopy();
|
||||
|
||||
if (this.ChatThread is null)
|
||||
{
|
||||
this.earlyDataSourceOptions = updatedStandardOptions;
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(updatedStandardOptions);
|
||||
this.earlyDataSourceOptions = updatedAutomaticOptions;
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(updatedAutomaticOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!DataSourceOptionsAreEqual(this.ChatThread.DataSourceOptions, previousStandardOptions))
|
||||
if (!DataSourceOptionsAreEqual(this.ChatThread.DataSourceOptions, previousAutomaticOptions))
|
||||
return;
|
||||
|
||||
await this.SetCurrentDataSourceOptions(updatedStandardOptions);
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(updatedStandardOptions, this.ChatThread.AISelectedDataSources);
|
||||
await this.SetCurrentDataSourceOptions(updatedAutomaticOptions);
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(updatedAutomaticOptions, this.ChatThread.AISelectedDataSources);
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
|
||||
@ -782,7 +822,18 @@ public partial class ChatComponent : MSGComponentBase
|
||||
this.ComposerState.ReplaceFileAttachments(this.currentChatTemplate.FileAttachments);
|
||||
|
||||
if (this.ChatThread is not null)
|
||||
{
|
||||
// Starting the new chat is what hands the selection of the new template to it:
|
||||
await this.StartNewChat(true);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Without a thread there is nothing to start anew, so the selection of the new template is
|
||||
// applied right here. It travels into the thread which the first message creates.
|
||||
//
|
||||
await this.ApplyChatTemplateToolSelectionAsync();
|
||||
this.ApplyAutomaticDataSourceOptions();
|
||||
}
|
||||
|
||||
private void RefreshCurrentProfileAndChatTemplate()
|
||||
@ -819,7 +870,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
if (!this.ComposerState.HasUserDraft && previousChatTemplate != this.currentChatTemplate)
|
||||
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
|
||||
|
||||
await this.ApplyUpdatedStandardDataSourceOptionsAfterConfigurationChange();
|
||||
await this.ApplyUpdatedAutomaticDataSourceOptionsAfterConfigurationChange();
|
||||
}
|
||||
|
||||
private IReadOnlyList<DataSourceAgentSelected> GetAgentSelectedDataSources()
|
||||
@ -1153,10 +1204,10 @@ public partial class ChatComponent : MSGComponentBase
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{ x => x.Message, "Are you sure you want to start a new chat? All unsaved changes will be lost." },
|
||||
{ x => x.Message, T("Are you sure you want to start a new chat? All unsaved changes will be lost.") },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Start New Chat"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
@ -1167,16 +1218,27 @@ public partial class ChatComponent : MSGComponentBase
|
||||
//
|
||||
if (this.ChatThread is not null && deletePreviousChat)
|
||||
{
|
||||
string chatPath;
|
||||
if (this.ChatThread.WorkspaceId == Guid.Empty)
|
||||
chatPath = Path.Join(SettingsManager.DataDirectory, "tempChats", this.ChatThread.ChatId.ToString());
|
||||
//
|
||||
// A deleted chat cannot be restored, and the check above never covers this path: it
|
||||
// exists only while chats are stored automatically, while that check runs only while
|
||||
// they are stored manually. So we let the deletion itself ask, with the question the
|
||||
// chat list asks. When it reports the chat is still there, the user declined or the
|
||||
// chat is busy, and we stop before the reset below takes it out of view:
|
||||
//
|
||||
bool chatIsGone;
|
||||
if (this.Workspaces is null)
|
||||
chatIsGone = await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, this.ChatThread.WorkspaceId, this.ChatThread.ChatId);
|
||||
else
|
||||
chatPath = Path.Join(SettingsManager.DataDirectory, "workspaces", this.ChatThread.WorkspaceId.ToString(), this.ChatThread.ChatId.ToString());
|
||||
{
|
||||
var chatPath = this.ChatThread.WorkspaceId == Guid.Empty
|
||||
? Path.Join(SettingsManager.DataDirectory, "tempChats", this.ChatThread.ChatId.ToString())
|
||||
: Path.Join(SettingsManager.DataDirectory, "workspaces", this.ChatThread.WorkspaceId.ToString(), this.ChatThread.ChatId.ToString());
|
||||
|
||||
if(this.Workspaces is null)
|
||||
await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, this.ChatThread.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
|
||||
else
|
||||
await this.Workspaces.DeleteChatAsync(chatPath, askForConfirmation: false, unloadChat: true);
|
||||
chatIsGone = await this.Workspaces.DeleteChatAsync(chatPath, unloadChat: true);
|
||||
}
|
||||
|
||||
if (!chatIsGone)
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
@ -1184,7 +1246,6 @@ public partial class ChatComponent : MSGComponentBase
|
||||
//
|
||||
this.hasUnsavedChanges = false;
|
||||
this.ComposerState.Clear();
|
||||
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT));
|
||||
this.RefreshCurrentProfileAndChatTemplate();
|
||||
|
||||
//
|
||||
@ -1233,8 +1294,10 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
|
||||
|
||||
// Now, we have to reset the data source options as well:
|
||||
this.ApplyStandardDataSourceOptions();
|
||||
// Now, the chat starts with what its template asks for, and with the chat defaults wherever
|
||||
// that template says nothing:
|
||||
await this.ApplyChatTemplateToolSelectionAsync();
|
||||
this.ApplyAutomaticDataSourceOptions();
|
||||
|
||||
// Notify the parent component about the change:
|
||||
await this.SyncForegroundChatAsync();
|
||||
@ -1254,7 +1317,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
{ x => x.Message, T("Are you sure you want to move this chat? All unsaved changes will be lost.") },
|
||||
};
|
||||
|
||||
var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Unsaved Changes", confirmationDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Unsaved Changes"), confirmationDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var confirmationDialogResult = await confirmationDialogReference.Result;
|
||||
if (confirmationDialogResult is null || confirmationDialogResult.Canceled)
|
||||
return;
|
||||
@ -1306,7 +1369,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
this.loadedParameterWorkspaceId = Guid.Empty;
|
||||
this.ClearWorkspaceHeaderState();
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.ApplyStandardDataSourceOptions();
|
||||
this.ApplyAutomaticDataSourceOptions();
|
||||
}
|
||||
|
||||
await this.SelectProviderWhenLoadingChat();
|
||||
@ -1341,7 +1404,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
this.ChatThread = null;
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.ApplyStandardDataSourceOptions();
|
||||
this.ApplyAutomaticDataSourceOptions();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
|
||||
|
||||
@ -179,13 +179,13 @@ else if (this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE)
|
||||
</MudText>
|
||||
}
|
||||
|
||||
<MudTextSwitch Label="@T("Are data sources enabled?")" Value="@this.areDataSourcesEnabled" LabelOn="@T("Yes, I want to use data sources.")" LabelOff="@T("No, I don't want to use data sources.")" ValueChanged="@this.EnabledChanged" Disabled="@this.IsPreselectedDataSourcesDisabledLocked()"/>
|
||||
<MudTextSwitch Label="@T("Are data sources enabled?")" Value="@this.areDataSourcesEnabled" LabelOn="@T("Yes, I want to use data sources.")" LabelOff="@T("No, I don't want to use data sources.")" ValueChanged="@this.EnabledChanged" Disabled="@(this.ReadOnly || this.IsPreselectedDataSourcesDisabledLocked())"/>
|
||||
@if (this.areDataSourcesEnabled)
|
||||
{
|
||||
<MudTextSwitch Label="@T("AI-based data source selection")" Value="@this.aiBasedSourceSelection" LabelOn="@T("Yes, let the AI decide which data sources are needed.")" LabelOff="@T("No, I manually decide which data source to use.")" ValueChanged="@this.AutoModeChanged" Disabled="@this.IsPreselectedDataSourcesAutomaticSelectionLocked()"/>
|
||||
<MudTextSwitch Label="@T("AI-based data validation")" Value="@this.aiBasedValidation" LabelOn="@T("Yes, let the AI validate & filter the retrieved data.")" LabelOff="@T("No, use all data retrieved from the data sources.")" ValueChanged="@this.ValidationModeChanged" Disabled="@this.IsPreselectedDataSourcesAutomaticValidationLocked()"/>
|
||||
<MudField Label="@T("Available Data Sources")" Variant="Variant.Outlined" Class="mb-3" Disabled="@(this.aiBasedSourceSelection || this.IsPreselectedDataSourceIdsLocked())">
|
||||
<MudList T="IDataSource" Dense="@true" Class="data-source-rows" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" ReadOnly="@this.IsPreselectedDataSourceIdsLocked()">
|
||||
<MudTextSwitch Label="@T("AI-based data source selection")" Value="@this.aiBasedSourceSelection" LabelOn="@T("Yes, let the AI decide which data sources are needed.")" LabelOff="@T("No, I manually decide which data source to use.")" ValueChanged="@this.AutoModeChanged" Disabled="@(this.ReadOnly || this.IsPreselectedDataSourcesAutomaticSelectionLocked())"/>
|
||||
<MudTextSwitch Label="@T("AI-based data validation")" Value="@this.aiBasedValidation" LabelOn="@T("Yes, let the AI validate & filter the retrieved data.")" LabelOff="@T("No, use all data retrieved from the data sources.")" ValueChanged="@this.ValidationModeChanged" Disabled="@(this.ReadOnly || this.IsPreselectedDataSourcesAutomaticValidationLocked())"/>
|
||||
<MudField Label="@T("Available Data Sources")" Variant="Variant.Outlined" Class="mb-3" Disabled="@(this.ReadOnly || this.aiBasedSourceSelection || this.IsPreselectedDataSourceIdsLocked())">
|
||||
<MudList T="IDataSource" Dense="@true" Class="data-source-rows" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" ReadOnly="@(this.ReadOnly || this.IsPreselectedDataSourceIdsLocked())">
|
||||
@*
|
||||
The configuration mode lists what was configured, without filtering it, so no
|
||||
row here is ever waiting for an index.
|
||||
|
||||
@ -39,6 +39,27 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
[Parameter]
|
||||
public bool AutoSaveAppSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Shows the options without letting the user change them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For options somebody else decided on, such as those of a chat template an organization
|
||||
/// rolled out. Seeing which data such a chat will search is the point; changing it here is not.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the options edited here are the data source defaults of the chat.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Those defaults can be locked by a configuration plugin, and this component reads the locks
|
||||
/// from the chat settings. Wherever the same options belong to something else — to a chat
|
||||
/// template, say — the locks of the chat defaults have nothing to say about them.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool ConfiguresChatDefaults { get; set; } = true;
|
||||
|
||||
[Inject]
|
||||
private DataSourceService DataSourceService { get; init; } = null!;
|
||||
|
||||
@ -334,6 +355,7 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
private bool IsPreselectedDataSourcesDisabledLocked()
|
||||
{
|
||||
return this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE
|
||||
&& this.ConfiguresChatDefaults
|
||||
&& ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedDataSourcesDisabled, out var meta)
|
||||
&& meta.IsLocked;
|
||||
}
|
||||
@ -341,6 +363,7 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
private bool IsPreselectedDataSourcesAutomaticSelectionLocked()
|
||||
{
|
||||
return this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE
|
||||
&& this.ConfiguresChatDefaults
|
||||
&& ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedDataSourcesAutomaticSelection, out var meta)
|
||||
&& meta.IsLocked;
|
||||
}
|
||||
@ -348,6 +371,7 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
private bool IsPreselectedDataSourcesAutomaticValidationLocked()
|
||||
{
|
||||
return this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE
|
||||
&& this.ConfiguresChatDefaults
|
||||
&& ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedDataSourcesAutomaticValidation, out var meta)
|
||||
&& meta.IsLocked;
|
||||
}
|
||||
@ -355,6 +379,7 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
private bool IsPreselectedDataSourceIdsLocked()
|
||||
{
|
||||
return this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE
|
||||
&& this.ConfiguresChatDefaults
|
||||
&& ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedDataSourceIds, out var meta)
|
||||
&& meta.IsLocked;
|
||||
}
|
||||
|
||||
@ -51,4 +51,20 @@
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@* 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)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
|
||||
@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.")
|
||||
</MudText>
|
||||
}
|
||||
|
||||
<ToolSelectionField Component="Components.CHAT" SelectedToolIds="@this.ToolIds" SelectedToolIdsChanged="@this.SetToolIds" Label="@T("Tools (Optional)")" Help="@T("These tools are preselected when the chat opens. Users can change the selection in the chat, and every tool has to meet the confidence requirements of the provider in use.")"/>
|
||||
|
||||
@if (this.SelectedChatTemplate.ToolIds is not null)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||
@T("The chosen chat template brings tools of its own, and those win over a selection made here.")
|
||||
</MudText>
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
@ -81,6 +83,19 @@ public partial class DirectChatLauncherForm : MSGComponentBase
|
||||
/// </summary>
|
||||
private bool OpensTemporaryChat => string.IsNullOrWhiteSpace(this.WorkspaceName);
|
||||
|
||||
/// <summary>
|
||||
/// The chat template the launcher would open its chat with, as far as it is known here.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// With "use chat default" chosen, this is whichever template the chat options name right now,
|
||||
/// and that may well be another one by the time somebody opens the launcher. The form therefore
|
||||
/// only says what such a template brings along instead of disabling the fields below it: a field
|
||||
/// which locks itself behind the user's back is worse than a sentence explaining the situation.
|
||||
/// </remarks>
|
||||
private ChatTemplate SelectedChatTemplate => string.IsNullOrWhiteSpace(this.ChatTemplateId)
|
||||
? this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT)
|
||||
: this.SettingsManager.GetChatTemplateById(this.ChatTemplateId);
|
||||
|
||||
private IReadOnlyList<WorkspaceTreeWorkspace> availableWorkspaces = [];
|
||||
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
@ -704,37 +704,35 @@ public partial class Workspaces : MSGComponentBase
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task DeleteChatAsync(string? chatPath, bool askForConfirmation = true, bool unloadChat = true)
|
||||
/// <summary>Deletes the given chat and updates the tree, asking the user to confirm that beforehand.</summary>
|
||||
/// <param name="chatPath">Path of the chat to delete.</param>
|
||||
/// <param name="askForConfirmation">False skips the question. Only for callers who already asked.</param>
|
||||
/// <param name="unloadChat">Whether to take the chat out of the view when it is the one being shown.</param>
|
||||
/// <returns>True when the chat is gone, which includes it never having been there. False when it is still there.</returns>
|
||||
/// <remarks>
|
||||
/// The question itself comes from the workspace behaviour, so that it is worded in one place only.
|
||||
/// Callers who do more than deleting have to honor the return value: a chat that is busy is not
|
||||
/// deleted either, and then nothing about it may be reset.
|
||||
/// </remarks>
|
||||
public async Task<bool> DeleteChatAsync(string? chatPath, bool askForConfirmation = true, bool unloadChat = true)
|
||||
{
|
||||
var chat = await this.LoadChatAsync(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
// There is nothing left to delete, so the caller may go on:
|
||||
if (chat is null)
|
||||
return true;
|
||||
|
||||
//
|
||||
// Deleting a chat while it is being worked on would pull the ground from under that work.
|
||||
// We check before asking: nobody should confirm something that cannot happen anyway.
|
||||
//
|
||||
var mediaOwner = MediaImportOwner.ForChat(chat.ChatId);
|
||||
if (this.AIJobService.IsChatGenerationActive(chat.ChatId) || this.MediaTranscriptionService.IsBusy(mediaOwner))
|
||||
return;
|
||||
return false;
|
||||
|
||||
if (askForConfirmation)
|
||||
{
|
||||
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(chat.WorkspaceId);
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{
|
||||
x => x.Message, (chat.WorkspaceId == Guid.Empty) switch
|
||||
{
|
||||
true => string.Format(T("Are you sure you want to delete the temporary chat '{0}'?"), chat.Name),
|
||||
false => string.Format(T("Are you sure you want to delete the chat '{0}' in the workspace '{1}'?"), chat.Name, workspaceName),
|
||||
}
|
||||
},
|
||||
};
|
||||
if (!await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, chat.WorkspaceId, chat.ChatId, askForConfirmation))
|
||||
return false;
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Delete Chat"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
}
|
||||
|
||||
await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, chat.WorkspaceId, chat.ChatId, askForConfirmation: false);
|
||||
this.MediaTranscriptionService.ClearOwnerState(mediaOwner);
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
|
||||
@ -743,6 +741,8 @@ public partial class Workspaces : MSGComponentBase
|
||||
this.CurrentChatThread = null;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task RenameChatAsync(string? chatPath)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
@using AIStudio.Chat
|
||||
@using AIStudio.Settings.DataModel
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudDialog>
|
||||
@ -109,6 +110,35 @@
|
||||
</MudJustifiedText>
|
||||
<MudTextSwitch @bind-Value="@this.AllowProfileUsage" Color="Color.Primary" Label="@T("Allow the use of profiles together with this chat template?")" LabelOn="@T("Yes, allow profiles when using this template")" LabelOff="@T("No, prohibit profile use for this template")" Disabled="@this.IsReadOnly" />
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Tools")
|
||||
</MudText>
|
||||
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
|
||||
@T("A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says.")
|
||||
</MudJustifiedText>
|
||||
<MudTextSwitch @bind-Value="@this.preselectTools" Color="Color.Primary" Label="@T("Does this chat template preselect tools?")" LabelOn="@T("Yes, this template decides which tools a chat starts with")" LabelOff="@T("No, chats keep the tools from your chat options")" Disabled="@this.IsReadOnly"/>
|
||||
@if (this.preselectTools)
|
||||
{
|
||||
<MudPaper Class="pa-3 mb-8 mt-3 border-dashed border rounded-lg">
|
||||
<ToolSelectionField Component="Components.CHAT" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SetSelectedToolIds" ReadOnly="@this.IsReadOnly" Label="@T("Preselected tools")" Help="@T("The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use.")"/>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Data Sources")
|
||||
</MudText>
|
||||
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
|
||||
@T("The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options.")
|
||||
</MudJustifiedText>
|
||||
<MudTextSwitch @bind-Value="@this.preselectDataSources" Color="Color.Primary" Label="@T("Does this chat template preselect data sources?")" LabelOn="@T("Yes, this template decides which data a chat starts with")" LabelOff="@T("No, chats keep the data source options from your chat options")" Disabled="@this.IsReadOnly"/>
|
||||
@if (this.preselectDataSources)
|
||||
{
|
||||
<DataSourceSelection SelectionMode="DataSourceSelectionMode.CONFIGURATION_MODE" AutoSaveAppSettings="@false" ConfiguresChatDefaults="@false" ReadOnly="@this.IsReadOnly" @bind-DataSourceOptions="@this.templateDataSourceOptions" ConfigurationHeaderMessage="@T("A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself.")"/>
|
||||
}
|
||||
}
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
|
||||
@T("Example Conversation")
|
||||
</MudText>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@ -59,6 +60,18 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
[Parameter]
|
||||
public bool AllowProfileUsage { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The tools this template preselects, or null when it says nothing about tools.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public HashSet<string>? ToolIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The data source options this template preselects, or null when it says nothing about them.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public DataSourceOptions? DataSourceOptions { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool CreateFromExistingChatThread { get; set; }
|
||||
|
||||
@ -78,6 +91,10 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
private bool dataIsValid;
|
||||
private List<ContentBlock> dataExampleConversation = [];
|
||||
private HashSet<FileAttachment> fileAttachments = [];
|
||||
private bool preselectTools;
|
||||
private HashSet<string> selectedToolIds = new(StringComparer.Ordinal);
|
||||
private bool preselectDataSources;
|
||||
private DataSourceOptions templateDataSourceOptions = new();
|
||||
private string[] dataIssues = [];
|
||||
private string dataEditingPreviousName = string.Empty;
|
||||
private bool isInlineEditOnGoing;
|
||||
@ -97,6 +114,20 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
// Load the used instance names:
|
||||
this.UsedNames = this.SettingsManager.ConfigurationData.ChatTemplates.Select(x => x.Name.ToLowerInvariant()).ToList();
|
||||
|
||||
//
|
||||
// The two switches below carry the third state of the preselection: switched off, this
|
||||
// template says nothing, and a chat started with it uses the defaults from the chat
|
||||
// options. Their working copies live apart from the parameters, so switching a
|
||||
// preselection off and on again does not throw away what was picked.
|
||||
//
|
||||
this.preselectTools = this.ToolIds is not null;
|
||||
this.selectedToolIds = this.ToolIds is null ? new(StringComparer.Ordinal) : new(this.ToolIds, StringComparer.Ordinal);
|
||||
this.preselectDataSources = this.DataSourceOptions is not null;
|
||||
|
||||
// Saying that this template preselects data sources is already the statement that it wants
|
||||
// them, so the switch inside the selection starts on instead of at its usual default:
|
||||
this.templateDataSourceOptions = this.DataSourceOptions?.CreateCopy() ?? new DataSourceOptions { DisableDataSources = false };
|
||||
|
||||
// When editing, we need to load the data:
|
||||
if(this.IsEditing)
|
||||
{
|
||||
@ -138,11 +169,15 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
ExampleConversation = this.dataExampleConversation,
|
||||
FileAttachments = this.fileAttachments.Select(attachment => attachment.Normalize()).ToList(),
|
||||
AllowProfileUsage = this.AllowProfileUsage,
|
||||
ToolIds = this.preselectTools ? new HashSet<string>(this.selectedToolIds, StringComparer.Ordinal) : null,
|
||||
DataSourceOptions = this.preselectDataSources ? this.templateDataSourceOptions.CreateCopy() : null,
|
||||
|
||||
EnterpriseConfigurationPluginId = Guid.Empty,
|
||||
IsEnterpriseConfiguration = false,
|
||||
};
|
||||
|
||||
private void SetSelectedToolIds(HashSet<string> toolIds) => this.selectedToolIds = toolIds;
|
||||
|
||||
private void RemoveMessage(ContentBlock item)
|
||||
{
|
||||
if (this.IsReadOnly)
|
||||
|
||||
@ -69,6 +69,8 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||
{ x => x.ExampleConversation, chatTemplate.ExampleConversation },
|
||||
{ x => x.FileAttachments, chatTemplate.FileAttachments },
|
||||
{ x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage },
|
||||
{ x => x.ToolIds, chatTemplate.ToolIds },
|
||||
{ x => x.DataSourceOptions, chatTemplate.DataSourceOptions },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ChatTemplateDialog>(T("Edit Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
@ -97,6 +99,8 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||
{ x => x.ExampleConversation, chatTemplate.ExampleConversation },
|
||||
{ x => x.FileAttachments, chatTemplate.FileAttachments },
|
||||
{ x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage },
|
||||
{ x => x.ToolIds, chatTemplate.ToolIds },
|
||||
{ x => x.DataSourceOptions, chatTemplate.DataSourceOptions },
|
||||
};
|
||||
|
||||
await this.DialogService.ShowAsync<ChatTemplateDialog>(T("View Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
@ -128,6 +132,9 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
if (!await this.ConfirmExportOfLocalDataSources(chatTemplate))
|
||||
return;
|
||||
|
||||
await this.CopyChatTemplateLuaToClipboard(chatTemplate);
|
||||
}
|
||||
|
||||
@ -141,10 +148,14 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||
|
||||
if (chatTemplate.FileAttachments.Count == 0)
|
||||
{
|
||||
// That way asks about the local data sources itself, so we must not ask twice:
|
||||
await this.ExportChatTemplateWithSharedAttachmentPaths(chatTemplate);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await this.ConfirmExportOfLocalDataSources(chatTemplate))
|
||||
return;
|
||||
|
||||
this.isPluginDirectoryDialogOpen = true;
|
||||
try
|
||||
{
|
||||
@ -160,6 +171,35 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asks whether to export a template although it preselects data sources of this machine.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The export writes the preselected data source IDs unchanged, which is what makes a template
|
||||
/// usable across an organization — but a local file or folder exists here and nowhere else, so
|
||||
/// its ID points at nothing on the machine reading the plugin. Nothing breaks, the chat simply
|
||||
/// starts without that source, and that is precisely why it has to be said beforehand: nobody
|
||||
/// would notice it afterwards. Exporting anyway is a fair choice, because the rest of the
|
||||
/// template is worth rolling out.
|
||||
/// </remarks>
|
||||
/// <param name="chatTemplate">The chat template about to be exported.</param>
|
||||
/// <returns>True when the export may go ahead.</returns>
|
||||
private async Task<bool> ConfirmExportOfLocalDataSources(ChatTemplate chatTemplate)
|
||||
{
|
||||
var localDataSourceNames = ChatTemplate.GetPreselectedLocalDataSourceNames(chatTemplate, this.SettingsManager.ConfigurationData.DataSources);
|
||||
if (localDataSourceNames.Count == 0)
|
||||
return true;
|
||||
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{ x => x.Message, string.Format(T("This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway?"), string.Join(", ", localDataSourceNames)) },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Export Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
return dialogResult is { Canceled: false };
|
||||
}
|
||||
|
||||
private async Task CopyChatTemplateLuaToClipboard(ChatTemplate chatTemplate)
|
||||
{
|
||||
if (!chatTemplate.TryExportAsConfigurationSection(out var luaCode, out var issue))
|
||||
|
||||
@ -102,4 +102,4 @@
|
||||
</MudLayout>
|
||||
</MudPaper>
|
||||
|
||||
<MudThemeProvider @ref="@this.themeProvider" Theme="@this.ColorTheme" IsDarkMode="@this.useDarkMode" />
|
||||
<MudThemeProvider @ref="@this.themeProvider" Theme="@this.ColorTheme" IsDarkMode="@this.useDarkMode" ObserveSystemThemeChange="@this.FollowSystemTheme" />
|
||||
@ -144,7 +144,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
// Send a message to start the plugin system:
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.STARTUP_PLUGIN_SYSTEM);
|
||||
|
||||
await this.themeProvider.WatchSystemDarkModeAsync(this.SystemeThemeChanged);
|
||||
await this.themeProvider.WatchSystemDarkModeAsync(this.SystemThemeChanged);
|
||||
this.CircuitState.ConnectionRestored += this.OnConnectionRestored;
|
||||
await this.UpdateThemeConfiguration();
|
||||
this.LoadNavItems();
|
||||
this.LoadEmbeddingItem();
|
||||
@ -564,15 +565,45 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SystemeThemeChanged(bool isDark)
|
||||
/// <summary>
|
||||
/// True, when the user wants AI Studio to follow the light or dark mode of the operating system.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This also decides whether the MudThemeProvider watches the operating system at all. On a system
|
||||
/// change, the provider takes the new mode into its own state first and calls our handler only
|
||||
/// afterward, so the handler cannot prevent it. Nor can a new render of this layout undo it: the
|
||||
/// provider takes over its IsDarkMode parameter only when that value changes, and with a fixed theme,
|
||||
/// it never does. Were the provider watching while the user chose a fixed theme, MudBlazor would show
|
||||
/// the colors of the system from the next render on, while the rest of the app kept the chosen ones.
|
||||
/// </remarks>
|
||||
private bool FollowSystemTheme => this.SettingsManager.ConfigurationData.App.PreferredTheme is Themes.SYSTEM;
|
||||
|
||||
private async Task SystemThemeChanged(bool isDark)
|
||||
{
|
||||
this.Logger.LogInformation($"The system theme changed to {(isDark ? "dark" : "light")}.");
|
||||
await this.UpdateThemeConfiguration();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the color theme anew once the browser connection of this circuit returned.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The browser reports a change of the system theme exactly once. Blazor drops that report while the
|
||||
/// connection is down, which happens when the machine switches its theme during sleep and wakes up
|
||||
/// again. Since the circuit survives the sleep (cf. the retention settings in Program.cs), no reload
|
||||
/// reads the theme anew either, so AI Studio would keep the theme it had before the sleep.
|
||||
/// <br/><br/>
|
||||
/// The update is deliberately not awaited: this handler runs while Blazor is still completing the
|
||||
/// reconnection, and the answer to the JavaScript call inside can only arrive afterward.
|
||||
/// </remarks>
|
||||
private void OnConnectionRestored()
|
||||
{
|
||||
this.InvokeAsync(this.UpdateThemeConfiguration).Observe($"{nameof(MainLayout)}: reading the color theme after the connection returned");
|
||||
}
|
||||
|
||||
private async Task UpdateThemeConfiguration()
|
||||
{
|
||||
if (this.SettingsManager.ConfigurationData.App.PreferredTheme is Themes.SYSTEM)
|
||||
if (this.FollowSystemTheme)
|
||||
this.useDarkMode = await this.themeProvider.GetSystemDarkModeAsync();
|
||||
else
|
||||
this.useDarkMode = this.SettingsManager.ConfigurationData.App.PreferredTheme == Themes.DARK;
|
||||
@ -664,6 +695,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
public void Dispose()
|
||||
{
|
||||
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
|
||||
this.CircuitState.ConnectionRestored -= this.OnConnectionRestored;
|
||||
this.MessageBus.Unregister(this);
|
||||
this.mandatoryInfoDialogSemaphore.Dispose();
|
||||
}
|
||||
|
||||
@ -147,10 +147,13 @@ ASSISTANT = {
|
||||
- `OPEN_TEMPORARY_CHAT` must not carry a `WorkspaceName`. A name next to it stops the plugin from loading rather than being ignored, so a leftover or mistyped name cannot silently turn a workspace launcher into a disappearing one.
|
||||
- Omitted optional IDs use the chat defaults active when the tile is opened. An explicit empty GUID selects no profile or no chat template; an empty provider or data-source GUID is invalid.
|
||||
- `ProviderId` overrides both the chat-specific and app-wide default provider. It must name a provider that is permitted for chats at the required confidence level.
|
||||
- Explicit data sources are enabled and manually preselected, automatic source selection is disabled, and the normal automatic-validation setting is retained. Every referenced source must currently be available and permitted for the effective provider.
|
||||
- Explicit data sources are enabled and manually preselected, automatic source selection is disabled, and the normal automatic-validation setting is retained. Every referenced source must currently be available and permitted for the effective provider. This describes a launcher whose chat template brings no data source options of its own; see the rule below for the case where it does.
|
||||
- Invalid or unavailable references stop the launch with an error before a workspace or chat is created.
|
||||
- A selected chat template supplies the chat system prompt, profile allowance, predefined user prompt, attachments, and cloned example conversation. A launcher `SystemPrompt`, if retained in an older plugin, is ignored, so there is never a second competing system prompt.
|
||||
- When the selected chat template does not allow profiles, the template wins: the launcher `ProfileId` is dropped and the chat starts without a profile. This matches the disabled profile selection such a template produces in the chat.
|
||||
- A chat template may preselect tools and data sources as well. When it does, it decides them alone: the launcher `ToolIds` and `DataSourceIds` are dropped, and AI Studio writes a warning into the log naming both sides. The rule is the same for tools and for data sources, so there is only one to remember.
|
||||
- The reason the template wins as a whole rather than field by field is a difference in what the two can express. A launcher can only ever say "these sources, picked by hand", while a chat template carries the whole data source options and can also say "let an agent pick the sources for each message". A mix of both would be something neither of them asked for.
|
||||
- The data sources of a chat template are checked exactly like the ones of a launcher: a source which no longer exists, or which is not permitted for the effective provider, stops the launch before a workspace or chat is created. The message then names the chat template as the cause, not the launcher. A template which leaves the choice to an agent names no source and is therefore not checked here; that decision is made per message in the chat.
|
||||
- The predefined user prompt and the attachments of the selected chat template are placed into the chat input, unless the user already has an unsent draft there.
|
||||
|
||||
### Editing a launcher in AI Studio
|
||||
|
||||
@ -454,12 +454,18 @@ ASSISTANT = {
|
||||
["ProviderId"] = "<optional provider GUID; omit to use the chat default>",
|
||||
["ProfileId"] = "<optional profile GUID; use the empty GUID for no profile>",
|
||||
["ChatTemplateId"] = "<optional chat template GUID; use the empty GUID for no template>",
|
||||
-- Optional: the data sources the chat starts with. A chat template chosen above may bring
|
||||
-- data source options of its own. It then decides them alone, the IDs named here are dropped,
|
||||
-- and AI Studio writes a warning into the log. Only a chat template can also say that the AI
|
||||
-- picks the sources for each message, which is why it wins as a whole instead of field by
|
||||
-- field.
|
||||
["DataSourceIds"] = {
|
||||
"<optional data source GUID>",
|
||||
},
|
||||
-- Optional: the tools preselected when the chat opens. Users may change the selection
|
||||
-- in the chat afterwards, and every tool has to meet the confidence requirements of the
|
||||
-- provider in use. A tool ID unknown to the installation is ignored.
|
||||
-- provider in use. A tool ID unknown to the installation is ignored. The same rule as for the
|
||||
-- data sources applies here: a chat template which names tools of its own wins over this list.
|
||||
-- Tool IDs include: web_search, read_web_page
|
||||
["ToolIds"] = {
|
||||
"<optional tool ID>",
|
||||
|
||||
@ -1034,6 +1034,56 @@ CONFIG["CHAT_TEMPLATES"] = {}
|
||||
-- }
|
||||
-- }
|
||||
|
||||
-- An example chat template which preselects tools and data sources:
|
||||
-- Both are optional and independent of each other. Leaving a field out is not the same as
|
||||
-- leaving it empty:
|
||||
--
|
||||
-- ToolIds omitted -> the chat starts with the tools set as its default
|
||||
-- ToolIds = {} -> the chat starts with no tools at all
|
||||
-- DataSourceOptions omitted -> the chat starts with the data source defaults
|
||||
-- DataSourceOptions = { ... } -> the chat starts with exactly what this table says
|
||||
--
|
||||
-- Both are a preselection, not a limit: users change either of them in the chat as usual.
|
||||
-- CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
|
||||
-- ["Id"] = "00000000-0000-0000-0000-000000000002",
|
||||
-- ["Name"] = "Intranet Research",
|
||||
-- ["SystemPrompt"] = "You are <Company Name>'s research assistant. Answer from our own documents and say where each answer comes from.",
|
||||
-- ["AllowProfileUsage"] = true,
|
||||
--
|
||||
-- -- Optional: the tools a chat with this template starts with, by tool ID.
|
||||
-- -- A tool ID unknown to the installation is ignored, and so is a tool your
|
||||
-- -- organization switched off. A tool has to meet the confidence requirements of the
|
||||
-- -- provider in use, so it may stay unavailable even though this template names it.
|
||||
-- -- Tool IDs include: web_search, read_web_page
|
||||
-- ["ToolIds"] = {
|
||||
-- "read_web_page",
|
||||
-- },
|
||||
--
|
||||
-- -- Optional: the data source options a chat with this template starts with.
|
||||
-- -- Every field inside is optional as well. DisableDataSources defaults to false here,
|
||||
-- -- because writing this table at all says that the template wants data sources; the
|
||||
-- -- other three default to false and an empty list.
|
||||
-- ["DataSourceOptions"] = {
|
||||
-- -- Set to true to start the chat with data sources switched off.
|
||||
-- ["DisableDataSources"] = false,
|
||||
--
|
||||
-- -- Let an agent choose the fitting data sources for each question. When true,
|
||||
-- -- PreselectedDataSourceIds is not used.
|
||||
-- ["AutomaticDataSourceSelection"] = false,
|
||||
--
|
||||
-- -- Let an agent check whether the retrieved data fits the question.
|
||||
-- ["AutomaticValidation"] = true,
|
||||
--
|
||||
-- -- Must contain IDs from CONFIG["DATA_SOURCES"] or user-configured data sources.
|
||||
-- -- IDs from another configuration of your organization work as well: they are
|
||||
-- -- resolved against every known data source, not only against the ones defined
|
||||
-- -- here. IDs that resolve to nothing are ignored.
|
||||
-- ["PreselectedDataSourceIds"] = {
|
||||
-- "00000000-0000-0000-0000-000000000000",
|
||||
-- },
|
||||
-- },
|
||||
-- }
|
||||
|
||||
-- Introduction texts shown as expansion panels on the welcome page:
|
||||
CONFIG["INTRODUCTIONS"] = {}
|
||||
|
||||
|
||||
@ -2193,6 +2193,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER
|
||||
-- View
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T1582017048"] = "Anzeigen"
|
||||
|
||||
-- Improve further
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T1582753277"] = "Weiter verbessern"
|
||||
|
||||
-- Separate context, task, constraints, and output format with headings or markers.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T1626024580"] = "Trennen Sie Kontext, Aufgabe, Einschränkungen und Ausgabeformat mit Überschriften oder Markierungen."
|
||||
|
||||
@ -2289,6 +2292,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER
|
||||
-- Use sequential steps
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T487578804"] = "Schrittweise vorgehen"
|
||||
|
||||
-- Moves the optimized prompt into the prompt field so you can optimize it again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T502438377"] = "Übernimmt den optimierten Prompt als neue Eingabe, damit Sie ihn erneut optimieren können."
|
||||
|
||||
-- Use clear, explicit instructions and directly state quality expectations.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T596557540"] = "Verwenden Sie klare, explizite Anweisungen und geben Sie direkt die Qualitätsmerkmale an."
|
||||
|
||||
@ -3603,6 +3609,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2036185364"] = "Code"
|
||||
-- plus {0} image(s), which is more than the {1} this model accepts
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2059172343"] = "plus {0} Bild(er), also mehr als die {1}, die dieses Modell akzeptiert"
|
||||
|
||||
-- Are you sure you want to start a new chat? All unsaved changes will be lost.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2111282488"] = "Möchten Sie wirklich einen neuen Chat starten? Alle nicht gespeicherten Änderungen gehen verloren."
|
||||
|
||||
-- Unsaved Changes
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2123670756"] = "Nicht gespeicherte Änderungen"
|
||||
|
||||
-- Start New Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2310454789"] = "Neuen Chat starten"
|
||||
|
||||
-- Italic
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2377171085"] = "Kursiv"
|
||||
|
||||
@ -4005,6 +4020,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = "
|
||||
-- Chat profile
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2412069346"] = "Chat-Profil"
|
||||
|
||||
-- The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2545184598"] = "Die ausgewählte Chat-Vorlage bringt eigene Datenquellen mit; diese haben Vorrang vor einer hier getroffenen Auswahl. Nur eine Vorlage kann die Auswahl der Quellen auch der KI überlassen, weshalb allein die Vorlage darüber entscheidet."
|
||||
|
||||
-- {0} data source(s) selected
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2777836629"] = "{0} Datenquelle(n) ausgewählt"
|
||||
|
||||
@ -4023,6 +4041,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = "
|
||||
-- Use the normal chat data source defaults
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3898572329"] = "Die Standardwerte der Datenquelle für den normalen Chat verwenden"
|
||||
|
||||
-- The chosen chat template brings tools of its own, and those win over a selection made here.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T4038774259"] = "Die ausgewählte Chat-Vorlage bringt eigene Werkzeuge mit; diese haben Vorrang vor einer hier getroffenen Auswahl."
|
||||
|
||||
-- Use no chat template
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T4258819635"] = "Kein Chat-Template verwenden"
|
||||
|
||||
@ -5289,9 +5310,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T588743762"] = "Während d
|
||||
-- The transcription result is empty.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T974954792"] = "Das Ergebnis der Transkription ist leer."
|
||||
|
||||
-- Are you sure you want to delete the chat '{0}' in the workspace '{1}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1016188706"] = "Möchten Sie den Chat „{0}“ im Arbeitsbereich „{1}“ wirklich löschen?"
|
||||
|
||||
-- Do you want to copy this chat? Your unsaved changes move into the copy, and the original chat keeps the state it was last saved with.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1047391993"] = "Möchten Sie diesen Chat kopieren? Ihre ungespeicherten Änderungen wandern in die Kopie, und der ursprüngliche Chat behält den zuletzt gespeicherten Stand."
|
||||
|
||||
@ -5346,9 +5364,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2151341762"] = "Möchten Sie
|
||||
-- Are you sure you want to create a another chat? All unsaved changes will be lost.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2237618267"] = "Möchten Sie wirklich einen neuen Chat erstellen? Alle nicht gespeicherten Änderungen gehen verloren."
|
||||
|
||||
-- Delete Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2244038752"] = "Chat löschen"
|
||||
|
||||
-- Please enter a chat name.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2301651387"] = "Bitte geben Sie einen Namen für diesen Chat ein."
|
||||
|
||||
@ -5358,9 +5373,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2446263209"] = "Name des Arb
|
||||
-- Move to workspace
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2509305748"] = "In einen Arbeitsbereich verschieben"
|
||||
|
||||
-- Are you sure you want to delete the chat '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3043761007"] = "Sind Sie sicher, dass Sie den Chat „{0}“ löschen möchten?"
|
||||
|
||||
-- Move Chat to Workspace
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3045856778"] = "Chat in den Arbeitsbereich verschieben"
|
||||
|
||||
@ -5694,12 +5706,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1352914344"] = "Im Bear
|
||||
-- Please enter a message for the example conversation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1362948628"] = "Bitte gib eine Nachricht für die Beispiel-Konversation ein."
|
||||
|
||||
-- No, chats keep the tools from your chat options
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1363645855"] = "Nein, Chats behalten die Werkzeuge aus Ihren Chat-Optionen"
|
||||
|
||||
-- The chat template name must be unique; the chosen name is already in use.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "Der Name der Chat-Vorlage muss eindeutig sein; der gewählte Name wird bereits verwendet."
|
||||
|
||||
-- The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1442266827"] = "Das Gleiche gilt für Ihre Daten. Eine Chat-Vorlage kann eigene Datenquellen-Optionen mitbringen – dazu gehört auch, die Auswahl der Quellen der KI zu überlassen. Ohne solche Optionen starten diese Chats mit den Datenquellen-Optionen aus Ihren Chat-Optionen."
|
||||
|
||||
-- Please enter a name for the chat template.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Bitte geben Sie einen Namen für die Chat-Vorlage ein."
|
||||
|
||||
-- Yes, this template decides which data a chat starts with
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T17861006"] = "Ja, diese Vorlage legt fest, mit welchen Daten ein Chat startet"
|
||||
|
||||
-- Load predefined user input from file
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Vordefinierte Benutzereingabe aus Datei laden"
|
||||
|
||||
@ -5721,6 +5742,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "Dateian
|
||||
-- Role
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Rolle"
|
||||
|
||||
-- Yes, this template decides which tools a chat starts with
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2494694135"] = "Ja, diese Vorlage legt fest, mit welchen Werkzeugen ein Chat startet"
|
||||
|
||||
-- Tools
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2499909372"] = "Werkzeuge"
|
||||
|
||||
-- What predefined user input do you want to use?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2501284417"] = "Welche vordefinierte Benutzereingabe möchten Sie verwenden?"
|
||||
|
||||
@ -5766,6 +5793,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Sind Si
|
||||
-- Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Die gleichzeitige Verwendung einiger Chat-Vorlagen mit Profilen kann zu Problemen führen. Deshalb könnten Sie hier die Nutzung von Profilen untersagen."
|
||||
|
||||
-- No, chats keep the data source options from your chat options
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "Nein, Chats behalten die Datenquellen-Optionen aus Ihren Chat-Optionen"
|
||||
|
||||
-- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "Eine Chat-Vorlage kann festlegen, mit welchen Werkzeugen ein Chat startet. Ohne eine solche Festlegung starten diese Chats mit den Werkzeugen, die Sie in den Chat-Optionen als Standard ausgewählt haben. Legen Sie es fest und wählen dann nichts aus, ist das eine andere Aussage: Solche Chats starten ohne jedes Werkzeug, ganz gleich, was Ihr Standard vorsieht."
|
||||
|
||||
-- Add a message
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Nachricht hinzufügen"
|
||||
|
||||
@ -5784,6 +5817,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3675108201"] = "Ja, Pro
|
||||
-- Add a new message below
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3757779731"] = "Neue Nachricht unten hinzufügen"
|
||||
|
||||
-- Does this chat template preselect data sources?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3779813414"] = "Wählt diese Chat-Vorlage Datenquellen vorab aus?"
|
||||
|
||||
-- Example Conversation
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T380891852"] = "Beispiel-Konversation"
|
||||
|
||||
@ -5796,6 +5832,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3883091650"] = "System-
|
||||
-- Messages per page
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3893704289"] = "Nachrichten pro Seite"
|
||||
|
||||
-- Does this chat template preselect tools?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T399377711"] = "Wählt diese Chat-Vorlage Werkzeuge vorab aus?"
|
||||
|
||||
-- Use the default system prompt
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4051106111"] = "Verwenden Sie den Standard-System-Prompt"
|
||||
|
||||
@ -5808,15 +5847,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Erstell
|
||||
-- Enter a message
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Nachricht eingeben"
|
||||
|
||||
-- Data Sources
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T558345131"] = "Datenquellen"
|
||||
|
||||
-- System Prompt
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System-Prompt"
|
||||
|
||||
-- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "Ein mit dieser Vorlage gestarteter Chat beginnt mit diesen Datenquellen und Optionen. Alles davon lässt sich im Chat selbst weiterhin ändern."
|
||||
|
||||
-- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "Die Auswahl lässt sich im Chat weiterhin ändern, und jedes Werkzeug muss die Vertrauensanforderungen des verwendeten Anbieters erfüllen."
|
||||
|
||||
-- Allow the use of profiles together with this chat template?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Erlauben Sie die Verwendung von Profilen zusammen mit dieser Chat-Vorlage?"
|
||||
|
||||
-- Cancel
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Abbrechen"
|
||||
|
||||
-- Preselected tools
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T975962532"] = "Vorausgewählte Werkzeuge"
|
||||
|
||||
-- {0} LLM providers
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T121235760"] = "{0} LLM-Anbieter"
|
||||
|
||||
@ -7812,6 +7863,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T20545
|
||||
-- No chat templates configured yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "Noch keine Chat-Vorlagen konfiguriert."
|
||||
|
||||
-- This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2563631533"] = "Diese Chat-Vorlage wählt Datenquellen vorab aus, die es nur auf diesem Rechner gibt: {0}. Solche Quellen lassen sich nicht bereitstellen; ein Chat, der auf einem anderen Rechner mit dieser Vorlage startet, beginnt daher ohne sie. Möchten Sie die Vorlage trotzdem exportieren?"
|
||||
|
||||
-- Chat Template Name
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T275026390"] = "Name der Chat-Vorlage"
|
||||
|
||||
@ -10113,6 +10167,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T265391888"] = "Das ausgewäh
|
||||
-- The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2819996431"] = "Der Anbieter „{0}“ konnte nicht erreicht werden. Bitte prüfen Sie, ob er läuft und erreichbar ist, und versuchen Sie es anschließend erneut."
|
||||
|
||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). The provider turned the request down with the status code {2} and would turn it down again, so we stopped trying. The provider message is: '{3}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2993640453"] = "Wir haben versucht, mit dem LLM-Anbieter „{0}“ (Typ={1}) zu kommunizieren. Der Anbieter hat die Anfrage mit dem Statuscode {2} abgelehnt und würde sie erneut ablehnen, daher haben wir keine weiteren Versuche unternommen. Die Nachricht des Anbieters lautet: „{3}“"
|
||||
|
||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). Something was not found. The provider message is: '{2}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3014737766"] = "Wir haben versucht, mit dem LLM-Anbieter „{0}“ (Typ={1}) zu kommunizieren. Etwas wurde nicht gefunden. Die Nachricht des Anbieters lautet: „{2}“"
|
||||
|
||||
@ -12162,12 +12219,21 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DATASOURCELOCALRETRIEVALSERVICE::T93
|
||||
-- The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T103791004"] = "Die folgenden vom Chat-Schnellstart-Assistenten ausgewählten Datenquellen sind derzeit nicht verfügbar oder für den ausgewählten Anbieter nicht zugelassen: {0}"
|
||||
|
||||
-- The following data sources selected by the chat template '{0}' are currently unavailable or not permitted for the selected provider: {1}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T1164564929"] = "Die folgenden von der Chat-Vorlage „{0}“ ausgewählten Datenquellen sind derzeit nicht verfügbar oder für den ausgewählten Anbieter nicht zugelassen: {1}"
|
||||
|
||||
-- The chat template '{0}' references data source '{1}', but that data source does not exist.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T1951110186"] = "Die Chat-Vorlage „{0}“ verweist auf die Datenquelle „{1}“, diese Datenquelle existiert jedoch nicht."
|
||||
|
||||
-- The assistant chat launcher references profile '{0}', but that profile does not exist.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T2466659933"] = "Der Chat-Schnellstart-Assistent verweist auf das Profil „{0}“, aber dieses Profil existiert nicht."
|
||||
|
||||
-- The assistant chat launcher references data source '{0}', but that data source does not exist.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T289191545"] = "Der Chat-Schnellstart-Assistent verweist auf die Datenquelle „{0}“, aber diese Datenquelle existiert nicht."
|
||||
|
||||
-- The chat template '{0}' selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3082876173"] = "Die Chat-Vorlage „{0}“ wählt Datenquellen aus, aber für Chats ist kein Anbieter verfügbar. Bitte wählen Sie zuerst einen Standardanbieter für Chats aus. Es wurde kein Chat erstellt."
|
||||
|
||||
-- The data sources selected by the assistant chat launcher could not be checked. No chat was created.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3232401465"] = "Die vom Chat-Schnellstart-Assistenten ausgewählten Datenquellen konnten nicht geprüft werden. Es wurde kein Chat erstellt."
|
||||
|
||||
@ -12177,6 +12243,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3242713584"] = "
|
||||
-- The provider '{0}' selected by the assistant chat launcher is not permitted for chats at the required confidence level.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3491209726"] = "Der vom Chat-Schnellstart-Assistenten ausgewählte Anbieter „{0}“ ist für Chats mit der erforderlichen Zuverlässigkeitsstufe nicht zugelassen."
|
||||
|
||||
-- The data sources selected by the chat template '{0}' could not be checked. No chat was created.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T361525913"] = "Die von der Chat-Vorlage „{0}“ ausgewählten Datenquellen konnten nicht geprüft werden. Es wurde kein Chat erstellt."
|
||||
|
||||
-- The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3780395901"] = "Der Chat-Schnellstart-Assistent wählt Datenquellen aus, aber für Chats ist kein Anbieter verfügbar. Bitte wählen Sie zuerst einen Standardanbieter für Chats aus. Es wurde kein Chat erstellt."
|
||||
|
||||
@ -12904,7 +12973,7 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T649507886"] =
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T818893091"] = "Bitte wählen Sie ein Modell aus."
|
||||
|
||||
-- Are you sure you want to delete the chat '{0}' in the workspace '{1}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1016188706"] = "Möchten Sie den Chat '{0}' im Arbeitsbereich '{1}' wirklich löschen?"
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1016188706"] = "Möchten Sie den Chat „{0}“ im Arbeitsbereich „{1}“ wirklich löschen?"
|
||||
|
||||
-- Unnamed workspace
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unbenannter Arbeitsbereich"
|
||||
@ -12913,7 +12982,7 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unbenannt
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Chat löschen"
|
||||
|
||||
-- Are you sure you want to delete the temporary chat '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3043761007"] = "Möchten Sie den temporären Chat '{0}' wirklich löschen?"
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3043761007"] = "Möchten Sie den temporären Chat „{0}“ wirklich löschen?"
|
||||
|
||||
-- Unnamed chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3310482275"] = "Unbenannter Chat"
|
||||
|
||||
@ -2193,6 +2193,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER
|
||||
-- View
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T1582017048"] = "View"
|
||||
|
||||
-- Improve further
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T1582753277"] = "Improve further"
|
||||
|
||||
-- Separate context, task, constraints, and output format with headings or markers.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T1626024580"] = "Separate context, task, constraints, and output format with headings or markers."
|
||||
|
||||
@ -2289,6 +2292,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER
|
||||
-- Use sequential steps
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T487578804"] = "Use sequential steps"
|
||||
|
||||
-- Moves the optimized prompt into the prompt field so you can optimize it again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T502438377"] = "Moves the optimized prompt into the prompt field so you can optimize it again."
|
||||
|
||||
-- Use clear, explicit instructions and directly state quality expectations.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T596557540"] = "Use clear, explicit instructions and directly state quality expectations."
|
||||
|
||||
@ -3603,6 +3609,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2036185364"] = "Code"
|
||||
-- plus {0} image(s), which is more than the {1} this model accepts
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2059172343"] = "plus {0} image(s), which is more than the {1} this model accepts"
|
||||
|
||||
-- Are you sure you want to start a new chat? All unsaved changes will be lost.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2111282488"] = "Are you sure you want to start a new chat? All unsaved changes will be lost."
|
||||
|
||||
-- Unsaved Changes
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2123670756"] = "Unsaved Changes"
|
||||
|
||||
-- Start New Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2310454789"] = "Start New Chat"
|
||||
|
||||
-- Italic
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2377171085"] = "Italic"
|
||||
|
||||
@ -4005,6 +4020,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = "
|
||||
-- Chat profile
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2412069346"] = "Chat profile"
|
||||
|
||||
-- The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2545184598"] = "The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own."
|
||||
|
||||
-- {0} data source(s) selected
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2777836629"] = "{0} data source(s) selected"
|
||||
|
||||
@ -4023,6 +4041,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = "
|
||||
-- Use the normal chat data source defaults
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3898572329"] = "Use the normal chat data source defaults"
|
||||
|
||||
-- The chosen chat template brings tools of its own, and those win over a selection made here.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T4038774259"] = "The chosen chat template brings tools of its own, and those win over a selection made here."
|
||||
|
||||
-- Use no chat template
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T4258819635"] = "Use no chat template"
|
||||
|
||||
@ -5289,9 +5310,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T588743762"] = "An error o
|
||||
-- The transcription result is empty.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T974954792"] = "The transcription result is empty."
|
||||
|
||||
-- Are you sure you want to delete the chat '{0}' in the workspace '{1}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1016188706"] = "Are you sure you want to delete the chat '{0}' in the workspace '{1}'?"
|
||||
|
||||
-- Do you want to copy this chat? Your unsaved changes move into the copy, and the original chat keeps the state it was last saved with.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1047391993"] = "Do you want to copy this chat? Your unsaved changes move into the copy, and the original chat keeps the state it was last saved with."
|
||||
|
||||
@ -5346,9 +5364,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2151341762"] = "Are you sure
|
||||
-- Are you sure you want to create a another chat? All unsaved changes will be lost.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2237618267"] = "Are you sure you want to create a another chat? All unsaved changes will be lost."
|
||||
|
||||
-- Delete Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2244038752"] = "Delete Chat"
|
||||
|
||||
-- Please enter a chat name.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2301651387"] = "Please enter a chat name."
|
||||
|
||||
@ -5358,9 +5373,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2446263209"] = "Workspace Na
|
||||
-- Move to workspace
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T2509305748"] = "Move to workspace"
|
||||
|
||||
-- Are you sure you want to delete the chat '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3043761007"] = "Are you sure you want to delete the chat '{0}'?"
|
||||
|
||||
-- Move Chat to Workspace
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3045856778"] = "Move Chat to Workspace"
|
||||
|
||||
@ -5694,12 +5706,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1352914344"] = "Only te
|
||||
-- Please enter a message for the example conversation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1362948628"] = "Please enter a message for the example conversation."
|
||||
|
||||
-- No, chats keep the tools from your chat options
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1363645855"] = "No, chats keep the tools from your chat options"
|
||||
|
||||
-- The chat template name must be unique; the chosen name is already in use.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "The chat template name must be unique; the chosen name is already in use."
|
||||
|
||||
-- The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1442266827"] = "The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options."
|
||||
|
||||
-- Please enter a name for the chat template.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Please enter a name for the chat template."
|
||||
|
||||
-- Yes, this template decides which data a chat starts with
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T17861006"] = "Yes, this template decides which data a chat starts with"
|
||||
|
||||
-- Load predefined user input from file
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Load predefined user input from file"
|
||||
|
||||
@ -5721,6 +5742,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "File At
|
||||
-- Role
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Role"
|
||||
|
||||
-- Yes, this template decides which tools a chat starts with
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2494694135"] = "Yes, this template decides which tools a chat starts with"
|
||||
|
||||
-- Tools
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2499909372"] = "Tools"
|
||||
|
||||
-- What predefined user input do you want to use?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2501284417"] = "What predefined user input do you want to use?"
|
||||
|
||||
@ -5766,6 +5793,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Are you
|
||||
-- Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here."
|
||||
|
||||
-- No, chats keep the data source options from your chat options
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "No, chats keep the data source options from your chat options"
|
||||
|
||||
-- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says."
|
||||
|
||||
-- Add a message
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message"
|
||||
|
||||
@ -5784,6 +5817,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3675108201"] = "Yes, al
|
||||
-- Add a new message below
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3757779731"] = "Add a new message below"
|
||||
|
||||
-- Does this chat template preselect data sources?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3779813414"] = "Does this chat template preselect data sources?"
|
||||
|
||||
-- Example Conversation
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T380891852"] = "Example Conversation"
|
||||
|
||||
@ -5796,6 +5832,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3883091650"] = "Load sy
|
||||
-- Messages per page
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3893704289"] = "Messages per page"
|
||||
|
||||
-- Does this chat template preselect tools?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T399377711"] = "Does this chat template preselect tools?"
|
||||
|
||||
-- Use the default system prompt
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4051106111"] = "Use the default system prompt"
|
||||
|
||||
@ -5808,15 +5847,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Create
|
||||
-- Enter a message
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Enter a message"
|
||||
|
||||
-- Data Sources
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T558345131"] = "Data Sources"
|
||||
|
||||
-- System Prompt
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System Prompt"
|
||||
|
||||
-- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself."
|
||||
|
||||
-- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use."
|
||||
|
||||
-- Allow the use of profiles together with this chat template?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Allow the use of profiles together with this chat template?"
|
||||
|
||||
-- Cancel
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel"
|
||||
|
||||
-- Preselected tools
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T975962532"] = "Preselected tools"
|
||||
|
||||
-- {0} LLM providers
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T121235760"] = "{0} LLM providers"
|
||||
|
||||
@ -7812,6 +7863,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T20545
|
||||
-- No chat templates configured yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "No chat templates configured yet."
|
||||
|
||||
-- This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2563631533"] = "This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway?"
|
||||
|
||||
-- Chat Template Name
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T275026390"] = "Chat Template Name"
|
||||
|
||||
@ -10113,6 +10167,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T265391888"] = "The selected
|
||||
-- The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2819996431"] = "The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again."
|
||||
|
||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). The provider turned the request down with the status code {2} and would turn it down again, so we stopped trying. The provider message is: '{3}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2993640453"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The provider turned the request down with the status code {2} and would turn it down again, so we stopped trying. The provider message is: '{3}'"
|
||||
|
||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). Something was not found. The provider message is: '{2}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3014737766"] = "We tried to communicate with the LLM provider '{0}' (type={1}). Something was not found. The provider message is: '{2}'"
|
||||
|
||||
@ -12162,12 +12219,21 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DATASOURCELOCALRETRIEVALSERVICE::T93
|
||||
-- The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T103791004"] = "The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}"
|
||||
|
||||
-- The following data sources selected by the chat template '{0}' are currently unavailable or not permitted for the selected provider: {1}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T1164564929"] = "The following data sources selected by the chat template '{0}' are currently unavailable or not permitted for the selected provider: {1}"
|
||||
|
||||
-- The chat template '{0}' references data source '{1}', but that data source does not exist.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T1951110186"] = "The chat template '{0}' references data source '{1}', but that data source does not exist."
|
||||
|
||||
-- The assistant chat launcher references profile '{0}', but that profile does not exist.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T2466659933"] = "The assistant chat launcher references profile '{0}', but that profile does not exist."
|
||||
|
||||
-- The assistant chat launcher references data source '{0}', but that data source does not exist.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T289191545"] = "The assistant chat launcher references data source '{0}', but that data source does not exist."
|
||||
|
||||
-- The chat template '{0}' selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3082876173"] = "The chat template '{0}' selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created."
|
||||
|
||||
-- The data sources selected by the assistant chat launcher could not be checked. No chat was created.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3232401465"] = "The data sources selected by the assistant chat launcher could not be checked. No chat was created."
|
||||
|
||||
@ -12177,6 +12243,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3242713584"] = "
|
||||
-- The provider '{0}' selected by the assistant chat launcher is not permitted for chats at the required confidence level.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3491209726"] = "The provider '{0}' selected by the assistant chat launcher is not permitted for chats at the required confidence level."
|
||||
|
||||
-- The data sources selected by the chat template '{0}' could not be checked. No chat was created.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T361525913"] = "The data sources selected by the chat template '{0}' could not be checked. No chat was created."
|
||||
|
||||
-- The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T3780395901"] = "The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created."
|
||||
|
||||
|
||||
@ -596,17 +596,12 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
/// <remarks>
|
||||
/// Providers word their errors differently, but they all put a sentence somewhere into the
|
||||
/// body. Passing that sentence on is what lets a user act on the problem instead of only
|
||||
/// learning that something went wrong.
|
||||
/// learning that something went wrong. Open to the providers themselves as well, because some
|
||||
/// of them talk to an endpoint of their own rather than through the shared request methods,
|
||||
/// and their users deserve the same explanation.
|
||||
/// </remarks>
|
||||
/// <param name="responseBody">The body of the failed response.</param>
|
||||
/// <returns>The message, or an empty string when the body carries none.</returns>
|
||||
/// <summary>
|
||||
/// Reads what the provider itself said about a failure out of its error response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Available to the providers because some of them talk to an endpoint of their own rather
|
||||
/// than through the shared request methods, and their users deserve the same explanation.
|
||||
/// </remarks>
|
||||
protected static string ReadProviderErrorMessage(string responseBody)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(responseBody))
|
||||
@ -652,6 +647,18 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
return propertyElement.GetString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the message a user gets to see when the chat outgrew what the model reads.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Two answers mean this: one provider says so in the body of a bad request, another turns the
|
||||
/// request down with 413 instead. For the user they are the same thing, and saying it in one
|
||||
/// place is also what keeps both on one I18N key.
|
||||
/// </remarks>
|
||||
/// <param name="providerMessage">What the provider itself said about the failure.</param>
|
||||
/// <returns>The message to show.</returns>
|
||||
private string GetContextTooLargeUserMessage(string? providerMessage) => string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'"), this.InstanceName, this.Provider, providerMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a request and handles rate limiting by exponential backoff.
|
||||
/// </summary>
|
||||
@ -672,6 +679,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
var retry = 0;
|
||||
var response = default(HttpResponseMessage);
|
||||
var errorMessage = string.Empty;
|
||||
var failureAlreadyExplained = false;
|
||||
var lastProviderRequestFailure = ProviderRequestFailureReason.NONE;
|
||||
HttpStatusCode? lastResponseStatusCode = null;
|
||||
var lastResponseReasonPhrase = string.Empty;
|
||||
@ -726,6 +734,32 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Block, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). You might not be able to use this provider from your location. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase)));
|
||||
this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
failureAlreadyExplained = true;
|
||||
break;
|
||||
}
|
||||
|
||||
//
|
||||
// Some providers answer an oversized request with 413 instead of describing the
|
||||
// problem in a 400 body. Handled here rather than below, because this is the one
|
||||
// failure in this loop which cannot get better by being sent again: without its own
|
||||
// branch it falls through to the retry delays, which resend the very same oversized
|
||||
// request for several minutes before the user learns anything at all.
|
||||
//
|
||||
if(nextResponse.StatusCode is HttpStatusCode.RequestEntityTooLarge)
|
||||
{
|
||||
//
|
||||
// The reason phrase of a 413 says no more than "Request Entity Too Large", and a
|
||||
// proxy which refuses the request before the provider sees it sends no body worth
|
||||
// reading. So we show what the body carries and fall back to the phrase:
|
||||
//
|
||||
var tooLargeMessage = ReadProviderErrorMessage(errorBody);
|
||||
if (string.IsNullOrWhiteSpace(tooLargeMessage))
|
||||
tooLargeMessage = nextResponse.ReasonPhrase;
|
||||
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, this.GetContextTooLargeUserMessage(tooLargeMessage)));
|
||||
this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
failureAlreadyExplained = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -755,7 +789,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
else if(errorBody.Contains("context", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
errorBody.Contains("token", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'"), this.InstanceName, this.Provider, badRequestMessage)));
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, this.GetContextTooLargeUserMessage(badRequestMessage)));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -764,6 +798,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
|
||||
this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
failureAlreadyExplained = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -772,6 +807,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). Something was not found. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase)));
|
||||
this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
failureAlreadyExplained = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -780,6 +816,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Key, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The API key might be invalid. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase)));
|
||||
this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
failureAlreadyExplained = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -788,6 +825,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase)));
|
||||
this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
failureAlreadyExplained = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -796,6 +834,32 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The provider is overloaded. The message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase)));
|
||||
this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
failureAlreadyExplained = true;
|
||||
break;
|
||||
}
|
||||
|
||||
//
|
||||
// Everything else the provider answers in the 400 range is about this request itself,
|
||||
// and sending the very same request again cannot change that answer. Only 408 and 429
|
||||
// say "later" rather than "no", and waiting them out is what the delay below exists
|
||||
// for. This branch comes last on purpose: every status code we have a better sentence
|
||||
// for is handled above, and only what is left over ends up with this general wording.
|
||||
//
|
||||
if(nextResponse.StatusCode is not (HttpStatusCode.RequestTimeout or HttpStatusCode.TooManyRequests) && (int)nextResponse.StatusCode is >= 400 and < 500)
|
||||
{
|
||||
//
|
||||
// What the provider said about it, falling back to the reason phrase. The status
|
||||
// code is named as well: this is the branch for refusals we have no wording of our
|
||||
// own for, and then the number is what the user can ask the provider about.
|
||||
//
|
||||
var refusalMessage = ReadProviderErrorMessage(errorBody);
|
||||
if (string.IsNullOrWhiteSpace(refusalMessage))
|
||||
refusalMessage = nextResponse.ReasonPhrase;
|
||||
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The provider turned the request down with the status code {2} and would turn it down again, so we stopped trying. The provider message is: '{3}'"), this.InstanceName, this.Provider, (int)nextResponse.StatusCode, refusalMessage)));
|
||||
this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
failureAlreadyExplained = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -808,7 +872,15 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
await Task.Delay(TimeSpan.FromSeconds(timeSeconds), effectiveCancellationToken);
|
||||
}
|
||||
|
||||
if(retry >= MAX_RETRIES || !string.IsNullOrWhiteSpace(errorMessage))
|
||||
//
|
||||
// Whether this request got an answer at all. The response is set in the success branch and
|
||||
// nowhere else, so its absence is what "we have nothing to hand on" means. Going by the
|
||||
// error message instead was wrong in both directions: a provider which sends no reason
|
||||
// phrase left that message empty, and this method then reported success without a response
|
||||
// for the caller to read; and an attempt which succeeded as the last one the loop allows
|
||||
// was reported as a failure although its answer was right there.
|
||||
//
|
||||
if(response is null)
|
||||
{
|
||||
if (lastProviderRequestFailure is not ProviderRequestFailureReason.NONE)
|
||||
{
|
||||
@ -817,7 +889,16 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
throw new ProviderRequestException(lastProviderRequestFailure, userMessage, lastResponseStatusCode, lastResponseReasonPhrase, lastErrorBody);
|
||||
}
|
||||
|
||||
//
|
||||
// This is the message for a failure nobody was able to explain. Where one of the
|
||||
// branches above named the cause, it has to stay silent: it speaks of all retries
|
||||
// having been spent, while those branches stop after the very first answer. Sending
|
||||
// both leaves the user with two messages which contradict each other, and the one
|
||||
// which explains nothing is the one arriving last.
|
||||
//
|
||||
if(!failureAlreadyExplained)
|
||||
await MessageBus.INSTANCE.SendError(new DataErrorMessage(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). Even after {2} retries, there were some problems with the request. The provider message is: '{3}'."), this.InstanceName, this.Provider, MAX_RETRIES, errorMessage)));
|
||||
|
||||
return new HttpRateLimitedStreamResult(false, true, errorMessage ?? $"Failed after {MAX_RETRIES} retries; no provider message available", response);
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using System.Text;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using SharedTools;
|
||||
@ -27,6 +28,29 @@ public record ChatTemplate(
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The tools this template preselects for a chat started with it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null means the template says nothing about tools, so the chat starts with the tools chosen
|
||||
/// as its default in the app settings. An empty set is the opposite statement: this template
|
||||
/// wants no tools at all, whatever that default says.<br/><br/>
|
||||
/// A preselection, not a limit: the user changes the selection in the chat as usual, and a
|
||||
/// tool still has to meet the confidence requirements of the provider in use.
|
||||
/// </remarks>
|
||||
public HashSet<string>? ToolIds { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The data source options a chat started with this template begins with.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null means the template says nothing, so the chat starts with the data source defaults from
|
||||
/// the app settings. Anything else is the template's own answer, and it carries more than a
|
||||
/// list of sources: whether data sources are used at all, whether an agent picks them, and
|
||||
/// whether the retrieved data is validated.
|
||||
/// </remarks>
|
||||
public DataSourceOptions? DataSourceOptions { get; init; }
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatTemplate).Namespace, nameof(ChatTemplate));
|
||||
|
||||
private static readonly ILogger<ChatTemplate> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ChatTemplate>();
|
||||
@ -41,6 +65,8 @@ public record ChatTemplate(
|
||||
ExampleConversation = [],
|
||||
FileAttachments = [],
|
||||
AllowProfileUsage = true,
|
||||
ToolIds = null,
|
||||
DataSourceOptions = null,
|
||||
EnterpriseConfigurationPluginId = Guid.Empty,
|
||||
IsEnterpriseConfiguration = false,
|
||||
};
|
||||
@ -80,6 +106,71 @@ public record ChatTemplate(
|
||||
return this.SystemPrompt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decides whose tools a chat started by a launcher begins with.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A launcher may name tools itself and may choose a chat template which names tools as well.
|
||||
/// When both do, the template wins as a whole — the same rule as for the data sources, so that
|
||||
/// nobody has to remember two of them.
|
||||
/// </remarks>
|
||||
/// <param name="chatTemplate">The chat template the launcher opens its chat with.</param>
|
||||
/// <param name="launcherToolIds">The tools the launcher names itself, or null when it names none.</param>
|
||||
/// <returns>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.</returns>
|
||||
public static (IReadOnlyCollection<string>? ToolIds, bool LauncherChoiceDropped) ChooseToolIds(ChatTemplate chatTemplate, IReadOnlyCollection<string>? launcherToolIds)
|
||||
{
|
||||
if (chatTemplate.ToolIds is not { } templateToolIds)
|
||||
return (launcherToolIds, false);
|
||||
|
||||
return (templateToolIds, launcherToolIds is not null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decides whose data source options a chat started by a launcher begins with.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The two sides are not equally expressive: a launcher can only ever say "these sources, picked
|
||||
/// by hand", while a chat template carries the whole options and can also say "let an agent pick
|
||||
/// them for each message". Mixing them field by field would produce something neither of them
|
||||
/// asked for, so the template wins as a whole.
|
||||
/// </remarks>
|
||||
/// <param name="chatTemplate">The chat template the launcher opens its chat with.</param>
|
||||
/// <param name="launcherOptions">The options built from the data sources the launcher names, or null when it names none.</param>
|
||||
/// <returns>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.</returns>
|
||||
public static (DataSourceOptions? Options, bool LauncherChoiceDropped) ChooseDataSourceOptions(ChatTemplate chatTemplate, DataSourceOptions? launcherOptions)
|
||||
{
|
||||
if (chatTemplate.DataSourceOptions is not { } templateOptions)
|
||||
return (launcherOptions, false);
|
||||
|
||||
return (templateOptions.CreateCopy(), launcherOptions is not null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Names the preselected data sources which exist on this machine only.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Such a source is a sensible choice inside a chat and a dead end in an export: its ID travels
|
||||
/// into the plugin unchanged, and on the machine which reads that plugin it points at nothing.
|
||||
/// Only ERI sources describe something the whole organization can reach, which is why they are
|
||||
/// also the only ones the app offers an export for.<br/><br/>
|
||||
/// IDs which match no configured source at all are left out. Those are covered by the note the
|
||||
/// export writes above the data source IDs anyway, and the name to warn about is missing.
|
||||
/// </remarks>
|
||||
/// <param name="chatTemplate">The chat template about to be exported.</param>
|
||||
/// <param name="configuredDataSources">The data sources configured on this machine.</param>
|
||||
/// <returns>The names of the preselected local data sources, in the order they are configured in.</returns>
|
||||
public static IReadOnlyList<string> GetPreselectedLocalDataSourceNames(ChatTemplate chatTemplate, IEnumerable<IDataSource> configuredDataSources)
|
||||
{
|
||||
if (chatTemplate.DataSourceOptions is not { PreselectedDataSourceIds.Count: > 0 } options)
|
||||
return [];
|
||||
|
||||
var preselectedIds = options.PreselectedDataSourceIds.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
return configuredDataSources
|
||||
.Where(source => source is IInternalDataSource && preselectedIds.Contains(source.Id))
|
||||
.Select(source => source.Name)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static bool TryParseChatTemplateTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject template)
|
||||
{
|
||||
template = NO_CHAT_TEMPLATE;
|
||||
@ -121,6 +212,8 @@ public record ChatTemplate(
|
||||
ExampleConversation = ParseExampleConversation(idx, table),
|
||||
FileAttachments = fileAttachments,
|
||||
AllowProfileUsage = allowProfileUsage,
|
||||
ToolIds = ParseToolIds(idx, table),
|
||||
DataSourceOptions = ParseDataSourceOptions(idx, table),
|
||||
IsEnterpriseConfiguration = true,
|
||||
EnterpriseConfigurationPluginId = configPluginId,
|
||||
};
|
||||
@ -175,6 +268,89 @@ public record ChatTemplate(
|
||||
return exampleConversation;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A missing list and an empty one mean different things here, so an empty one must not fall
|
||||
/// back to null: the template then states that it wants no tools. The assistant plugins reject
|
||||
/// an empty list instead, because there it carries no meaning at all.
|
||||
/// </remarks>
|
||||
private static HashSet<string>? ParseToolIds(int idx, LuaTable table)
|
||||
{
|
||||
if (!table.TryGetValue("ToolIds", out var toolIdsValue) || !toolIdsValue.TryRead<LuaTable>(out var toolIdsTable))
|
||||
return null;
|
||||
|
||||
var toolIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
var numToolIds = toolIdsTable.ArrayLength;
|
||||
for (var toolNum = 1; toolNum <= numToolIds; toolNum++)
|
||||
{
|
||||
if (!toolIdsTable[toolNum].TryRead<string>(out var toolId) || string.IsNullOrWhiteSpace(toolId))
|
||||
{
|
||||
LOGGER.LogWarning("The ToolIds entry {ToolNum} in chat template {IdxChatTemplate} is not a valid tool ID and will be ignored.", toolNum, idx);
|
||||
continue;
|
||||
}
|
||||
|
||||
toolIds.Add(toolId.Trim());
|
||||
}
|
||||
|
||||
return toolIds;
|
||||
}
|
||||
|
||||
private static DataSourceOptions? ParseDataSourceOptions(int idx, LuaTable table)
|
||||
{
|
||||
if (!table.TryGetValue("DataSourceOptions", out var optionsValue) || !optionsValue.TryRead<LuaTable>(out var optionsTable))
|
||||
return null;
|
||||
|
||||
//
|
||||
// Writing this table at all is already the statement that the template wants data sources,
|
||||
// hence the switch starts enabled here. Everywhere else in the app, data sources start
|
||||
// switched off.
|
||||
//
|
||||
var disableDataSources = false;
|
||||
if (optionsTable.TryGetValue("DisableDataSources", out var disableValue) && disableValue.TryRead<bool>(out var disable))
|
||||
disableDataSources = disable;
|
||||
|
||||
var automaticSelection = false;
|
||||
if (optionsTable.TryGetValue("AutomaticDataSourceSelection", out var automaticSelectionValue) && automaticSelectionValue.TryRead<bool>(out var automaticSelectionFlag))
|
||||
automaticSelection = automaticSelectionFlag;
|
||||
|
||||
var automaticValidation = false;
|
||||
if (optionsTable.TryGetValue("AutomaticValidation", out var automaticValidationValue) && automaticValidationValue.TryRead<bool>(out var automaticValidationFlag))
|
||||
automaticValidation = automaticValidationFlag;
|
||||
|
||||
return new DataSourceOptions
|
||||
{
|
||||
DisableDataSources = disableDataSources,
|
||||
AutomaticDataSourceSelection = automaticSelection,
|
||||
AutomaticValidation = automaticValidation,
|
||||
PreselectedDataSourceIds = ParsePreselectedDataSourceIds(idx, optionsTable),
|
||||
};
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The IDs stay strings instead of being parsed as GUIDs: a data source of another
|
||||
/// configuration may carry an ID which is none, and rejecting it here would make it
|
||||
/// unreferenceable for no gain.
|
||||
/// </remarks>
|
||||
private static List<string> ParsePreselectedDataSourceIds(int idx, LuaTable optionsTable)
|
||||
{
|
||||
var dataSourceIds = new List<string>();
|
||||
if (!optionsTable.TryGetValue("PreselectedDataSourceIds", out var idsValue) || !idsValue.TryRead<LuaTable>(out var idsTable))
|
||||
return dataSourceIds;
|
||||
|
||||
var numIds = idsTable.ArrayLength;
|
||||
for (var idNum = 1; idNum <= numIds; idNum++)
|
||||
{
|
||||
if (!idsTable[idNum].TryRead<string>(out var dataSourceId) || string.IsNullOrWhiteSpace(dataSourceId))
|
||||
{
|
||||
LOGGER.LogWarning("The PreselectedDataSourceIds entry {IdNum} in chat template {IdxChatTemplate} is not a valid data source ID and will be ignored.", idNum, idx);
|
||||
continue;
|
||||
}
|
||||
|
||||
dataSourceIds.Add(dataSourceId.Trim());
|
||||
}
|
||||
|
||||
return dataSourceIds;
|
||||
}
|
||||
|
||||
private static List<FileAttachment> ParseFileAttachments(int idx, LuaTable table, string pluginPath)
|
||||
{
|
||||
var fileAttachments = new List<FileAttachment>();
|
||||
@ -258,15 +434,24 @@ public record ChatTemplate(
|
||||
{
|
||||
issue = string.Empty;
|
||||
var fileAttachmentsLua = this.BuildFileAttachmentsLua(fileAttachmentPaths);
|
||||
|
||||
//
|
||||
// Both of these may be absent entirely, because saying nothing about tools or data sources
|
||||
// is a statement of its own. They therefore bring their own line break and indentation
|
||||
// instead of sitting on a line of the template:
|
||||
//
|
||||
var toolIdsLua = this.BuildToolIdsLua();
|
||||
var dataSourceOptionsLua = this.BuildDataSourceOptionsLua();
|
||||
|
||||
luaCode = $$"""
|
||||
CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
|
||||
{{this.BuildDataSourceIdNote()}}CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
|
||||
["Id"] = "{{LuaTools.EscapeLuaString(exportId)}}",
|
||||
["Name"] = {{LuaTools.ToLuaStringLiteral(this.Name)}},
|
||||
["SystemPrompt"] = {{LuaTools.ToLuaStringLiteral(this.SystemPrompt)}},
|
||||
["PredefinedUserPrompt"] = {{LuaTools.ToLuaStringLiteral(this.PredefinedUserPrompt)}},
|
||||
["AllowProfileUsage"] = {{this.AllowProfileUsage.ToString().ToLowerInvariant()}},
|
||||
["FileAttachments"] = {{fileAttachmentsLua}},
|
||||
["ExampleConversation"] = {{exampleConversationLua}},
|
||||
["ExampleConversation"] = {{exampleConversationLua}},{{toolIdsLua}}{{dataSourceOptionsLua}}
|
||||
}
|
||||
""";
|
||||
return true;
|
||||
@ -376,6 +561,84 @@ public record ChatTemplate(
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// An empty set is written out as an empty table rather than being left out: the two say
|
||||
/// different things, and dropping the line would turn "no tools at all" into "whatever the
|
||||
/// chat default is" on the machine which reads this back.
|
||||
/// </remarks>
|
||||
private string BuildToolIdsLua()
|
||||
{
|
||||
if (this.ToolIds is null)
|
||||
return string.Empty;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine();
|
||||
if (this.ToolIds.Count == 0)
|
||||
{
|
||||
builder.Append(""" ["ToolIds"] = {},""");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
builder.AppendLine(""" ["ToolIds"] = {""");
|
||||
|
||||
//
|
||||
// A set has no order of its own, so exporting the same template twice would otherwise
|
||||
// produce two different files. Sorting keeps the plugin diffs readable:
|
||||
//
|
||||
foreach (var toolId in this.ToolIds.Order(StringComparer.Ordinal))
|
||||
builder.AppendLine($" {LuaTools.ToLuaStringLiteral(toolId)},");
|
||||
|
||||
builder.Append(" },");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private string BuildDataSourceOptionsLua()
|
||||
{
|
||||
if (this.DataSourceOptions is not { } options)
|
||||
return string.Empty;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(""" ["DataSourceOptions"] = {""");
|
||||
builder.AppendLine($""" ["DisableDataSources"] = {options.DisableDataSources.ToString().ToLowerInvariant()},""");
|
||||
builder.AppendLine($""" ["AutomaticDataSourceSelection"] = {options.AutomaticDataSourceSelection.ToString().ToLowerInvariant()},""");
|
||||
builder.AppendLine($""" ["AutomaticValidation"] = {options.AutomaticValidation.ToString().ToLowerInvariant()},""");
|
||||
|
||||
if (options.PreselectedDataSourceIds.Count == 0)
|
||||
builder.AppendLine(""" ["PreselectedDataSourceIds"] = {},""");
|
||||
else
|
||||
{
|
||||
builder.AppendLine(""" ["PreselectedDataSourceIds"] = {""");
|
||||
foreach (var dataSourceId in options.PreselectedDataSourceIds)
|
||||
builder.AppendLine($" {LuaTools.ToLuaStringLiteral(dataSourceId)},");
|
||||
|
||||
builder.AppendLine(" },");
|
||||
}
|
||||
|
||||
builder.Append(" },");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The template itself gets a fresh ID on export, but the data source IDs must not: they point
|
||||
/// at the sources of the organization and only work when both sides agree on them. Nobody can
|
||||
/// see that from the exported code alone, hence this note.
|
||||
/// </remarks>
|
||||
private string BuildDataSourceIdNote()
|
||||
{
|
||||
if (this.DataSourceOptions is not { PreselectedDataSourceIds.Count: > 0 })
|
||||
return string.Empty;
|
||||
|
||||
// The empty line before the closing delimiter is what ends the last comment line. Without
|
||||
// it, the assignment would continue that comment and the whole export would be one comment:
|
||||
return """
|
||||
-- The data source IDs below are the ones of the machine this was exported from.
|
||||
-- Please check them against your CONFIG["DATA_SOURCES"]: an ID which resolves to
|
||||
-- nothing is ignored, and a chat with this template then starts without that source.
|
||||
|
||||
""";
|
||||
}
|
||||
|
||||
private string BuildFileAttachmentsLua(IReadOnlyList<string>? fileAttachmentPaths)
|
||||
{
|
||||
var paths = fileAttachmentPaths ?? this.FileAttachments.Select(attachment => attachment.FilePath).ToList();
|
||||
|
||||
@ -28,6 +28,18 @@ public sealed class CircuitStateService
|
||||
/// </summary>
|
||||
public string CircuitId { get; private set; } = "n/a";
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the browser connection returned after it was lost.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It does not occur for the first connection of a circuit, only for the ones which follow a loss. Use it
|
||||
/// to fetch again what the browser reports on its own: Blazor drops such reports while the connection is
|
||||
/// down, and nothing sends them a second time. The event is raised while Blazor is still completing the
|
||||
/// reconnection, though. A handler must not wait for JavaScript interop, because the browser's answer can
|
||||
/// only be processed once the reconnection has finished. Start such work without awaiting it instead.
|
||||
/// </remarks>
|
||||
public event Action? ConnectionRestored;
|
||||
|
||||
/// <summary>
|
||||
/// Called by the circuit handler when the circuit was opened.
|
||||
/// </summary>
|
||||
@ -37,7 +49,17 @@ public sealed class CircuitStateService
|
||||
/// <summary>
|
||||
/// Called by the circuit handler when the browser connection was established or restored.
|
||||
/// </summary>
|
||||
public void MarkAsConnected() => this.isConnected = true;
|
||||
/// <remarks>
|
||||
/// A restored connection raises ConnectionRestored. Blazor never runs the handler's events of one circuit
|
||||
/// concurrently, so reading and writing the state in two steps is safe here.
|
||||
/// </remarks>
|
||||
public void MarkAsConnected()
|
||||
{
|
||||
var wasConnected = this.isConnected;
|
||||
this.isConnected = true;
|
||||
if (!wasConnected)
|
||||
this.ConnectionRestored?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the circuit handler when the browser connection was lost or the circuit ended.
|
||||
|
||||
@ -45,7 +45,7 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
|
||||
profile = Profile.NO_PROFILE;
|
||||
}
|
||||
|
||||
var dataSourceOptionsResult = await this.ResolveDataSourceOptionsAsync(providerResult.Provider, launchConfiguration.DataSourceIds);
|
||||
var dataSourceOptionsResult = await this.ResolveDataSourceOptionsAsync(assistantPlugin, providerResult.Provider, chatTemplate, launchConfiguration.DataSourceIds);
|
||||
var dataSourceOptions = dataSourceOptionsResult.Options;
|
||||
if (dataSourceOptions is null)
|
||||
return new(null, dataSourceOptionsResult.ErrorMessage);
|
||||
@ -75,15 +75,21 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
|
||||
}
|
||||
}
|
||||
|
||||
var toolChoice = ChatTemplate.ChooseToolIds(chatTemplate, launchConfiguration.ToolIds);
|
||||
if (toolChoice.LauncherChoiceDropped)
|
||||
logger.LogWarning(
|
||||
"Assistant plugin '{PluginName}' selects the tools '{LauncherToolIds}', but its chat template '{ChatTemplateName}' names tools of its own. The chat starts with the tools of that template.",
|
||||
assistantPlugin.Name, string.Join(", ", launchConfiguration.ToolIds!), chatTemplate.GetSafeName());
|
||||
|
||||
//
|
||||
// Only the tools the user could have switched on themselves. A launcher may name one whose
|
||||
// Only the tools the user could have switched on themselves. Either side may name one whose
|
||||
// settings are incomplete — an unconfigured web search, say — and starting the chat with it
|
||||
// enabled would show a state the user cannot produce by hand and cannot fix from the chat.
|
||||
// Null keeps the chat's own defaults, which is what a launcher without tools wants.
|
||||
//
|
||||
var selectedToolIds = launchConfiguration.ToolIds is null
|
||||
var selectedToolIds = toolChoice.ToolIds is null
|
||||
? null
|
||||
: await toolRegistry.FilterSelectableToolIdsAsync(Components.CHAT, launchConfiguration.ToolIds);
|
||||
: await toolRegistry.FilterSelectableToolIdsAsync(Components.CHAT, toolChoice.ToolIds);
|
||||
|
||||
var chatThread = new ChatThread
|
||||
{
|
||||
@ -101,7 +107,12 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
|
||||
Blocks = chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : chatTemplate.ExampleConversation.Select(block => block.DeepClone()).ToList(),
|
||||
};
|
||||
|
||||
return new(new(chatThread, ApplySelectedChatTemplateToComposer: true, PreserveDataSourceOptions: launchConfiguration.DataSourceIds is not null), string.Empty);
|
||||
//
|
||||
// Whoever decided these options — the chat template or the launcher — decided them for this
|
||||
// chat. Without saying so, the chat page would replace them with the chat defaults again:
|
||||
//
|
||||
var dataSourcesWereChosen = chatTemplate.DataSourceOptions is not null || launchConfiguration.DataSourceIds is not null;
|
||||
return new(new(chatThread, ApplySelectedChatTemplateToComposer: true, PreserveDataSourceOptions: dataSourcesWereChosen), string.Empty);
|
||||
}
|
||||
|
||||
private (ProviderSettings Provider, bool IsExplicit, string ErrorMessage) ResolveProvider(Guid? providerId)
|
||||
@ -167,64 +178,119 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
|
||||
: new(chatTemplate, string.Empty);
|
||||
}
|
||||
|
||||
private async Task<(DataSourceOptions? Options, string ErrorMessage)> ResolveDataSourceOptionsAsync(ProviderSettings provider, IReadOnlyList<Guid>? dataSourceIds)
|
||||
private async Task<(DataSourceOptions? Options, string ErrorMessage)> ResolveDataSourceOptionsAsync(PluginAssistants assistantPlugin, ProviderSettings provider, ChatTemplate chatTemplate, IReadOnlyList<Guid>? launcherDataSourceIds)
|
||||
{
|
||||
if (dataSourceIds is null)
|
||||
//
|
||||
// The launcher names data sources as plain IDs, and the options around them are always the
|
||||
// same ones. Building them here turns its choice into the same kind of thing the chat
|
||||
// template carries, which is what lets one rule decide between the two.
|
||||
//
|
||||
DataSourceOptions? launcherOptions = null;
|
||||
if (launcherDataSourceIds is not null)
|
||||
{
|
||||
var standardOptions = settingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions;
|
||||
launcherOptions = new DataSourceOptions
|
||||
{
|
||||
DisableDataSources = false,
|
||||
AutomaticDataSourceSelection = false,
|
||||
AutomaticValidation = standardOptions.AutomaticValidation,
|
||||
PreselectedDataSourceIds = launcherDataSourceIds.Select(dataSourceId => dataSourceId.ToString()).ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(chatTemplate, launcherOptions);
|
||||
if (optionsChoice.LauncherChoiceDropped)
|
||||
logger.LogWarning(
|
||||
"Assistant plugin '{PluginName}' selects the data sources '{LauncherDataSourceIds}', but its chat template '{ChatTemplateName}' brings data source options of its own. The chat starts with the data sources of that template.",
|
||||
assistantPlugin.Name, string.Join(", ", launcherDataSourceIds!), chatTemplate.GetSafeName());
|
||||
|
||||
// Neither side says anything, so the chat starts the way it would start on its own:
|
||||
if (optionsChoice.Options is not { } chosenOptions)
|
||||
return new(settingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy(), string.Empty);
|
||||
|
||||
return await this.CheckChosenDataSourcesAsync(provider, chosenOptions, chatTemplate.DataSourceOptions is null ? null : chatTemplate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks that the chosen data sources exist and may be used with the provider of the chat.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Opening a launcher is one click, so a source which is gone or not permitted has to be said
|
||||
/// out loud instead of being dropped quietly: nobody would see what the chat is missing. Which
|
||||
/// of the two sides chose the sources changes nothing but the wording — and that wording is the
|
||||
/// only place where the user learns which of them to go and fix.
|
||||
/// </remarks>
|
||||
/// <param name="provider">The provider the launched chat runs with.</param>
|
||||
/// <param name="chosenOptions">The options the chat is about to start with.</param>
|
||||
/// <param name="originChatTemplate">The chat template the options came from, or null when the launcher named the sources itself.</param>
|
||||
/// <returns>The checked options, or null and a message saying why no chat was created.</returns>
|
||||
private async Task<(DataSourceOptions? Options, string ErrorMessage)> CheckChosenDataSourcesAsync(ProviderSettings provider, DataSourceOptions chosenOptions, ChatTemplate? originChatTemplate)
|
||||
{
|
||||
//
|
||||
// There is nothing to check when data sources are switched off, and nothing to check either
|
||||
// when an agent picks them: that choice is made per message in the chat, exactly as it is
|
||||
// for a chat template the user picks by hand.
|
||||
//
|
||||
if (chosenOptions.DisableDataSources || chosenOptions.AutomaticDataSourceSelection || chosenOptions.PreselectedDataSourceIds.Count == 0)
|
||||
return new(chosenOptions, string.Empty);
|
||||
|
||||
//
|
||||
// Deciding which data sources are permitted needs an effective provider. Without one,
|
||||
// the check below would report every requested source as unavailable, which would hide
|
||||
// the actual cause from the user:
|
||||
//
|
||||
if (provider == ProviderSettings.NONE)
|
||||
return new(null, TB("The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created."));
|
||||
return new(null, originChatTemplate is null
|
||||
? TB("The assistant chat launcher selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created.")
|
||||
: string.Format(TB("The chat template '{0}' selects data sources, but no provider is available for chats. Please choose a default provider for chats first. No chat was created."), originChatTemplate.GetSafeName()));
|
||||
|
||||
var requestedDataSources = new List<IDataSource>(dataSourceIds.Count);
|
||||
foreach (var dataSourceId in dataSourceIds)
|
||||
var requestedDataSources = new List<IDataSource>(chosenOptions.PreselectedDataSourceIds.Count);
|
||||
foreach (var dataSourceId in chosenOptions.PreselectedDataSourceIds)
|
||||
{
|
||||
// Data sources have no lookup helper in the settings manager, so we match their ids
|
||||
// the same way the rest of the app does:
|
||||
var dataSourceIdText = dataSourceId.ToString();
|
||||
var dataSource = settingsManager.ConfigurationData.DataSources.FirstOrDefault(candidate =>
|
||||
string.Equals(candidate.Id, dataSourceIdText, StringComparison.OrdinalIgnoreCase));
|
||||
string.Equals(candidate.Id, dataSourceId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (dataSource is null)
|
||||
return new(null, string.Format(TB("The assistant chat launcher references data source '{0}', but that data source does not exist."), dataSourceId));
|
||||
return new(null, originChatTemplate is null
|
||||
? string.Format(TB("The assistant chat launcher references data source '{0}', but that data source does not exist."), dataSourceId)
|
||||
: string.Format(TB("The chat template '{0}' references data source '{1}', but that data source does not exist."), originChatTemplate.GetSafeName(), dataSourceId));
|
||||
|
||||
requestedDataSources.Add(dataSource);
|
||||
}
|
||||
|
||||
//
|
||||
// The options the launched chat will run under. We build them here already, because the
|
||||
// data-source check depends on them: they decide which agent providers take part, and an
|
||||
// agent with too little confidence makes a data source unavailable.
|
||||
// The IDs are written back from the sources they resolved to: one of them may be spelled in
|
||||
// another case than the source itself, and the chat matches its preselection literally.
|
||||
//
|
||||
var standardOptions = settingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions;
|
||||
var launchedDataSourceOptions = new DataSourceOptions
|
||||
{
|
||||
DisableDataSources = false,
|
||||
AutomaticDataSourceSelection = false,
|
||||
AutomaticValidation = standardOptions.AutomaticValidation,
|
||||
PreselectedDataSourceIds = requestedDataSources.Select(source => source.Id).ToList(),
|
||||
};
|
||||
chosenOptions.PreselectedDataSourceIds = requestedDataSources.Select(source => source.Id).ToList();
|
||||
|
||||
IReadOnlyList<IDataSource> availableDataSources;
|
||||
try
|
||||
{
|
||||
availableDataSources = await dataSourceService.GetAllowedDataSources(provider, launchedDataSourceOptions, requestedDataSources);
|
||||
//
|
||||
// The options the launched chat will run under are what this check runs against: they
|
||||
// decide which agent providers take part, and an agent with too little confidence makes
|
||||
// a data source unavailable.
|
||||
//
|
||||
availableDataSources = await dataSourceService.GetAllowedDataSources(provider, chosenOptions, requestedDataSources);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "The data sources configured by an assistant chat launcher could not be checked.");
|
||||
return new(null, TB("The data sources selected by the assistant chat launcher could not be checked. No chat was created."));
|
||||
logger.LogError(exception, "The data sources an assistant chat launcher would start its chat with could not be checked.");
|
||||
return new(null, originChatTemplate is null
|
||||
? TB("The data sources selected by the assistant chat launcher could not be checked. No chat was created.")
|
||||
: string.Format(TB("The data sources selected by the chat template '{0}' could not be checked. No chat was created."), originChatTemplate.GetSafeName()));
|
||||
}
|
||||
|
||||
var availableSelectedIds = availableDataSources.Select(source => source.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var unavailableDataSources = requestedDataSources.Where(source => !availableSelectedIds.Contains(source.Id)).Select(source => source.Name).ToList();
|
||||
if (unavailableDataSources.Count > 0)
|
||||
return new(null, string.Format(TB("The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}"), string.Join(", ", unavailableDataSources)));
|
||||
return new(null, originChatTemplate is null
|
||||
? string.Format(TB("The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}"), string.Join(", ", unavailableDataSources))
|
||||
: string.Format(TB("The following data sources selected by the chat template '{0}' are currently unavailable or not permitted for the selected provider: {1}"), originChatTemplate.GetSafeName(), string.Join(", ", unavailableDataSources)));
|
||||
|
||||
return new(launchedDataSourceOptions, string.Empty);
|
||||
return new(chosenOptions, string.Empty);
|
||||
}
|
||||
}
|
||||
@ -1192,11 +1192,24 @@ public static class WorkspaceBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task DeleteChatAsync(IDialogService dialogService, Guid workspaceId, Guid chatId, bool askForConfirmation = true)
|
||||
/// <summary>Deletes the given chat, asking the user to confirm that beforehand.</summary>
|
||||
/// <param name="dialogService">Used to show the confirmation.</param>
|
||||
/// <param name="workspaceId">Workspace that owns the chat; an empty id means a temporary chat.</param>
|
||||
/// <param name="chatId">Chat to delete.</param>
|
||||
/// <param name="askForConfirmation">False skips the question. Only for callers who already asked.</param>
|
||||
/// <returns>True when the chat is gone, which includes it never having been there. False when it is still there.</returns>
|
||||
/// <remarks>
|
||||
/// This is the one place that asks whether a chat may be deleted, because a deleted chat cannot
|
||||
/// be restored: there is no trash. Callers who do more than deleting have to honor the return
|
||||
/// value, or a declined question would still take the rest of their work with it.
|
||||
/// </remarks>
|
||||
public static async Task<bool> DeleteChatAsync(IDialogService dialogService, Guid workspaceId, Guid chatId, bool askForConfirmation = true)
|
||||
{
|
||||
var chat = await LoadChatAsync(new(workspaceId, chatId));
|
||||
|
||||
// There is nothing left to delete, so the caller may go on:
|
||||
if (chat is null)
|
||||
return;
|
||||
return true;
|
||||
|
||||
if (askForConfirmation)
|
||||
{
|
||||
@ -1215,7 +1228,7 @@ public static class WorkspaceBehaviour
|
||||
var dialogReference = await dialogService.ShowAsync<ConfirmDialog>(TB("Delete Chat"), dialogParameters, Dialogs.DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
var chatDirectory = chat.WorkspaceId == Guid.Empty
|
||||
@ -1223,8 +1236,10 @@ public static class WorkspaceBehaviour
|
||||
: Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString());
|
||||
|
||||
var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(workspaceId, chatId, nameof(DeleteChatAsync));
|
||||
|
||||
// Another operation holds the chat, so it stays where it is:
|
||||
if (!acquired)
|
||||
return;
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
@ -1238,6 +1253,8 @@ public static class WorkspaceBehaviour
|
||||
semaphore.Release();
|
||||
ForgetChatSemaphore(workspaceId, chatId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task EnsureWorkspace(Guid workspaceId, string workspaceName)
|
||||
|
||||
@ -9,9 +9,12 @@
|
||||
- Added tools to the policies of the Document Analysis assistant. A policy states which tools an analysis may use, and the AI uses exactly those — nobody has to pick them per document. AI Studio warns you beforehand when the provider you selected is not trusted enough for a tool the policy names. IT departments can roll policies out together with their tools.
|
||||
- Added tools to assistant plugins and direct-chat launchers. Plugin authors name them in the new `ToolIds` field, either as the tools an assistant runs with or as the tools a launcher preselects for the chat it opens; the example assistant plugin shows both. Which tools an assistant asks for is part of what you get to see before you enable it: its security card names them, and the security audit takes them into account.
|
||||
- Added tools to the Assistant Builder. For a direct-chat launcher you pick them yourself, alongside the workspace, provider, and data sources. For an assistant, the AI chooses from the tools installed here and says so in the draft, so you see the decision before the assistant is written.
|
||||
- Added tools and data sources to your chat templates. A template can now decide which tools a chat starts with and which of your documents it may search, so a template such as "Research in our intranet" is complete on its own instead of leaving you to set the same things up by hand every time you switch to it.
|
||||
- Added a way for IT departments to roll out chat templates that bring their own tools and data sources. You do not have to write any of it by hand: set the template up in the app, then export it as ready-made Lua code for your configuration plugin.
|
||||
- Added organization-wide management for tools. Among other options, IT departments can switch tools off entirely, disable individual ones, or define the provider trust a tool requires. You do not have to write any of it by hand: set a tool up in the app, then export its configuration as ready-made Lua code for your plugin, with encrypted API keys if you want them.
|
||||
- Added tool calling to the abilities you can state yourself in the expert provider settings. When you use a model AI Studio does not recognize as tool-capable, you can now declare that it is, the same way you already could for image input or reasoning. It works in the other direction as well: a model AI Studio has never heard of is offered tools, because most models can use them by now. Should one turn out not to be able to, AI Studio says so in plain words and points you to the same setting to switch the ability off again, instead of only passing the provider's error on.
|
||||
- Added support for OpenAI's GPT-6 Astra.
|
||||
- Added a way to keep improving a prompt in the Prompt Optimizer. Select "Improve further" to move the latest proposal back into the prompt field, edit it the way you want, and optimize it again. Your recommendations and everything you selected stay as they are.
|
||||
- Added the context window to what AI Studio knows about a model, wherever its metadata states one.
|
||||
- Added a live read of that context window at the providers which report it, among them Mistral, Groq, OpenRouter, and self-hosted vLLM servers. You then get the window your own server was started with, not the one the model card advertises.
|
||||
- Added a token count below the message field, so you always see how much of the conversation you have used. It counts everything that travels along: your messages, the files you attached, what your data sources contributed, and the tools you offered the AI. It can be an estimate when a provider does not give AI Studio everything it needs to count exactly.
|
||||
@ -45,6 +48,7 @@
|
||||
- Improved the question AI Studio asks before you delete an embedding provider. It now names the data sources depending on that provider, together with what they can still do without it.
|
||||
- Improved what the AI is told when it answers from your own documents (RAG): it now learns which page a passage came from, so it can name the page an answer rests on.
|
||||
- Improved what happens when you ask for web content to be cleaned up and no model is available for it. AI Studio loads the page and tells you it arrived uncleaned, instead of quietly handing you the raw page with its navigation and advertising still in it.
|
||||
- Changed what a tile that opens a chat directly starts with: when it uses a chat template that brings its own tools or data sources, that template decides them. The Assistant Builder says so while you build such a tile.
|
||||
- Changed which model a security check of an assistant plugin may fall back on. When you have set none aside for these checks, AI Studio uses the model you were working with, but only when that model meets the trust your organization requires for a check. One ruled out for that purpose no longer gets to read a plugin's code.
|
||||
- Changed how provider trust and provider confidence work together. Marking a provider as trustworthy in a configuration no longer also satisfies a required confidence level: one says who runs the provider, the other how confidential it is. Organizations raise a provider's level in their own confidence scheme instead. This applies beyond local data sources, for example, when a model reads a page from your intranet.
|
||||
- Fixed the abilities AI Studio assumed for many models. We checked the families against their documentation: some models gained image input, reasoning, or tool calling, others lost an ability they never had.
|
||||
@ -81,5 +85,11 @@
|
||||
- Fixed AI Studio asking such a server for its models with an empty key attached when you had stored none at all. Servers behind a login turn those requests down.
|
||||
- Fixed a key that could not be saved going unmentioned for the servers you host yourself. You are now told what went wrong, instead of the settings simply staying open.
|
||||
- Fixed transcripts quietly losing what was said softly, such as a greeting at the very beginning of a recording. AI Studio compressed recordings so far before sending them to your transcription provider that the model could no longer make out those passages. Recordings now keep enough details for the whole of what you said to arrive.
|
||||
- Fixed AI Studio seeming to hang for minutes when a chat had grown too large for the model. Some providers turn such a chat down in a way AI Studio did not recognize, so it kept sending the very same chat again and again. You are now told right away that the chat, including its attachments, is too large for the selected model.
|
||||
- Fixed AI Studio trying for minutes when a provider turns a request down for good. Such an answer does not change by asking a second time, so AI Studio now stops at the first one and tells you what the provider said about it.
|
||||
- Fixed errors about a provider arriving as two messages at once, the second of which spoke of several attempts that were never made. You now get the single message which names the cause.
|
||||
- Fixed the button in the chat toolbar that deletes the current chat and starts a new one doing so without asking. It now asks for your confirmation first, just like the chat list does, because a deleted chat cannot be brought back. The button shows a delete icon in red now, instead of one that looked like a reload.
|
||||
- Fixed AI Studio following your system into light or dark mode even though you had chosen a fixed color theme in the app settings.
|
||||
- Fixed AI Studio keeping its previous color theme after your computer woke up from sleep, when your system had switched between light and dark mode during that time.
|
||||
- Upgraded the Visual Briefing assistant (in preview) from the prototype to the beta state. The assistant is now completely implemented and is undergoing a deeper testing phase in preparation for release. To try it, open the app settings, allow preview features down to beta, and then enable the Visual Briefing assistant there.
|
||||
- Upgraded the vector database behind local RAG (Qdrant Edge) to version 0.8.0.
|
||||
|
||||
299
app/Tests/Settings/ChatTemplateConfigurationTests.cs
Normal file
299
app/Tests/Settings/ChatTemplateConfigurationTests.cs
Normal file
@ -0,0 +1,299 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Lua;
|
||||
using Lua.Standard;
|
||||
|
||||
namespace AIStudio.Tests.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The interesting part is not that a value survives, but that the difference between "this
|
||||
/// template says nothing" and "this template says none" survives. Both end up as an empty
|
||||
/// selection in the chat on a fresh installation, so a mistake here stays invisible until somebody
|
||||
/// sets a default tool for their chats -- and then quietly hands out a tool the template ruled out.
|
||||
/// The last tests cover what the export says out loud before it runs, for the same reason: a data
|
||||
/// source which cannot be rolled out goes unnoticed on the machine reading the plugin.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class ChatTemplateConfigurationTests
|
||||
{
|
||||
private static readonly Guid PLUGIN_ID = new("22222222-2222-2222-2222-222222222222");
|
||||
|
||||
[Test]
|
||||
public async Task WhatTheAppExportsIsWhatAConfigurationPluginCanReadBack()
|
||||
{
|
||||
var written = NewTemplate() with
|
||||
{
|
||||
ToolIds = ["read_web_page", "web_search"],
|
||||
DataSourceOptions = new()
|
||||
{
|
||||
DisableDataSources = false,
|
||||
AutomaticDataSourceSelection = false,
|
||||
AutomaticValidation = true,
|
||||
PreselectedDataSourceIds = ["11111111-1111-1111-1111-111111111111"],
|
||||
},
|
||||
};
|
||||
|
||||
var read = await ExportAndReadBackAsync(written);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(read.ToolIds, Is.EquivalentTo(written.ToolIds!));
|
||||
Assert.That(read.DataSourceOptions, Is.Not.Null);
|
||||
Assert.That(read.DataSourceOptions!.DisableDataSources, Is.False);
|
||||
Assert.That(read.DataSourceOptions.AutomaticDataSourceSelection, Is.False);
|
||||
Assert.That(read.DataSourceOptions.AutomaticValidation, Is.True);
|
||||
Assert.That(read.DataSourceOptions.PreselectedDataSourceIds, Is.EqualTo(written.DataSourceOptions!.PreselectedDataSourceIds));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AnAgenticSelectionSurvivesTheExportAsWell()
|
||||
{
|
||||
var written = NewTemplate() with
|
||||
{
|
||||
DataSourceOptions = new()
|
||||
{
|
||||
DisableDataSources = false,
|
||||
AutomaticDataSourceSelection = true,
|
||||
AutomaticValidation = true,
|
||||
PreselectedDataSourceIds = [],
|
||||
},
|
||||
};
|
||||
|
||||
var read = await ExportAndReadBackAsync(written);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(read.DataSourceOptions, Is.Not.Null);
|
||||
Assert.That(read.DataSourceOptions!.AutomaticDataSourceSelection, Is.True, "Letting an agent pick the sources is the one thing only a chat template can state, so it must not be lost on the way through Lua.");
|
||||
Assert.That(read.DataSourceOptions.PreselectedDataSourceIds, Is.Empty);
|
||||
Assert.That(read.ToolIds, Is.Null);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ATemplateWhichSaysNothingExportsNeitherTable()
|
||||
{
|
||||
var written = NewTemplate();
|
||||
Assert.That(written.TryExportAsConfigurationSection(out var luaCode, out var issue), Is.True, issue);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(luaCode, Does.Not.Contain("ToolIds"));
|
||||
Assert.That(luaCode, Does.Not.Contain("DataSourceOptions"));
|
||||
});
|
||||
|
||||
var read = await ParseAsync(luaCode);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(read.ToolIds, Is.Null, "A template without a tool selection must stay without one, so the chat keeps using its own default.");
|
||||
Assert.That(read.DataSourceOptions, Is.Null);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ATemplateWhichRulesOutEveryToolStaysThatWay()
|
||||
{
|
||||
var written = NewTemplate() with { ToolIds = [] };
|
||||
var read = await ExportAndReadBackAsync(written);
|
||||
|
||||
Assert.That(read.ToolIds, Is.Not.Null, "An empty selection is the statement that this template wants no tools. Reading it back as null would hand out the chat default instead.");
|
||||
Assert.That(read.ToolIds, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NamingTheDataSourceOptionsAtAllSwitchesDataSourcesOn()
|
||||
{
|
||||
var read = await ParseAsync("""
|
||||
CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
|
||||
["Id"] = "33333333-3333-3333-3333-333333333333",
|
||||
["Name"] = "Intranet Research",
|
||||
["SystemPrompt"] = "You are a research assistant.",
|
||||
["DataSourceOptions"] = {
|
||||
["PreselectedDataSourceIds"] = {
|
||||
"11111111-1111-1111-1111-111111111111",
|
||||
},
|
||||
},
|
||||
}
|
||||
""");
|
||||
|
||||
Assert.That(read.DataSourceOptions, Is.Not.Null);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(read.DataSourceOptions!.DisableDataSources, Is.False, "Writing this table is already the statement that the template wants data sources, so an omitted switch must not turn them off again.");
|
||||
Assert.That(read.DataSourceOptions.AutomaticDataSourceSelection, Is.False);
|
||||
Assert.That(read.DataSourceOptions.AutomaticValidation, Is.False);
|
||||
Assert.That(read.DataSourceOptions.PreselectedDataSourceIds, Is.EqualTo(new[] { "11111111-1111-1111-1111-111111111111" }));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AnUnusableEntryIsSkippedAndTheRestOfTheListSurvives()
|
||||
{
|
||||
var read = await ParseAsync("""
|
||||
CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
|
||||
["Id"] = "33333333-3333-3333-3333-333333333333",
|
||||
["Name"] = "Intranet Research",
|
||||
["SystemPrompt"] = "You are a research assistant.",
|
||||
["ToolIds"] = {
|
||||
"web_search",
|
||||
"",
|
||||
{},
|
||||
"read_web_page",
|
||||
},
|
||||
["DataSourceOptions"] = {
|
||||
["PreselectedDataSourceIds"] = {
|
||||
"11111111-1111-1111-1111-111111111111",
|
||||
" ",
|
||||
},
|
||||
},
|
||||
}
|
||||
""");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(read.ToolIds, Is.EquivalentTo(new[] { "web_search", "read_web_page" }));
|
||||
Assert.That(read.DataSourceOptions!.PreselectedDataSourceIds, Is.EqualTo(new[] { "11111111-1111-1111-1111-111111111111" }));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExportedDataSourceIdsComeWithTheNoteThatTheyAreLocalOnes()
|
||||
{
|
||||
var withSources = NewTemplate() with
|
||||
{
|
||||
DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["11111111-1111-1111-1111-111111111111"] },
|
||||
};
|
||||
|
||||
var agenticOnly = NewTemplate() with
|
||||
{
|
||||
DataSourceOptions = new() { DisableDataSources = false, AutomaticDataSourceSelection = true },
|
||||
};
|
||||
|
||||
Assert.That(withSources.TryExportAsConfigurationSection(out var withSourcesLua, out var issue), Is.True, issue);
|
||||
Assert.That(agenticOnly.TryExportAsConfigurationSection(out var agenticOnlyLua, out issue), Is.True, issue);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(withSourcesLua, Does.StartWith("--"), "Whoever pastes this into a plugin cannot see from the IDs alone that they belong to another machine.");
|
||||
Assert.That(agenticOnlyLua, Does.Not.StartWith("--"), "Without IDs there is nothing to check, so the note would only be noise.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LocalDataSourcesOfATemplateAreNamedBeforeItIsExported()
|
||||
{
|
||||
var template = NewTemplate() with
|
||||
{
|
||||
DataSourceOptions = new()
|
||||
{
|
||||
DisableDataSources = false,
|
||||
PreselectedDataSourceIds = ["11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222"],
|
||||
},
|
||||
};
|
||||
|
||||
var localNames = ChatTemplate.GetPreselectedLocalDataSourceNames(template, ConfiguredDataSources());
|
||||
|
||||
Assert.That(localNames, Is.EqualTo(new[] { "Meeting notes" }), "Only the local source can be named: its ID means nothing on the machine which reads the exported plugin, while the ERI source points at something the whole organization reaches.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ATemplateWithoutLocalDataSourcesIsExportedWithoutAQuestion()
|
||||
{
|
||||
var eriOnly = NewTemplate() with
|
||||
{
|
||||
DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["22222222-2222-2222-2222-222222222222"] },
|
||||
};
|
||||
|
||||
var agentic = NewTemplate() with
|
||||
{
|
||||
DataSourceOptions = new() { DisableDataSources = false, AutomaticDataSourceSelection = true },
|
||||
};
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(ChatTemplate.GetPreselectedLocalDataSourceNames(eriOnly, ConfiguredDataSources()), Is.Empty);
|
||||
Assert.That(ChatTemplate.GetPreselectedLocalDataSourceNames(agentic, ConfiguredDataSources()), Is.Empty, "An agent picks the sources per message, so this template names none to begin with.");
|
||||
Assert.That(ChatTemplate.GetPreselectedLocalDataSourceNames(NewTemplate(), ConfiguredDataSources()), Is.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnIdWhichMatchesNoDataSourceIsNotReportedAsALocalOne()
|
||||
{
|
||||
var template = NewTemplate() with
|
||||
{
|
||||
DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["99999999-9999-9999-9999-999999999999"] },
|
||||
};
|
||||
|
||||
Assert.That(ChatTemplate.GetPreselectedLocalDataSourceNames(template, ConfiguredDataSources()), Is.Empty, "There is no name to warn about, and the note above the exported IDs already tells the admin to check them.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A template with the parts every export needs, and nothing said about tools or data sources.
|
||||
/// </summary>
|
||||
private static ChatTemplate NewTemplate() => new()
|
||||
{
|
||||
Num = 1,
|
||||
Id = "33333333-3333-3333-3333-333333333333",
|
||||
Name = "Intranet Research",
|
||||
SystemPrompt = "You are a research assistant.",
|
||||
PredefinedUserPrompt = string.Empty,
|
||||
ExampleConversation = [],
|
||||
FileAttachments = [],
|
||||
AllowProfileUsage = true,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// One data source of each kind: a local one, which cannot be rolled out, and an ERI one, which can.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<IDataSource> ConfiguredDataSources() =>
|
||||
[
|
||||
new DataSourceLocalFile { Id = "11111111-1111-1111-1111-111111111111", Name = "Meeting notes", Type = DataSourceType.LOCAL_FILE },
|
||||
new DataSourceERI_V1 { Id = "22222222-2222-2222-2222-222222222222", Name = "Intranet", Type = DataSourceType.ERI_V1 },
|
||||
];
|
||||
|
||||
private static async Task<ChatTemplate> ExportAndReadBackAsync(ChatTemplate template)
|
||||
{
|
||||
Assert.That(template.TryExportAsConfigurationSection(out var luaCode, out var issue), Is.True, issue);
|
||||
return await ParseAsync(luaCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a chat template the way a configuration plugin states it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Through a real Lua state rather than a table put together in C#, so that the exported code
|
||||
/// has to be valid Lua before anything else is checked.
|
||||
/// </remarks>
|
||||
/// <param name="luaCode">The lines a plugin would contain, including the assignment itself.</param>
|
||||
/// <returns>The chat template read from it.</returns>
|
||||
private static async Task<ChatTemplate> ParseAsync(string luaCode)
|
||||
{
|
||||
var state = LuaState.Create();
|
||||
state.OpenBasicLibrary();
|
||||
state.OpenTableLibrary();
|
||||
|
||||
await state.DoStringAsync($$"""
|
||||
CONFIG = {}
|
||||
CONFIG["CHAT_TEMPLATES"] = {}
|
||||
{{luaCode}}
|
||||
""");
|
||||
|
||||
if (!state.Environment["CONFIG"].TryRead<LuaTable>(out var configTable) ||
|
||||
!configTable["CHAT_TEMPLATES"].TryRead<LuaTable>(out var templatesTable) ||
|
||||
!templatesTable[1].TryRead<LuaTable>(out var templateTable))
|
||||
throw new InvalidOperationException("The code of this test did not produce a chat template table.");
|
||||
|
||||
if (!ChatTemplate.TryParseChatTemplateTable(1, templateTable, PLUGIN_ID, string.Empty, out var parsed) || parsed is not ChatTemplate chatTemplate)
|
||||
throw new InvalidOperationException("The chat template of this test could not be read.");
|
||||
|
||||
return chatTemplate;
|
||||
}
|
||||
}
|
||||
165
app/Tests/Settings/ChatTemplatePrecedenceTests.cs
Normal file
165
app/Tests/Settings/ChatTemplatePrecedenceTests.cs
Normal file
@ -0,0 +1,165 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Tests.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Checks who decides the tools and data sources when a direct chat launcher and the chat template
|
||||
/// it opens its chat with both name some.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Either side can be filled in without knowing about the other, so all four combinations happen.
|
||||
/// The rule is deliberately the same for tools and for data sources: the chat template wins as a
|
||||
/// whole, because it is the only one of the two which can also leave the choice of sources to an
|
||||
/// agent, and a field-by-field mix of both would be something neither of them asked for.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class ChatTemplatePrecedenceTests
|
||||
{
|
||||
[Test]
|
||||
public void WhenNeitherSideSaysAnythingTheChatDefaultsStay()
|
||||
{
|
||||
var toolChoice = ChatTemplate.ChooseToolIds(NewTemplate(), null);
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(NewTemplate(), null);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(toolChoice.ToolIds, Is.Null, "Nobody named any tool, so the chat has to keep using the tools of its own default.");
|
||||
Assert.That(toolChoice.LauncherChoiceDropped, Is.False);
|
||||
Assert.That(optionsChoice.Options, Is.Null, "Nobody named any data source, so the chat has to keep using its own default options.");
|
||||
Assert.That(optionsChoice.LauncherChoiceDropped, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ALauncherAloneDecidesForItself()
|
||||
{
|
||||
var template = NewTemplate();
|
||||
var launcherOptions = NewLauncherOptions("11111111-1111-1111-1111-111111111111");
|
||||
|
||||
var toolChoice = ChatTemplate.ChooseToolIds(template, new[] { "web_search" });
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, launcherOptions);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(toolChoice.ToolIds, Is.EquivalentTo(new[] { "web_search" }));
|
||||
Assert.That(toolChoice.LauncherChoiceDropped, Is.False, "Nothing was dropped here, so nothing may be reported as dropped either.");
|
||||
Assert.That(optionsChoice.Options, Is.SameAs(launcherOptions));
|
||||
Assert.That(optionsChoice.LauncherChoiceDropped, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ATemplateAloneDecidesForItself()
|
||||
{
|
||||
var template = NewTemplate() with
|
||||
{
|
||||
ToolIds = ["read_web_page"],
|
||||
DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["22222222-2222-2222-2222-222222222222"] },
|
||||
};
|
||||
|
||||
var toolChoice = ChatTemplate.ChooseToolIds(template, null);
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, null);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(toolChoice.ToolIds, Is.EquivalentTo(new[] { "read_web_page" }));
|
||||
Assert.That(toolChoice.LauncherChoiceDropped, Is.False);
|
||||
Assert.That(optionsChoice.Options!.PreselectedDataSourceIds, Is.EqualTo(new[] { "22222222-2222-2222-2222-222222222222" }));
|
||||
Assert.That(optionsChoice.LauncherChoiceDropped, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenBothSidesSpeakTheTemplateWinsAndTheLossIsReported()
|
||||
{
|
||||
var template = NewTemplate() with
|
||||
{
|
||||
ToolIds = ["read_web_page"],
|
||||
DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["22222222-2222-2222-2222-222222222222"] },
|
||||
};
|
||||
|
||||
var toolChoice = ChatTemplate.ChooseToolIds(template, new[] { "web_search" });
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, NewLauncherOptions("11111111-1111-1111-1111-111111111111"));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(toolChoice.ToolIds, Is.EquivalentTo(new[] { "read_web_page" }));
|
||||
Assert.That(toolChoice.LauncherChoiceDropped, Is.True, "The tools of the launcher are gone, and only this flag can make the log say so.");
|
||||
Assert.That(optionsChoice.Options!.PreselectedDataSourceIds, Is.EqualTo(new[] { "22222222-2222-2222-2222-222222222222" }));
|
||||
Assert.That(optionsChoice.LauncherChoiceDropped, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ATemplateWhichWantsNoToolsWinsJustTheSame()
|
||||
{
|
||||
var template = NewTemplate() with { ToolIds = [] };
|
||||
|
||||
var toolChoice = ChatTemplate.ChooseToolIds(template, new[] { "web_search" });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(toolChoice.ToolIds, Is.Empty, "An empty selection is the statement that this template wants no tools, which is as much of a statement as naming one.");
|
||||
Assert.That(toolChoice.LauncherChoiceDropped, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheAgenticSelectionOfATemplateSurvivesALauncherWithItsOwnSources()
|
||||
{
|
||||
var template = NewTemplate() with
|
||||
{
|
||||
DataSourceOptions = new() { DisableDataSources = false, AutomaticDataSourceSelection = true, PreselectedDataSourceIds = [] },
|
||||
};
|
||||
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, NewLauncherOptions("11111111-1111-1111-1111-111111111111"));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(optionsChoice.Options!.AutomaticDataSourceSelection, Is.True, "Letting an agent pick the sources is the one thing a launcher cannot express, so it is exactly what must not be overwritten by one.");
|
||||
Assert.That(optionsChoice.Options.PreselectedDataSourceIds, Is.Empty);
|
||||
Assert.That(optionsChoice.LauncherChoiceDropped, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheChosenOptionsAreACopyRatherThanTheOnesOfTheTemplate()
|
||||
{
|
||||
var template = NewTemplate() with
|
||||
{
|
||||
DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["22222222-2222-2222-2222-222222222222"] },
|
||||
};
|
||||
|
||||
var optionsChoice = ChatTemplate.ChooseDataSourceOptions(template, null);
|
||||
optionsChoice.Options!.PreselectedDataSourceIds.Clear();
|
||||
|
||||
Assert.That(template.DataSourceOptions!.PreselectedDataSourceIds, Is.Not.Empty, "The launched chat goes on to change these options, and the template is a setting of the user which must not change with it.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A template which says nothing about tools or data sources.
|
||||
/// </summary>
|
||||
private static ChatTemplate NewTemplate() => new()
|
||||
{
|
||||
Num = 1,
|
||||
Id = "33333333-3333-3333-3333-333333333333",
|
||||
Name = "Intranet Research",
|
||||
SystemPrompt = "You are a research assistant.",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The options a launcher which names data sources ends up with.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A launcher has no switches of its own: it names sources, and the rest is always this. Which
|
||||
/// is the whole reason the chat template wins whenever both of them speak.
|
||||
/// </remarks>
|
||||
private static DataSourceOptions NewLauncherOptions(params string[] dataSourceIds) => new()
|
||||
{
|
||||
DisableDataSources = false,
|
||||
AutomaticDataSourceSelection = false,
|
||||
AutomaticValidation = false,
|
||||
PreselectedDataSourceIds = [..dataSourceIds],
|
||||
};
|
||||
}
|
||||
71
app/Tests/Tools/CircuitStateServiceTests.cs
Normal file
71
app/Tests/Tools/CircuitStateServiceTests.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
namespace AIStudio.Tests.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Checks when the circuit state reports that the browser connection came back.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The report is there to fetch again what the browser sent while the connection was down, because Blazor
|
||||
/// drops it. The layout reads the color theme anew on it, for instance, since the machine may have switched
|
||||
/// its theme during sleep. So it has to come after every loss, and only then: missing one leaves the app
|
||||
/// with stale state, while a connection which was never lost has nothing to fetch again.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class CircuitStateServiceTests
|
||||
{
|
||||
[Test]
|
||||
public void AConnectionWhichReturnsAfterALossIsReportedOnce()
|
||||
{
|
||||
var circuitState = new CircuitStateService();
|
||||
var numReports = 0;
|
||||
circuitState.ConnectionRestored += () => numReports++;
|
||||
|
||||
circuitState.MarkAsDisconnected();
|
||||
circuitState.MarkAsConnected();
|
||||
|
||||
Assert.That(numReports, Is.EqualTo(1), "The connection was lost and came back, so whatever the browser sent in between is gone.");
|
||||
Assert.That(circuitState.IsConnected, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheFirstConnectionIsNotReported()
|
||||
{
|
||||
var circuitState = new CircuitStateService();
|
||||
var numReports = 0;
|
||||
circuitState.ConnectionRestored += () => numReports++;
|
||||
|
||||
circuitState.MarkAsConnected();
|
||||
|
||||
Assert.That(numReports, Is.Zero, "A circuit starts out connected, so its first connection has not lost anything.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AConnectionWhichWasNotLostIsNotReportedAgain()
|
||||
{
|
||||
var circuitState = new CircuitStateService();
|
||||
var numReports = 0;
|
||||
circuitState.ConnectionRestored += () => numReports++;
|
||||
|
||||
circuitState.MarkAsDisconnected();
|
||||
circuitState.MarkAsConnected();
|
||||
circuitState.MarkAsConnected();
|
||||
|
||||
Assert.That(numReports, Is.EqualTo(1), "The second call follows a connection which was up all along.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EveryLossIsReportedOnItsOwn()
|
||||
{
|
||||
var circuitState = new CircuitStateService();
|
||||
var numReports = 0;
|
||||
circuitState.ConnectionRestored += () => numReports++;
|
||||
|
||||
circuitState.MarkAsDisconnected();
|
||||
circuitState.MarkAsConnected();
|
||||
circuitState.MarkAsDisconnected();
|
||||
circuitState.MarkAsConnected();
|
||||
|
||||
Assert.That(numReports, Is.EqualTo(2), "The machine went to sleep twice, and each time something may have been lost.");
|
||||
}
|
||||
}
|
||||
@ -424,6 +424,9 @@ Currently, you can configure the following things:
|
||||
- Any number of LLM providers (self-hosted or cloud providers with encrypted API keys)
|
||||
- Any number of transcription providers for voice-to-text functionality
|
||||
- Any number of embedding providers for RAG
|
||||
- Any number of ERI data sources for RAG
|
||||
- Any number of profiles and chat templates, including the tools and data sources a template brings along
|
||||
- Any number of policies for the Document Analysis assistant
|
||||
- Enterprise hash approvals for assistant plugins
|
||||
- Tool settings, encrypted tool API keys, and minimum provider confidence requirements
|
||||
- The update behavior of AI Studio
|
||||
@ -589,6 +592,33 @@ A test configuration carries the rights of an organization configuration without
|
||||
|
||||
The data directory belongs to the user account, so whoever can write there can approve assistant plugins in the name of your organization until the next restart. Treat write access to the data directory as equivalent to deploying a configuration, and protect it accordingly on managed devices.
|
||||
|
||||
## Exporting configurations from the app
|
||||
|
||||
You do not have to write your configuration plugin by hand. Set something up in AI Studio, export it, and paste the Lua fragment into your plugin.
|
||||
|
||||
Enable **Show administration settings** in the app settings once. It reveals the **Enterprise Administration** section and an **Export configuration** button next to each of these:
|
||||
|
||||
| What you can export | Where the button sits | Offered for |
|
||||
|---|---|---|
|
||||
| LLM providers | the provider list in the app settings | providers you created yourself |
|
||||
| Embedding providers | the provider list in the app settings | as above, and only while the RAG preview is enabled |
|
||||
| Transcription providers | the provider list in the app settings | as above, and only while the speech-to-text preview is enabled |
|
||||
| Profiles | the profile dialog in the app settings | profiles you created yourself |
|
||||
| Chat templates | the chat template dialog in the app settings | templates you created yourself |
|
||||
| ERI data sources | the data source list in the app settings | ERI sources only, and not the ones using Kerberos |
|
||||
| Document analysis policies | the Document Analysis assistant itself | the policy you have selected |
|
||||
| Tools | **Tool Settings** in the app settings | every tool |
|
||||
|
||||
Anything your organization already manages has no export button: it came from a plugin to begin with. Local files and local directories have none either — such a data source exists on one machine only, so there is nothing to hand to your colleagues.
|
||||
|
||||
The button copies the fragment to your clipboard. Paste it into your [configuration plugin](../app/MindWork%20AI%20Studio/Plugins/configuration/plugin.lua), after the initialization of the table it extends, such as `CONFIG["LLM_PROVIDERS"] = {}`. One export writes to disk as well: a chat template whose attachments you package copies those files into a folder of your plugin and puts the Lua into your clipboard as usual.
|
||||
|
||||
**An export mints a new ID** for the exported object, so exporting the same provider or template twice deploys two of them to your colleagues. Once something is in your plugin, keep its ID and edit the rest around it. Document analysis policies are the exception: they keep the ID they have.
|
||||
|
||||
Some exports ask a question first: a provider with an API key offers to include it encrypted (see [Encrypted API Keys](#encrypted-api-keys)), an ERI data source does the same for its token or its credentials, a chat template with file attachments asks whether to keep their paths or copy them into your plugin, and a tool opens a dialog for the areas and the kind of management you want (see [Exporting tool configurations](#exporting-tool-configurations)).
|
||||
|
||||
Handing a whole plugin to a colleague is a different thing: that is the **Share** function on the plugins page, which writes a `.mwplugin` archive and is governed by its own organization setting rather than by the administration settings.
|
||||
|
||||
## Encrypted API Keys
|
||||
|
||||
You can include encrypted API keys in your configuration plugins for cloud providers (like OpenAI, Anthropic) or secured on-premise models. This feature provides obfuscation to prevent casual exposure of API keys in configuration files.
|
||||
@ -601,7 +631,7 @@ You can include encrypted API keys in your configuration plugins for cloud provi
|
||||
### Setting Up Encrypted API Keys
|
||||
|
||||
1. **Generate an encryption secret:**
|
||||
In AI Studio, enable the "Show administration settings" toggle in the app settings. Then click the "Generate encryption secret and copy to clipboard" button in the "Enterprise Administration" section. This generates a cryptographically secure 256-bit key and copies it to your clipboard as a base64 string.
|
||||
In AI Studio, click the "Generate encryption secret and copy to clipboard" button in the "Enterprise Administration" section of the app settings, which [Show administration settings](#exporting-configurations-from-the-app) reveals. This generates a cryptographically secure 256-bit key and copies it to your clipboard as a base64 string.
|
||||
|
||||
2. **Deploy the encryption secret:**
|
||||
Distribute the secret to all client machines using any supported enterprise source. The secret can be deployed on its own, even when no enterprise configuration IDs or server URLs are defined on that machine:
|
||||
@ -612,11 +642,7 @@ You can include encrypted API keys in your configuration plugins for cloud provi
|
||||
You must also deploy the same secret on the machine where you will export the encrypted API keys (step 3).
|
||||
|
||||
3. **Export encrypted API keys from AI Studio:**
|
||||
Once the encryption secret is deployed on your machine:
|
||||
- Configure a provider with an API key in AI Studio's settings
|
||||
- Click the export button for that provider
|
||||
- If an API key is configured, you will be asked if you want to include the encrypted API key in the export
|
||||
- The exported Lua code will contain the encrypted API key in the format `ENC:v1:<base64-encoded data>`
|
||||
Once the encryption secret is deployed on your machine, configure the provider with its API key and [export it](#exporting-configurations-from-the-app). AI Studio asks whether to include the key; the exported Lua code then contains it in the format `ENC:v1:<base64-encoded data>`.
|
||||
|
||||
4. **Add encrypted keys to your configuration:**
|
||||
Copy the exported configuration (including the encrypted API key) into your configuration plugin.
|
||||
@ -643,9 +669,9 @@ The API key will be automatically decrypted when the configuration is loaded and
|
||||
|
||||
## Exporting tool configurations
|
||||
|
||||
Start from the [example configuration plugin](../app/MindWork%20AI%20Studio/Plugins/configuration/plugin.lua). The export produces a Lua fragment to insert into that file; it assumes `CONFIG` and `CONFIG["SETTINGS"]` already exist.
|
||||
A tool export is the one that asks the most before it writes anything. It assumes `CONFIG` and `CONFIG["SETTINGS"]` already exist in your plugin.
|
||||
|
||||
1. Enable **Show administration settings** in the app settings. In **Tool Settings**, configure the tool and save your changes, then click its **Export configuration** button next to the settings button.
|
||||
1. In **Tool Settings**, configure the tool and save your changes, then [export it](#exporting-configurations-from-the-app).
|
||||
2. Select the areas to export. All areas start selected. For Web Search, SearXNG, Staan, Tavily, and General are independent: selecting only Tavily does not include the search language, strategy, or preferred backend. Select General separately when you need those settings.
|
||||
3. Choose **Locked settings** or **Editable defaults**. Locked settings go into `DataTools.LockedToolSettings` and cannot be changed by users. Editable defaults go into `DataTools.DefaultToolSettings`; a user's saved value takes precedence over them.
|
||||
4. Optionally select **Include encrypted API keys and other secrets**, which starts off. The option is available only when the selected areas contain configured secrets and this machine has a valid enterprise encryption secret. Deploy the same secret to recipients as described in [Setting Up Encrypted API Keys](#setting-up-encrypted-api-keys). Secrets always go into `LockedToolSettings`, including when you choose editable defaults for the other fields. Managed tool secrets are used from the configuration without replacing the user's own keyring entries; removing the managed secret makes the user's own key available again.
|
||||
@ -685,6 +711,23 @@ This does not change the SearXNG or Staan settings, the general Web Search setti
|
||||
|
||||
You can combine both fragments in the same plugin: their table initializations preserve earlier entries, and only a later assignment to an identical key replaces its value. A later whole-table assignment such as `CONFIG["SETTINGS"]["DataTools.LockedToolSettings"] = { ... }` replaces those entries, so place exports after it or merge them manually. This behavior applies within one plugin; across separate configuration plugins, the winning plugin replaces the whole managed table as described in [Settings that hold a list or a table](#settings-that-hold-a-list-or-a-table).
|
||||
|
||||
## Chat templates with tools and data sources
|
||||
|
||||
On top of its system prompt and the rest, a chat template decides the tools and the data sources a chat started with it begins with. The data source IDs in an exported template are **carried over unchanged**, unlike the template's own ID: they point at the data sources of your organization. Check them against your `CONFIG["DATA_SOURCES"]` -- an ID that resolves to nothing is ignored, and a chat with that template then starts without that source.
|
||||
|
||||
Writing such a template by hand means knowing that saying nothing and saying none are two different statements:
|
||||
|
||||
| What the template says | What a chat started with it does |
|
||||
|---|---|
|
||||
| no `ToolIds` at all | starts with the tools the user has set as their chat default |
|
||||
| `ToolIds` present but empty | starts with no tool at all, whatever that default says |
|
||||
| no `DataSourceOptions` at all | starts with the data source options the user has set as their chat default |
|
||||
| `DataSourceOptions` present | starts with exactly those, including the choice to let an agent pick the sources |
|
||||
|
||||
Writing the `DataSourceOptions` table at all is already the statement that this template wants data sources, so `DisableDataSources` starts at `false` inside it, unlike everywhere else in the app.
|
||||
|
||||
When an [assistant plugin](../app/MindWork%20AI%20Studio/Plugins/assistants/README.md) opens a chat directly and its chat template names tools or data sources, that template decides them alone; what the launcher names is dropped with a warning in the log. Its README explains the rule and how such sources are checked.
|
||||
|
||||
## Letting users provide their own API key
|
||||
|
||||
Sometimes you want to hand out a preconfigured provider -- a fixed host, model, and instance name
|
||||
|
||||
Loading…
Reference in New Issue
Block a user