mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 19:52:10 +00:00
Merge c13255fe77 into f13c35d814
This commit is contained in:
commit
3c96517cca
15
AGENTS.md
15
AGENTS.md
@ -123,6 +123,19 @@ When adding configuration plugin capabilities:
|
||||
- For live plugin content, add a data type implementing `ILivePluginContent`, parse it in `PluginConfiguration`, expose it through `PluginFactory`, and add any required cleanup only for persistent side data.
|
||||
- Always document the new capability in `app/MindWork AI Studio/Plugins/configuration/plugin.lua`.
|
||||
|
||||
## Tool Calling System
|
||||
|
||||
**Documentation:** `documentation/Tools.md`
|
||||
|
||||
When adding, changing, or removing model-driven tools, keep these parts in sync:
|
||||
- `app/MindWork AI Studio/wwwroot/tool_definitions/` for the tool JSON definition.
|
||||
- `app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/` for the `IToolImplementation` class.
|
||||
- `app/MindWork AI Studio/Program.cs` for DI registration of the implementation.
|
||||
- `app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs` when default tool dependencies or minimum provider confidence rules change.
|
||||
- `app/MindWork AI Studio/Plugins/configuration/plugin.lua` when administrators can configure or manage the tool or its settings.
|
||||
|
||||
Tool implementations must treat model-provided arguments as untrusted input. Validate settings and arguments, protect secrets with `SensitiveTraceArgumentNames`, use `ToolExecutionBlockedException` for intentional policy blocks, and check provider confidence before returning sensitive data to the model.
|
||||
|
||||
## RAG (Retrieval-Augmented Generation)
|
||||
|
||||
RAG integration is currently in development (preview feature). Architecture:
|
||||
@ -221,4 +234,4 @@ following words:
|
||||
- Downgraded
|
||||
- Upgraded
|
||||
|
||||
The entire changelog is sorted by these categories in the order shown above. The language used for the changelog is US English.
|
||||
The entire changelog is sorted by these categories in the order shown above. The language used for the changelog is US English.
|
||||
|
||||
@ -184,6 +184,8 @@ If you're interested in learning more about future plans, check out our [roadmap
|
||||
|
||||
You want to know how to build MindWork AI Studio from source? [Check out the instructions here](documentation/Build.md).
|
||||
|
||||
Do you want to add or maintain model-driven tools? [Read the tool development guide here](documentation/Tools.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
@ -175,6 +175,11 @@
|
||||
<ProfileSelection MarginLeft="" @bind-CurrentProfile="@this.CurrentProfile"/>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.AreToolsEnabled() && this.SettingsManager.IsToolSelectionVisible(this.Component))
|
||||
{
|
||||
<ToolSelection Component="@this.Component" LLMProvider="@this.ProviderSettings" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.IsProcessing" />
|
||||
}
|
||||
|
||||
<MudSpacer />
|
||||
<HalluzinationReminder ContainerClass="my-0 ml-2"/>
|
||||
</MudStack>
|
||||
|
||||
@ -6,6 +6,7 @@ using AIStudio.Tools.AIJobs;
|
||||
using AIStudio.Tools.AssistantSessions;
|
||||
using AIStudio.Tools.Media;
|
||||
using AIStudio.Tools.Services;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@ -130,8 +131,10 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
|
||||
protected virtual bool HasSettingsPanel => typeof(TSettings) != typeof(NoSettingsPanel);
|
||||
|
||||
protected HashSet<string> selectedToolIds = [];
|
||||
|
||||
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
|
||||
|
||||
|
||||
protected MudForm? Form;
|
||||
protected CancellationTokenSource? CancellationTokenSource;
|
||||
private bool isDisposed;
|
||||
@ -183,6 +186,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
|
||||
this.CurrentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
|
||||
this.CurrentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(this.Component);
|
||||
this.selectedToolIds = this.SettingsManager.GetDefaultToolIds(this.Component);
|
||||
this.assistantSessionKey = new(this.Component, this.AssistantSessionInstanceId);
|
||||
await this.AttachAssistantSessionIfAvailable();
|
||||
await this.ConsumeMediaOutcomeAsync();
|
||||
@ -233,6 +237,10 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
|
||||
private async Task Start()
|
||||
{
|
||||
await this.RefreshProviderSelectionFromConfigurationAsync();
|
||||
if (this.ProviderSettings == Settings.Provider.NONE)
|
||||
return;
|
||||
|
||||
if (this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner))
|
||||
return;
|
||||
|
||||
@ -349,6 +357,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = string.Format(this.TB("Assistant - {0}"), this.Title),
|
||||
Blocks = [],
|
||||
RuntimeComponent = this.Component,
|
||||
};
|
||||
}
|
||||
|
||||
@ -365,16 +374,30 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
ChatId = chatId,
|
||||
Name = name,
|
||||
Blocks = [],
|
||||
RuntimeComponent = this.Component,
|
||||
};
|
||||
|
||||
return chatId;
|
||||
}
|
||||
|
||||
private Task RefreshProviderSelectionFromConfigurationAsync()
|
||||
{
|
||||
this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(this.Component, this.ProviderSettings.Id);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected virtual void ResetProviderAndProfileSelection()
|
||||
{
|
||||
this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
|
||||
this.CurrentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
|
||||
this.CurrentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(this.Component);
|
||||
this.selectedToolIds = this.SettingsManager.GetDefaultToolIds(this.Component);
|
||||
}
|
||||
|
||||
protected Task SelectedToolIdsChanged(HashSet<string> updatedToolIds)
|
||||
{
|
||||
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(updatedToolIds);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false, params List<FileAttachment> attachments)
|
||||
@ -435,6 +458,10 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
{
|
||||
this.ChatThread.Blocks.Add(this.ResultingContentBlock);
|
||||
this.ChatThread.SelectedProvider = this.ProviderSettings.Id;
|
||||
this.ChatThread.RuntimeComponent = this.Component;
|
||||
this.ChatThread.RuntimeSelectedToolIds = this.SettingsManager.IsToolSelectionVisible(this.Component)
|
||||
? this.SettingsManager.FilterToolIdsForProvider(this.ProviderSettings, this.selectedToolIds)
|
||||
: [];
|
||||
}
|
||||
|
||||
this.IsProcessing = true;
|
||||
@ -916,4 +943,4 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
protected virtual void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) { }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -2293,21 +2293,42 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T601166687"] = "AI"
|
||||
-- Edit Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1183581066"] = "Edit Message"
|
||||
|
||||
-- Result
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347088452"] = "Result"
|
||||
|
||||
-- Do you really want to remove this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Do you really want to remove this message?"
|
||||
|
||||
-- Yes, remove the AI response and edit it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Yes, remove the AI response and edit it"
|
||||
|
||||
-- Failed
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1434043348"] = "Failed"
|
||||
|
||||
-- Tool Calls ({0})
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1493057571"] = "Tool Calls ({0})"
|
||||
|
||||
-- Executed
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1564757972"] = "Executed"
|
||||
|
||||
-- Yes, regenerate it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Yes, regenerate it"
|
||||
|
||||
-- No result
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1684269223"] = "No result"
|
||||
|
||||
-- Yes, remove it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1820166585"] = "Yes, remove it"
|
||||
|
||||
-- Number of sources
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1848978959"] = "Number of sources"
|
||||
|
||||
-- Show {0} tool calls
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1981771421"] = "Show {0} tool calls"
|
||||
|
||||
-- Show tool call for {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2004842583"] = "Show tool call for {0}"
|
||||
|
||||
-- Do you really want to edit this message? In order to edit this message, the AI response will be deleted.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2018431076"] = "Do you really want to edit this message? In order to edit this message, the AI response will be deleted."
|
||||
|
||||
@ -2317,6 +2338,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Removes
|
||||
-- Regenerate Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Regenerate Message"
|
||||
|
||||
-- Arguments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2738624831"] = "Arguments"
|
||||
|
||||
-- Number of attachments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Number of attachments"
|
||||
|
||||
@ -2326,9 +2350,15 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Cannot
|
||||
-- Edit
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3267849393"] = "Edit"
|
||||
|
||||
-- Unknown
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3424652889"] = "Unknown"
|
||||
|
||||
-- Regenerate
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3587744975"] = "Regenerate"
|
||||
|
||||
-- Blocked
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3816336467"] = "Blocked"
|
||||
|
||||
-- Do you really want to regenerate this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3878878761"] = "Do you really want to regenerate this message?"
|
||||
|
||||
@ -2338,9 +2368,15 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove
|
||||
-- No, keep it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "No, keep it"
|
||||
|
||||
-- No tool calls
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4224149521"] = "No tool calls"
|
||||
|
||||
-- Export Chat to Microsoft Word
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T861873672"] = "Export Chat to Microsoft Word"
|
||||
|
||||
-- No arguments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T931993614"] = "No arguments"
|
||||
|
||||
-- The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T3267850764"] = "The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings."
|
||||
|
||||
@ -2656,15 +2692,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T252
|
||||
-- Select a minimum confidence level
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T2579793544"] = "Select a minimum confidence level"
|
||||
|
||||
-- You have selected 1 preview feature.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMULTISELECT::T1384241824"] = "You have selected 1 preview feature."
|
||||
|
||||
-- No preview features selected.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMULTISELECT::T2809641588"] = "No preview features selected."
|
||||
|
||||
-- You have selected {0} preview features.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMULTISELECT::T3513450626"] = "You have selected {0} preview features."
|
||||
|
||||
-- Preselected provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONPROVIDERSELECTION::T1469984996"] = "Preselected provider"
|
||||
|
||||
@ -3550,6 +3577,42 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237
|
||||
-- Export configuration
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T975426229"] = "Export configuration"
|
||||
|
||||
-- Settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T1258653480"] = "Settings"
|
||||
|
||||
-- Description
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T1725856265"] = "Description"
|
||||
|
||||
-- Icon
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T1759955728"] = "Icon"
|
||||
|
||||
-- This tool still needs to be configured.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T1958939818"] = "This tool still needs to be configured."
|
||||
|
||||
-- Missing required settings: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T2588115579"] = "Missing required settings: {0}"
|
||||
|
||||
-- Name
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T266367750"] = "Name"
|
||||
|
||||
-- No minimum confidence level chosen
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T2828607242"] = "No minimum confidence level chosen"
|
||||
|
||||
-- Minimum provider confidence
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3461070436"] = "Minimum provider confidence"
|
||||
|
||||
-- Configure global settings for each tool.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3728248397"] = "Configure global settings for each tool."
|
||||
|
||||
-- Tool Settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3730473128"] = "Tool Settings"
|
||||
|
||||
-- This tool has been disabled by your organization.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3794167684"] = "This tool has been disabled by your organization."
|
||||
|
||||
-- Status
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T6222351"] = "Status"
|
||||
|
||||
-- No transcription provider configured yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1079350363"] = "No transcription provider configured yet."
|
||||
|
||||
@ -3622,6 +3685,63 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::THIRDPARTYCOMPONENT::T1392042694"] = "Ope
|
||||
-- License:
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::THIRDPARTYCOMPONENT::T1908172666"] = "License:"
|
||||
|
||||
-- Tool selection is hidden
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T2096103917"] = "Tool selection is hidden"
|
||||
|
||||
-- You have selected 1 tool.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T2493128368"] = "You have selected 1 tool."
|
||||
|
||||
-- Choose which tools should be preselected for new runs of this assistant.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T2696618758"] = "Choose which tools should be preselected for new runs of this assistant."
|
||||
|
||||
-- Default tools for this assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3253667950"] = "Default tools for this assistant"
|
||||
|
||||
-- Tool selection is visible
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3384582069"] = "Tool selection is visible"
|
||||
|
||||
-- Show tool selection in this assistant?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3494508870"] = "Show tool selection in this assistant?"
|
||||
|
||||
-- You have selected {0} tools.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3729156356"] = "You have selected {0} tools."
|
||||
|
||||
-- No tools selected.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3934845540"] = "No tools selected."
|
||||
|
||||
-- Default tools for chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T907403808"] = "Default tools for chat"
|
||||
|
||||
-- Choose which tools should be preselected for new chats.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T948842182"] = "Choose which tools should be preselected for new chats."
|
||||
|
||||
-- Tool changes are locked while a response is running. Your current selection is shown below and applies again from the next message once the run is finished.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T1688023907"] = "Tool changes are locked while a response is running. Your current selection is shown below and applies again from the next message once the run is finished."
|
||||
|
||||
-- Tools allow the LLM to perform targeted additional actions such as web searches or reading web pages.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T1944689297"] = "Tools allow the LLM to perform targeted additional actions such as web searches or reading web pages."
|
||||
|
||||
-- Required settings are missing. Configure this tool before enabling it.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3119156561"] = "Required settings are missing. Configure this tool before enabling it."
|
||||
|
||||
-- Close
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3448155331"] = "Close"
|
||||
|
||||
-- This tool has been disabled by your organization.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3794167684"] = "This tool has been disabled by your organization."
|
||||
|
||||
-- No tools are available in this context.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3904490680"] = "No tools are available in this context."
|
||||
|
||||
-- This tool requires provider confidence {0}. The selected provider has {1}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T4097602620"] = "This tool requires provider confidence {0}. The selected provider has {1}."
|
||||
|
||||
-- Tool Selection
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T749664565"] = "Tool Selection"
|
||||
|
||||
-- Select tools
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T998515990"] = "Select tools"
|
||||
|
||||
-- You'll interact with the AI systems using your voice. To achieve this, we want to integrate voice input (speech-to-text) and output (text-to-speech). However, later on, it should also have a natural conversation flow, i.e., seamless conversation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1015366320"] = "You'll interact with the AI systems using your voice. To achieve this, we want to integrate voice input (speech-to-text) and output (text-to-speech). However, later on, it should also have a natural conversation flow, i.e., seamless conversation."
|
||||
|
||||
@ -6409,6 +6529,30 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3547
|
||||
-- Preselect e-mail options?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3832719342"] = "Preselect e-mail options?"
|
||||
|
||||
-- Save
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T1294818664"] = "Save"
|
||||
|
||||
-- Please configure the required settings: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T2412603418"] = "Please configure the required settings: {0}"
|
||||
|
||||
-- Not set
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3616903110"] = "Not set"
|
||||
|
||||
-- Tool Settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3730473128"] = "Tool Settings"
|
||||
|
||||
-- This tool has been disabled by your organization.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3794167684"] = "This tool has been disabled by your organization."
|
||||
|
||||
-- The selected tool could not be loaded.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3907843187"] = "The selected tool could not be loaded."
|
||||
|
||||
-- {0} Default: {1}
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T403490413"] = "{0} Default: {1}"
|
||||
|
||||
-- Cancel
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T900713019"] = "Cancel"
|
||||
|
||||
-- Save
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1294818664"] = "Save"
|
||||
|
||||
@ -7489,6 +7633,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3014737766"] = "We tried to
|
||||
-- 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}'.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3049689432"] = "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}'."
|
||||
|
||||
-- The tool calling request failed with status code {0}. See the logs for details.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3117779001"] = "The tool calling request failed with status code {0}. See the logs for details."
|
||||
|
||||
-- Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3573577433"] = "Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'"
|
||||
|
||||
@ -7579,6 +7726,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T37333904
|
||||
-- We could not load models from '{0}' due to an unknown error.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T3907712809"] = "We could not load models from '{0}' due to an unknown error."
|
||||
|
||||
-- The tool calling request failed with status code {0}. See the logs for details.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T3117779001"] = "The tool calling request failed with status code {0}. See the logs for details."
|
||||
|
||||
-- It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again."
|
||||
|
||||
@ -9058,6 +9208,147 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Sources pro
|
||||
-- Sources provided by the AI
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Sources provided by the AI"
|
||||
|
||||
-- Tool
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T3517012711"] = "Tool"
|
||||
|
||||
-- Tool description
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T4056470505"] = "Tool description"
|
||||
|
||||
-- (Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T1410249500"] = "(Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication."
|
||||
|
||||
-- Allowed private hosts must be host names only, without scheme or path.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2196457612"] = "Allowed private hosts must be host names only, without scheme or path."
|
||||
|
||||
-- Maximum Content Characters
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2801581200"] = "Maximum Content Characters"
|
||||
|
||||
-- Allowed private host '{0}' is not valid.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3089707139"] = "Allowed private host '{0}' is not valid."
|
||||
|
||||
-- Allowed Private Hosts
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3415515539"] = "Allowed Private Hosts"
|
||||
|
||||
-- Timeout Seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3567699845"] = "Timeout Seconds"
|
||||
|
||||
-- Read Web Page
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3612587998"] = "Read Web Page"
|
||||
|
||||
-- Load a web page and extract its readable content, links, and page details.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3715690061"] = "Load a web page and extract its readable content, links, and page details."
|
||||
|
||||
-- The web page was not loaded because private or VPN web pages require a High-confidence provider.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3856267430"] = "The web page was not loaded because private or VPN web pages require a High-confidence provider."
|
||||
|
||||
-- (Optional) HTTP timeout for loading a web page in seconds.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T4126164830"] = "(Optional) HTTP timeout for loading a web page in seconds."
|
||||
|
||||
-- The setting '{0}' must be a positive integer.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T4199432074"] = "The setting '{0}' must be a positive integer."
|
||||
|
||||
-- (Optional) Global truncation limit for extracted characters returned to the model.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T900659180"] = "(Optional) Global truncation limit for extracted characters returned to the model."
|
||||
|
||||
-- Maximum Results
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1273024715"] = "Maximum Results"
|
||||
|
||||
-- Optional comma-separated default categories. Do not set this together with default engines.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1342681591"] = "Optional comma-separated default categories. Do not set this together with default engines."
|
||||
|
||||
-- Default Safe Search
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1343180281"] = "Default Safe Search"
|
||||
|
||||
-- The setting '{0}' must be less than or equal to {1}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1391527409"] = "The setting '{0}' must be less than or equal to {1}."
|
||||
|
||||
-- Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1739312423"] = "Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint."
|
||||
|
||||
-- A SearXNG URL is required.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1746583720"] = "A SearXNG URL is required."
|
||||
|
||||
-- Default Engines
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1865580137"] = "Default Engines"
|
||||
|
||||
-- Optional fallback language code when the model does not provide a language.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1868101906"] = "Optional fallback language code when the model does not provide a language."
|
||||
|
||||
-- Default Categories
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T2053347010"] = "Default Categories"
|
||||
|
||||
-- The total content budget must reserve at least {0} characters for each of up to {1} results.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T2124070269"] = "The total content budget must reserve at least {0} characters for each of up to {1} results."
|
||||
|
||||
-- Retrieval Timeout Seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T2479422697"] = "Retrieval Timeout Seconds"
|
||||
|
||||
-- Default Language
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T2526826120"] = "Default Language"
|
||||
|
||||
-- The configured web search content budget is not valid.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T299004879"] = "The configured web search content budget is not valid."
|
||||
|
||||
-- The configured SearXNG URL is not a valid absolute URL.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3038368943"] = "The configured SearXNG URL is not a valid absolute URL."
|
||||
|
||||
-- Optional HTTP timeout for the search request in seconds.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3078115445"] = "Optional HTTP timeout for the search request in seconds."
|
||||
|
||||
-- The default safe search setting must be 0, 1, or 2.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3187215042"] = "The default safe search setting must be 0, 1, or 2."
|
||||
|
||||
-- Search the web with a configured SearXNG instance and retrieve the readable content of the best matching pages.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3361633224"] = "Search the web with a configured SearXNG instance and retrieve the readable content of the best matching pages."
|
||||
|
||||
-- Page Timeout Seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3459475852"] = "Page Timeout Seconds"
|
||||
|
||||
-- Timeout Seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3567699845"] = "Timeout Seconds"
|
||||
|
||||
-- Optional default maximum number of results returned to the model when the model does not provide a limit.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3603838271"] = "Optional default maximum number of results returned to the model when the model does not provide a limit."
|
||||
|
||||
-- Maximum Total Content Characters
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T366488298"] = "Maximum Total Content Characters"
|
||||
|
||||
-- Optional timeout for loading each individual result page in seconds.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3668086641"] = "Optional timeout for loading each individual result page in seconds."
|
||||
|
||||
-- Web Search
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3815068443"] = "Web Search"
|
||||
|
||||
-- Optional overall timeout for retrieving all result pages in seconds.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3854998169"] = "Optional overall timeout for retrieving all result pages in seconds."
|
||||
|
||||
-- Optional safe search policy sent to SearXNG when configured.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3967748757"] = "Optional safe search policy sent to SearXNG when configured."
|
||||
|
||||
-- Default categories and default engines cannot both be set for the web search tool.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T4009446158"] = "Default categories and default engines cannot both be set for the web search tool."
|
||||
|
||||
-- Optional comma-separated default engines. Do not set this together with default categories.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T4108908537"] = "Optional comma-separated default engines. Do not set this together with default categories."
|
||||
|
||||
-- The setting '{0}' must be a positive integer.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T4199432074"] = "The setting '{0}' must be a positive integer."
|
||||
|
||||
-- Optional minimum character budget reserved for each successfully retrieved page.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T647700675"] = "Optional minimum character budget reserved for each successfully retrieved page."
|
||||
|
||||
-- Minimum Content Characters Per Result
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T75712506"] = "Minimum Content Characters Per Result"
|
||||
|
||||
-- Optional total character budget shared by all retrieved pages.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T836062282"] = "Optional total character budget shared by all retrieved pages."
|
||||
|
||||
-- The configured SearXNG URL must start with http:// or https://.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T944878454"] = "The configured SearXNG URL must start with http:// or https://."
|
||||
|
||||
-- SearXNG URL
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T993547568"] = "SearXNG URL"
|
||||
|
||||
-- Pandoc Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::USERFILE::T185447014"] = "Pandoc Installation"
|
||||
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
using AIStudio.Tools.ERIClient.DataModel;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
@ -76,6 +80,18 @@ public sealed record ChatThread
|
||||
/// </summary>
|
||||
public DataSourceSecurity DataSecurity { get; set; } = DataSourceSecurity.NOT_SPECIFIED;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum confidence required for providers that continue this chat after a tool returned sensitive data.
|
||||
/// </summary>
|
||||
[JsonInclude]
|
||||
public ConfidenceLevel RequiredProviderConfidence { get; private set; } = ConfidenceLevel.NONE;
|
||||
|
||||
public void RequireProviderConfidence(ConfidenceLevel minimumProviderConfidence)
|
||||
{
|
||||
if (minimumProviderConfidence > this.RequiredProviderConfidence)
|
||||
this.RequiredProviderConfidence = minimumProviderConfidence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
||||
/// </summary>
|
||||
@ -90,6 +106,12 @@ public sealed record ChatThread
|
||||
/// The content blocks of the chat thread.
|
||||
/// </summary>
|
||||
public List<ContentBlock> Blocks { get; init; } = [];
|
||||
|
||||
[JsonIgnore]
|
||||
public AIStudio.Tools.Components RuntimeComponent { get; set; } = AIStudio.Tools.Components.CHAT;
|
||||
|
||||
[JsonIgnore]
|
||||
public HashSet<string> RuntimeSelectedToolIds { get; set; } = [];
|
||||
|
||||
private bool allowProfile = true;
|
||||
|
||||
@ -103,7 +125,7 @@ public sealed record ChatThread
|
||||
/// </remarks>
|
||||
/// <param name="settingsManager">The settings manager instance to use.</param>
|
||||
/// <returns>The prepared system prompt.</returns>
|
||||
public string PrepareSystemPrompt(SettingsManager settingsManager)
|
||||
public string PrepareSystemPrompt(SettingsManager settingsManager, IEnumerable<ToolDefinition>? runnableToolDefinitions = null)
|
||||
{
|
||||
this.allowProfile = true;
|
||||
|
||||
@ -198,6 +220,17 @@ public sealed record ChatThread
|
||||
}
|
||||
|
||||
LOGGER.LogInformation(logMessage);
|
||||
|
||||
var toolPolicy = ToolSelectionRules.BuildToolPolicyPrompt(runnableToolDefinitions ?? []);
|
||||
if (!string.IsNullOrWhiteSpace(toolPolicy))
|
||||
{
|
||||
systemPromptText = $"""
|
||||
{systemPromptText}
|
||||
|
||||
{toolPolicy}
|
||||
""";
|
||||
}
|
||||
|
||||
if(!this.IncludeDateTime)
|
||||
return systemPromptText;
|
||||
|
||||
@ -314,4 +347,4 @@ public sealed record ChatThread
|
||||
|
||||
return new Tools.ERIClient.DataModel.ChatThread { ContentBlocks = contentBlocks };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,17 @@ public static class ChatThreadExtensions
|
||||
if (chatThread is null)
|
||||
return true;
|
||||
|
||||
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||
var providerConfidence = provider switch
|
||||
{
|
||||
IProvider p => p.Provider.GetConfidence(settingsManager).Level,
|
||||
AIStudio.Settings.Provider p => p.UsedLLMProvider.GetConfidence(settingsManager).Level,
|
||||
|
||||
_ => ConfidenceLevel.UNKNOWN,
|
||||
};
|
||||
if (providerConfidence < chatThread.RequiredProviderConfidence)
|
||||
return false;
|
||||
|
||||
// The chat thread is available, but the data security is not specified.
|
||||
// Means, we never used RAG or RAG was enabled, but no data sources were selected.
|
||||
// That's fine as well:
|
||||
@ -36,7 +47,6 @@ public static class ChatThreadExtensions
|
||||
//
|
||||
// Is the provider trusted for data-source security checks?
|
||||
//
|
||||
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||
var isTrustedProvider = provider switch
|
||||
{
|
||||
IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),
|
||||
@ -57,4 +67,4 @@ public static class ChatThreadExtensions
|
||||
false => chatThread.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,9 +11,27 @@
|
||||
</MudAvatar>
|
||||
</CardHeaderAvatar>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.body1">
|
||||
@this.Role.ToName() (@this.Time.LocalDateTime)
|
||||
</MudText>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudText Typo="Typo.body1">
|
||||
@this.Role.ToName() (@this.Time.LocalDateTime)
|
||||
</MudText>
|
||||
@if (this.HasToolTrace)
|
||||
{
|
||||
<MudTooltip Text="@this.GetToolTraceTooltip()" Placement="Placement.Bottom">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Default"
|
||||
Size="Size.Small"
|
||||
Class="px-2 py-1 rounded-pill"
|
||||
Style="min-width:auto; border-width:1px; text-transform:none;"
|
||||
OnClick="@this.ToggleToolTrace">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Build" Color="Color.Default" Size="Size.Small" />
|
||||
<MudIcon Icon="@(this.showToolTrace ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)" Size="Size.Small" />
|
||||
</MudStack>
|
||||
</MudButton>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
</CardHeaderContent>
|
||||
<CardHeaderActions>
|
||||
@if (this.Content.FileAttachments.Count > 0)
|
||||
@ -96,6 +114,67 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (this.HasToolTrace && this.showToolTrace)
|
||||
{
|
||||
<MudPaper Class="pa-3 mb-3 border rounded-lg" Style="border-width:1px;">
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">
|
||||
@string.Format(T("Tool Calls ({0})"), textContent.ToolInvocations.Count)
|
||||
</MudText>
|
||||
@foreach (var invocation in textContent.ToolInvocations.OrderBy(x => x.Order))
|
||||
{
|
||||
<MudPaper Class="pa-3 mb-3 border rounded-lg" Style="border-width:1px;">
|
||||
<MudButton Variant="Variant.Text"
|
||||
Color="Color.Default"
|
||||
FullWidth="@true"
|
||||
Class="px-0 py-0 justify-space-between"
|
||||
Style="min-width:auto; text-transform:none;"
|
||||
OnClick="@(() => this.ToggleToolInvocation(invocation.Order))">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="w-100">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@invocation.ToolIcon" Color="Color.Info" />
|
||||
<MudText Typo="Typo.subtitle1">@($"{invocation.Order}. {invocation.ToolName}")</MudText>
|
||||
<MudChip T="string" Color="@ContentBlockComponent.GetTraceColor(invocation.Status)" Size="Size.Small" Variant="Variant.Outlined">
|
||||
@this.GetTraceStatusText(invocation)
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
<MudIcon Icon="@(this.IsToolInvocationExpanded(invocation.Order) ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)" Size="Size.Small" />
|
||||
</MudStack>
|
||||
</MudButton>
|
||||
|
||||
@if (this.IsToolInvocationExpanded(invocation.Order))
|
||||
{
|
||||
@if (!string.IsNullOrWhiteSpace(invocation.StatusMessage))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Warning" Class="mt-3 mb-3">@invocation.StatusMessage</MudText>
|
||||
}
|
||||
|
||||
<MudText Typo="Typo.subtitle2">@T("Result")</MudText>
|
||||
<MudPaper Class="pa-3 mt-2 mb-3">
|
||||
<MudText Typo="Typo.body2" Style="white-space: pre-wrap;">@this.GetToolInvocationResult(invocation)</MudText>
|
||||
</MudPaper>
|
||||
|
||||
<MudText Typo="Typo.subtitle2">@T("Arguments")</MudText>
|
||||
@if (invocation.Arguments.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mb-3">@T("No arguments")</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudList T="string" Dense="@true" Class="mb-0">
|
||||
@foreach (var argument in invocation.Arguments)
|
||||
{
|
||||
<MudListItem T="string">
|
||||
<MudText Typo="Typo.body2"><strong>@argument.Key:</strong> @argument.Value</MudText>
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
}
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
var renderPlan = this.GetMarkdownRenderPlan(textContent.Text);
|
||||
<div @ref="this.mathContentContainer" class="chat-math-container">
|
||||
@foreach (var segment in renderPlan.Segments)
|
||||
@ -115,6 +194,13 @@
|
||||
<MudMarkdown Value="@textContent.Sources.ToMarkdown()" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE" />
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (this.Role is ChatRole.AI && !string.IsNullOrWhiteSpace(textContent.ToolRuntimeStatus.Message))
|
||||
{
|
||||
<MudAlert Dense="@true" Severity="Severity.Info" Variant="Variant.Outlined" Class="mt-4">
|
||||
@textContent.ToolRuntimeStatus.Message
|
||||
</MudAlert>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Tools.Services;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
@ -103,6 +105,8 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
|
||||
private string lastMathRenderSignature = string.Empty;
|
||||
private bool hasActiveMathContainer;
|
||||
private bool isDisposed;
|
||||
private bool showToolTrace;
|
||||
private readonly HashSet<int> expandedToolInvocations = [];
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
@ -199,6 +203,27 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
|
||||
hash.Add(textValue.Length);
|
||||
hash.Add(textValue.GetHashCode(StringComparison.Ordinal));
|
||||
hash.Add(text.Sources.Count);
|
||||
hash.Add(text.ToolInvocations.Count);
|
||||
hash.Add(text.ToolRuntimeStatus.IsRunning);
|
||||
hash.Add(text.ToolRuntimeStatus.Message);
|
||||
hash.Add(this.showToolTrace);
|
||||
hash.Add(this.expandedToolInvocations.Count);
|
||||
foreach (var expandedInvocation in this.expandedToolInvocations.Order())
|
||||
hash.Add(expandedInvocation);
|
||||
foreach (var invocation in text.ToolInvocations)
|
||||
{
|
||||
hash.Add(invocation.Order);
|
||||
hash.Add(invocation.ToolId);
|
||||
hash.Add(invocation.Status);
|
||||
hash.Add(invocation.StatusMessage);
|
||||
hash.Add(invocation.Result);
|
||||
hash.Add(invocation.Arguments.Count);
|
||||
foreach (var argument in invocation.Arguments)
|
||||
{
|
||||
hash.Add(argument.Key);
|
||||
hash.Add(argument.Value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ContentImage image:
|
||||
@ -214,8 +239,55 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
private string CardClasses => $"my-2 rounded-lg {this.Class}";
|
||||
|
||||
private bool HasToolTrace => this.Role is ChatRole.AI && this.GetToolInvocations().Count > 0;
|
||||
|
||||
private CodeBlockTheme CodeColorPalette => this.SettingsManager.IsDarkMode ? CodeBlockTheme.Dark : CodeBlockTheme.Default;
|
||||
|
||||
private static Color GetTraceColor(ToolInvocationTraceStatus status) => status switch
|
||||
{
|
||||
ToolInvocationTraceStatus.SUCCESS => Color.Success,
|
||||
ToolInvocationTraceStatus.ERROR => Color.Error,
|
||||
ToolInvocationTraceStatus.BLOCKED => Color.Warning,
|
||||
_ => Color.Default,
|
||||
};
|
||||
|
||||
private string GetTraceStatusText(ToolInvocationTrace trace) => trace.Status switch
|
||||
{
|
||||
ToolInvocationTraceStatus.SUCCESS => this.T("Executed"),
|
||||
ToolInvocationTraceStatus.ERROR => this.T("Failed"),
|
||||
ToolInvocationTraceStatus.BLOCKED => this.T("Blocked"),
|
||||
_ => this.T("Unknown"),
|
||||
};
|
||||
|
||||
private IReadOnlyList<ToolInvocationTrace> GetToolInvocations() => this.Content is ContentText textContent
|
||||
? textContent.ToolInvocations.OrderBy(x => x.Order).ToList()
|
||||
: [];
|
||||
|
||||
private string GetToolTraceTooltip()
|
||||
{
|
||||
var invocations = this.GetToolInvocations();
|
||||
return invocations.Count switch
|
||||
{
|
||||
0 => this.T("No tool calls"),
|
||||
1 => string.Format(this.T("Show tool call for {0}"), invocations[0].ToolName),
|
||||
_ => string.Format(this.T("Show {0} tool calls"), invocations.Count),
|
||||
};
|
||||
}
|
||||
|
||||
private void ToggleToolTrace() => this.showToolTrace = !this.showToolTrace;
|
||||
|
||||
private bool IsToolInvocationExpanded(int order) => this.expandedToolInvocations.Contains(order);
|
||||
|
||||
private void ToggleToolInvocation(int order)
|
||||
{
|
||||
if (!this.expandedToolInvocations.Add(order))
|
||||
this.expandedToolInvocations.Remove(order);
|
||||
}
|
||||
|
||||
private string GetToolInvocationResult(ToolInvocationTrace invocation) => string.IsNullOrWhiteSpace(invocation.Result)
|
||||
? this.T("No result")
|
||||
: invocation.Result;
|
||||
|
||||
private MudMarkdownStyling MarkdownStyling => new()
|
||||
{
|
||||
CodeBlock = { Theme = this.CodeColorPalette },
|
||||
|
||||
@ -5,6 +5,7 @@ using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.RAG.RAGProcesses;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
@ -46,6 +47,11 @@ public sealed class ContentText : IContent
|
||||
/// <inheritdoc />
|
||||
public List<FileAttachment> FileAttachments { get; set; } = [];
|
||||
|
||||
public List<ToolInvocationTrace> ToolInvocations { get; set; } = [];
|
||||
|
||||
[JsonIgnore]
|
||||
public ToolRuntimeStatus ToolRuntimeStatus { get; set; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastUserPrompt, ChatThread? chatThread, CancellationToken token = default)
|
||||
{
|
||||
@ -248,6 +254,19 @@ public sealed class ContentText : IContent
|
||||
IsStreaming = this.IsStreaming,
|
||||
Sources = [..this.Sources],
|
||||
FileAttachments = [..this.FileAttachments],
|
||||
ToolInvocations = [..this.ToolInvocations.Select(x => new ToolInvocationTrace
|
||||
{
|
||||
Order = x.Order,
|
||||
ToolId = x.ToolId,
|
||||
ToolName = x.ToolName,
|
||||
ToolIcon = x.ToolIcon,
|
||||
ToolCallId = x.ToolCallId,
|
||||
Status = x.Status,
|
||||
WasExecuted = x.WasExecuted,
|
||||
StatusMessage = x.StatusMessage,
|
||||
Arguments = new Dictionary<string, string>(x.Arguments, StringComparer.Ordinal),
|
||||
Result = x.Result,
|
||||
})],
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
@ -124,6 +124,11 @@
|
||||
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
|
||||
|
||||
<ProfileSelection MarginLeft="" CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" Disabled="@(!this.currentChatTemplate.AllowProfileUsage)" DisabledText="@T("Profile usage is disabled according to your chat template settings.")"/>
|
||||
|
||||
@if (this.SettingsManager.AreToolsEnabled())
|
||||
{
|
||||
<ToolSelection Component="Components.CHAT" LLMProvider="@this.Provider" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.IsCurrentChatStreaming" />
|
||||
}
|
||||
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
|
||||
@ -3,6 +3,7 @@ using AIStudio.Dialogs;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
using AIStudio.Tools.Media;
|
||||
using AIStudio.Tools.Services;
|
||||
@ -76,6 +77,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
private bool mustLoadChat;
|
||||
private LoadChat loadChat;
|
||||
private bool autoSaveEnabled;
|
||||
private HashSet<string> selectedToolIds = [];
|
||||
private bool previousInputForbidden = true;
|
||||
private Guid lastSeenChatId = Guid.Empty;
|
||||
private AIStudio.Settings.Provider lastSeenProvider = AIStudio.Settings.Provider.NONE;
|
||||
@ -113,7 +115,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
|
||||
|
||||
|
||||
// Apply the filters for the message bus:
|
||||
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED, Event.WORKSPACE_RENAMED, Event.CONFIGURATION_CHANGED ]);
|
||||
|
||||
@ -128,6 +130,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
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();
|
||||
|
||||
@ -769,6 +772,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if (this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner))
|
||||
return;
|
||||
|
||||
await this.RefreshProviderSelectionFromConfigurationAsync();
|
||||
|
||||
if (!this.IsProviderSelected)
|
||||
return;
|
||||
|
||||
@ -890,15 +895,17 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
}
|
||||
|
||||
this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'.");
|
||||
this.StateHasChanged();
|
||||
this.ChatThread!.RuntimeComponent = Tools.Components.CHAT;
|
||||
this.ChatThread.RuntimeSelectedToolIds = this.SettingsManager.FilterToolIdsForProvider(this.Provider, this.selectedToolIds);
|
||||
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
|
||||
{
|
||||
ChatThread = this.ChatThread!,
|
||||
ChatThread = this.ChatThread,
|
||||
AIText = aiText,
|
||||
LastUserPrompt = lastUserPrompt,
|
||||
ProviderSettings = this.Provider,
|
||||
IsForeground = true,
|
||||
});
|
||||
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.StateHasChanged();
|
||||
}
|
||||
@ -908,6 +915,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if (this.ChatThread is not null)
|
||||
await this.AIJobService.CancelChatGenerationAsync(this.ChatThread.ChatId);
|
||||
}
|
||||
|
||||
private Task SelectedToolIdsChanged(HashSet<string> updatedToolIds)
|
||||
{
|
||||
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(updatedToolIds);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task SaveThread()
|
||||
{
|
||||
@ -971,6 +984,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
//
|
||||
this.hasUnsavedChanges = false;
|
||||
this.ComposerState.Clear();
|
||||
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT));
|
||||
this.RefreshCurrentProfileAndChatTemplate();
|
||||
|
||||
//
|
||||
@ -1114,6 +1128,19 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task RefreshProviderSelectionFromConfigurationAsync()
|
||||
{
|
||||
var updatedProvider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT, this.Provider.Id);
|
||||
var providerChanged = updatedProvider != this.Provider;
|
||||
if (providerChanged)
|
||||
this.Provider = updatedProvider;
|
||||
|
||||
if (!providerChanged)
|
||||
return;
|
||||
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
}
|
||||
|
||||
private async Task ResetState()
|
||||
{
|
||||
@ -1266,6 +1293,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.StateHasChanged();
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1304,4 +1332,4 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +33,15 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<TData, bool> IsItemLocked { get; set; } = _ => false;
|
||||
|
||||
[Parameter]
|
||||
public string EmptySelectionText { get; set; } = "No items selected.";
|
||||
|
||||
[Parameter]
|
||||
public string SingleSelectionText { get; set; } = "You have selected 1 item.";
|
||||
|
||||
[Parameter]
|
||||
public string MultipleSelectionText { get; set; } = "You have selected {0} items.";
|
||||
|
||||
#region Overrides of ConfigurationBase
|
||||
|
||||
@ -61,12 +70,12 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
|
||||
private string GetMultiSelectionText(List<TData?>? selectedValues)
|
||||
{
|
||||
if(selectedValues is null || selectedValues.Count == 0)
|
||||
return T("No preview features selected.");
|
||||
return T(this.EmptySelectionText);
|
||||
|
||||
if(selectedValues.Count == 1)
|
||||
return T("You have selected 1 preview feature.");
|
||||
return T(this.SingleSelectionText);
|
||||
|
||||
return string.Format(T("You have selected {0} preview features."), selectedValues.Count);
|
||||
return string.Format(T(this.MultipleSelectionText), selectedValues.Count);
|
||||
}
|
||||
|
||||
private bool IsLockedValue(TData value) => this.IsItemLocked(value);
|
||||
@ -76,4 +85,4 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
|
||||
"This feature is managed by your organization and has therefore been disabled.",
|
||||
typeof(ConfigurationBase).Namespace,
|
||||
nameof(ConfigurationBase));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
@using AIStudio.Provider
|
||||
@using AIStudio.Tools.ToolCallingSystem
|
||||
@inherits SettingsPanelBase
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Build" HeaderText="@T("Tool Settings")">
|
||||
<MudText Typo="Typo.body1" Class="mb-4">
|
||||
@T("Configure global settings for each tool.")
|
||||
</MudText>
|
||||
|
||||
<MudTable Items="@this.items" Hover="@true" Dense="@true">
|
||||
<HeaderContent>
|
||||
<MudTh>@T("Icon")</MudTh>
|
||||
<MudTh>@T("Name")</MudTh>
|
||||
<MudTh>@T("Description")</MudTh>
|
||||
<MudTh>@T("Minimum provider confidence")</MudTh>
|
||||
<MudTh>@T("Status")</MudTh>
|
||||
<MudTh>@T("Settings")</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>
|
||||
<MudIcon Icon="@context.Implementation.Icon" Color="Color.Info" />
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudText Typo="Typo.body1">@context.Implementation.GetDisplayName()</MudText>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudText Typo="Typo.body2">@context.Implementation.GetDescription()</MudText>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)" Disabled="@this.IsToolConfidenceManaged()">
|
||||
@foreach (var confidenceLevel in this.GetSelectableConfidenceLevels())
|
||||
{
|
||||
<MudMenuItem OnClick="@(async () => await this.ChangeMinimumProviderConfidence(context, confidenceLevel))">
|
||||
@this.GetConfidenceLevelName(confidenceLevel)
|
||||
</MudMenuItem>
|
||||
}
|
||||
</MudMenu>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
@if (!context.IsActive)
|
||||
{
|
||||
<MudTooltip Text="@T("This tool has been disabled by your organization.")">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Error" />
|
||||
</MudTooltip>
|
||||
}
|
||||
else if (context.ConfigurationState.IsConfigured)
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="@this.GetConfigurationTooltip(context)">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" />
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Settings" OnClick="@(async () => await this.OpenSettings(context.Definition.Id))" />
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</ExpansionPanel>
|
||||
@ -0,0 +1,89 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Dialogs.Settings;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.Settings;
|
||||
|
||||
public partial class SettingsPanelTools : SettingsPanelBase
|
||||
{
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; init; } = null!;
|
||||
|
||||
private IReadOnlyList<ToolCatalogItem> items = [];
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
|
||||
this.items = await this.ToolRegistry.GetCatalogAsync(this.ToolRegistry.GetAllDefinitions());
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private async Task OpenSettings(string toolId)
|
||||
{
|
||||
var parameters = new DialogParameters<ToolSettingsDialog>
|
||||
{
|
||||
{ x => x.ToolId, toolId },
|
||||
};
|
||||
|
||||
var dialog = await this.DialogService.ShowAsync<ToolSettingsDialog>(null, parameters, Dialogs.DialogOptions.FULLSCREEN);
|
||||
await dialog.Result;
|
||||
this.items = await this.ToolRegistry.GetCatalogAsync(this.ToolRegistry.GetAllDefinitions());
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private string GetConfigurationTooltip(ToolCatalogItem item) => item.ConfigurationState.MissingRequiredFields.Count switch
|
||||
{
|
||||
_ when !string.IsNullOrWhiteSpace(item.ConfigurationState.Message) => item.ConfigurationState.Message,
|
||||
0 => this.T("This tool still needs to be configured."),
|
||||
_ => string.Format(this.T("Missing required settings: {0}"), string.Join(", ", item.ConfigurationState.MissingRequiredFields.Select(fieldName => this.GetFieldDisplayName(item, fieldName))))
|
||||
};
|
||||
|
||||
private string GetFieldDisplayName(ToolCatalogItem item, string fieldName)
|
||||
{
|
||||
var fieldDefinition = item.Definition.SettingsSchema.Properties.GetValueOrDefault(fieldName);
|
||||
if (fieldDefinition is null)
|
||||
return fieldName;
|
||||
|
||||
return item.Implementation.GetSettingsFieldLabel(fieldName, fieldDefinition);
|
||||
}
|
||||
|
||||
private IEnumerable<ConfidenceLevel> GetSelectableConfidenceLevels() =>
|
||||
Enum.GetValues<ConfidenceLevel>().OrderBy(x => x).Where(x => x is not ConfidenceLevel.UNKNOWN);
|
||||
|
||||
private string GetCurrentConfidenceLevelName(ToolCatalogItem item) => this.GetConfidenceLevelName(this.GetMinimumProviderConfidence(item));
|
||||
|
||||
private string GetConfidenceLevelName(ConfidenceLevel confidenceLevel) => confidenceLevel is ConfidenceLevel.NONE
|
||||
? this.T("No minimum confidence level chosen")
|
||||
: confidenceLevel.GetName();
|
||||
|
||||
private string SetCurrentConfidenceLevelColorStyle(ToolCatalogItem item) =>
|
||||
$"background-color: {this.GetMinimumProviderConfidence(item).GetColor(this.SettingsManager)};";
|
||||
|
||||
private bool IsToolConfidenceManaged() =>
|
||||
ManagedConfiguration.TryGet(x => x.Tools, x => x.MinimumProviderConfidenceByToolId, out var meta) && meta.IsLocked;
|
||||
|
||||
private ConfidenceLevel GetMinimumProviderConfidence(ToolCatalogItem item) => this.SettingsManager.GetMinimumProviderConfidenceForTool(item.Definition.Id);
|
||||
|
||||
private async Task ChangeMinimumProviderConfidence(ToolCatalogItem item, ConfidenceLevel confidenceLevel)
|
||||
{
|
||||
this.SettingsManager.SetMinimumProviderConfidenceForTool(item.Definition.Id, confidenceLevel);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
this.items = await this.ToolRegistry.GetCatalogAsync(this.ToolRegistry.GetAllDefinitions());
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
this.items = await this.ToolRegistry.GetCatalogAsync(this.ToolRegistry.GetAllDefinitions());
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
@using AIStudio.Tools
|
||||
@using AIStudio.Tools.ToolCallingSystem
|
||||
@inherits MSGComponentBase
|
||||
|
||||
@if (this.availableTools.Count > 0)
|
||||
{
|
||||
@if (this.Component is not Components.CHAT && this.IncludeVisibilityToggle)
|
||||
{
|
||||
<ConfigurationOption OptionDescription="@T("Show tool selection in this assistant?")" LabelOn="@T("Tool selection is visible")" LabelOff="@T("Tool selection is hidden")" State="@(() => this.SettingsManager.IsToolSelectionVisible(this.Component))" StateUpdate="@(value => this.SettingsManager.SetToolSelectionVisibility(this.Component, value))" />
|
||||
}
|
||||
<ConfigurationMultiSelect TData="string" OptionDescription="@this.OptionTitle" SelectedValues="@this.GetSelectedValues" Data="@this.availableTools" SelectionUpdate="@this.UpdateSelection" OptionHelp="@this.OptionHelp" Disabled="@(() => this.AreDefaultToolsDisabled)" IsItemLocked="@this.IsToolDisabled" EmptySelectionText="@T("No tools selected.")" SingleSelectionText="@T("You have selected 1 tool.")" MultipleSelectionText="@T("You have selected {0} tools.")" />
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class ToolDefaultsConfiguration : MSGComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public AIStudio.Tools.Components Component { get; set; } = AIStudio.Tools.Components.CHAT;
|
||||
|
||||
[Parameter]
|
||||
public bool IncludeVisibilityToggle { get; set; } = true;
|
||||
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; init; } = null!;
|
||||
|
||||
private List<ConfigurationSelectData<string>> availableTools = [];
|
||||
|
||||
private string OptionTitle => this.Component is AIStudio.Tools.Components.CHAT ? this.T("Default tools for chat") : this.T("Default tools for this assistant");
|
||||
|
||||
private string OptionHelp => this.Component is AIStudio.Tools.Components.CHAT
|
||||
? this.T("Choose which tools should be preselected for new chats.")
|
||||
: this.T("Choose which tools should be preselected for new runs of this assistant.");
|
||||
|
||||
private bool AreDefaultToolsDisabled =>
|
||||
this.Component is not AIStudio.Tools.Components.CHAT &&
|
||||
!this.SettingsManager.IsToolSelectionVisible(this.Component);
|
||||
|
||||
private bool IsToolDisabled(string toolId) => !this.SettingsManager.IsToolActive(toolId);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.availableTools = (await this.ToolRegistry.GetCatalogAsync(this.Component))
|
||||
.Select(x => new ConfigurationSelectData<string>(x.Implementation.GetDisplayName(), x.Definition.Id))
|
||||
.ToList();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private HashSet<string> GetSelectedValues() => this.SettingsManager.GetDefaultToolIds(this.Component);
|
||||
|
||||
private void UpdateSelection(HashSet<string> values) => this.SettingsManager.ConfigurationData.Tools.DefaultToolIdsByComponent[this.Component.ToString()] = [..ToolSelectionRules.NormalizeSelection(values)];
|
||||
}
|
||||
86
app/MindWork AI Studio/Components/ToolSelection.razor
Normal file
86
app/MindWork AI Studio/Components/ToolSelection.razor
Normal file
@ -0,0 +1,86 @@
|
||||
@using AIStudio.Settings
|
||||
@using AIStudio.Tools.ToolCallingSystem
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<div class="d-flex">
|
||||
<MudTooltip Text="@this.ToolButtonTooltip" Placement="Placement.Top">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Build" Class="@this.PopoverButtonClasses" OnClick="@this.ToggleSelection"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudPopover Open="@this.showSelection" AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" DropShadow="@true" Class="border-solid border-4 rounded-lg">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">@T("Tool Selection")</MudText>
|
||||
<MudSpacer />
|
||||
</MudStack>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent Style="min-width: 28em; max-height: 60vh; max-width: 48vw; overflow: auto;">
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Tools allow the LLM to perform targeted additional actions such as web searches or reading web pages.")
|
||||
</MudText>
|
||||
@if (!this.SupportsTools)
|
||||
{
|
||||
<MudText Typo="Typo.body1">@this.UnsupportedToolsMessage</MudText>
|
||||
}
|
||||
else if (this.Disabled)
|
||||
{
|
||||
<MudAlert Dense="@true" Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-3">
|
||||
@T("Tool changes are locked while a response is running. Your current selection is shown below and applies again from the next message once the run is finished.")
|
||||
</MudAlert>
|
||||
}
|
||||
else if (this.catalog.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1">@T("No tools are available in this context.")</MudText>
|
||||
}
|
||||
|
||||
@if (this.SupportsTools && this.catalog.Count > 0)
|
||||
{
|
||||
@foreach (var item in this.catalog)
|
||||
{
|
||||
var isSelected = this.SelectedToolIds.Contains(item.Definition.Id);
|
||||
var isConfigured = item.ConfigurationState.IsConfigured;
|
||||
var providerConfidenceHint = this.GetProviderConfidenceHint(item);
|
||||
var isBlockedByProviderConfidence = this.IsBlockedByProviderConfidence(item);
|
||||
<MudPaper Class="pa-2 mb-2 border rounded-lg">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudSwitch T="bool" Color="Color.Primary" Value="@isSelected" ValueChanged="@(value => this.ChangeSelection(item.Definition.Id, value))" Disabled="@(!item.IsActive || !isConfigured || isBlockedByProviderConfidence || this.Disabled || !this.SupportsTools)" />
|
||||
<MudIcon Icon="@item.Implementation.Icon" Color="Color.Info" />
|
||||
@if (!item.IsActive)
|
||||
{
|
||||
<MudTooltip Text="@T("This tool has been disabled by your organization.")">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Error" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudTooltip Text="@item.Implementation.GetDescription()">
|
||||
<MudText Typo="Typo.body1">@item.Implementation.GetDisplayName()</MudText>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Settings" OnClick="@(async () => await this.OpenSettings(item.Definition.Id))" />
|
||||
</MudStack>
|
||||
@if (!isConfigured)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Warning">@(string.IsNullOrWhiteSpace(item.ConfigurationState.Message) ? T("Required settings are missing. Configure this tool before enabling it.") : item.ConfigurationState.Message)</MudText>
|
||||
}
|
||||
@if (!item.IsActive)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Warning">@T("This tool has been disabled by your organization.")</MudText>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(providerConfidenceHint))
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Warning">@providerConfidenceHint</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
}
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudSpacer />
|
||||
<MudButton Variant="Variant.Filled" OnClick="@this.Hide">@T("Close")</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</MudPopover>
|
||||
</div>
|
||||
128
app/MindWork AI Studio/Components/ToolSelection.razor.cs
Normal file
128
app/MindWork AI Studio/Components/ToolSelection.razor.cs
Normal file
@ -0,0 +1,128 @@
|
||||
using AIStudio.Dialogs.Settings;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class ToolSelection : MSGComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public AIStudio.Tools.Components Component { get; set; } = AIStudio.Tools.Components.CHAT;
|
||||
|
||||
[Parameter]
|
||||
public required AIStudio.Settings.Provider LLMProvider { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public HashSet<string> SelectedToolIds { get; set; } = [];
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<HashSet<string>> SelectedToolIdsChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string PopoverButtonClasses { get; set; } = string.Empty;
|
||||
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
private bool showSelection;
|
||||
private IReadOnlyList<ToolCatalogItem> catalog = [];
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
this.SelectedToolIds = ToolSelectionRules.NormalizeSelection(this.SelectedToolIds);
|
||||
base.OnParametersSet();
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private ToolCallingAvailability ToolCallingAvailability => this.LLMProvider.GetToolCallingAvailability();
|
||||
|
||||
private bool SupportsTools => this.ToolCallingAvailability.IsAvailable;
|
||||
|
||||
private string ToolButtonTooltip => this.SupportsTools
|
||||
? this.T("Select tools")
|
||||
: this.UnsupportedToolsMessage;
|
||||
|
||||
private string UnsupportedToolsMessage => this.ToolCallingAvailability.Message;
|
||||
|
||||
private ConfidenceLevel ProviderConfidence => this.LLMProvider == AIStudio.Settings.Provider.NONE
|
||||
? ConfidenceLevel.NONE
|
||||
: this.LLMProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level;
|
||||
|
||||
private async Task ToggleSelection()
|
||||
{
|
||||
this.showSelection = !this.showSelection;
|
||||
if (this.showSelection)
|
||||
this.catalog = await this.ToolRegistry.GetCatalogAsync(this.Component);
|
||||
}
|
||||
|
||||
private void Hide() => this.showSelection = false;
|
||||
|
||||
private async Task ChangeSelection(string toolId, bool isSelected)
|
||||
{
|
||||
if (isSelected && !this.SettingsManager.IsToolActive(toolId))
|
||||
return;
|
||||
|
||||
var updated = new HashSet<string>(this.SelectedToolIds, StringComparer.Ordinal);
|
||||
if (isSelected)
|
||||
updated.Add(toolId);
|
||||
else
|
||||
updated.Remove(toolId);
|
||||
|
||||
updated = ToolSelectionRules.NormalizeSelection(updated);
|
||||
this.SelectedToolIds = updated;
|
||||
await this.SelectedToolIdsChanged.InvokeAsync(updated);
|
||||
}
|
||||
|
||||
private ConfidenceLevel GetMinimumProviderConfidence(ToolCatalogItem item) => this.SettingsManager.GetMinimumProviderConfidenceForTool(item.Definition.Id);
|
||||
|
||||
private bool IsBlockedByProviderConfidence(ToolCatalogItem item) => !ToolSelectionRules.IsProviderConfidenceAllowed(this.ProviderConfidence, this.GetMinimumProviderConfidence(item));
|
||||
|
||||
private string? GetProviderConfidenceHint(ToolCatalogItem item)
|
||||
{
|
||||
if (!this.IsBlockedByProviderConfidence(item))
|
||||
return null;
|
||||
|
||||
return string.Format(
|
||||
this.T("This tool requires provider confidence {0}. The selected provider has {1}."),
|
||||
this.GetMinimumProviderConfidence(item).GetName(),
|
||||
this.ProviderConfidence.GetName());
|
||||
}
|
||||
|
||||
private async Task OpenSettings(string toolId)
|
||||
{
|
||||
var parameters = new DialogParameters<ToolSettingsDialog>
|
||||
{
|
||||
{ x => x.ToolId, toolId },
|
||||
};
|
||||
|
||||
var dialog = await this.DialogService.ShowAsync<ToolSettingsDialog>(null, parameters, Dialogs.DialogOptions.FULLSCREEN);
|
||||
await dialog.Result;
|
||||
this.catalog = await this.ToolRegistry.GetCatalogAsync(this.Component);
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.CONFIGURATION_CHANGED when this.showSelection:
|
||||
this.catalog = await this.ToolRegistry.GetCatalogAsync(this.Component);
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -36,6 +36,7 @@
|
||||
<ConfigurationProviderSelection Component="Components.AGENDA_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.AGENDA_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
<ConfigurationProviderSelection Component="Components.BIAS_DAY_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
</MudField>
|
||||
<ToolDefaultsConfiguration Component="Components.BIAS_DAY_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
|
||||
@ -22,6 +22,8 @@
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect one of your chat templates?")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate)" Data="@ConfigurationSelectDataFactory.GetChatTemplatesData(this.SettingsManager.ConfigurationData.ChatTemplates)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate = selectedValue)" OptionHelp="@T("Would you like to set one of your chat templates as the default for chats?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedChatTemplate, out var meta) && meta.IsLocked"/>
|
||||
</MudPaper>
|
||||
|
||||
<ToolDefaultsConfiguration Component="Components.CHAT" IncludeVisibilityToggle="@false" />
|
||||
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<DataSourceSelection SelectionMode="DataSourceSelectionMode.CONFIGURATION_MODE" AutoSaveAppSettings="@true" @bind-DataSourceOptions="@this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions" ConfigurationHeaderMessage="@T("You can set default data sources and options for new chats. You can change these settings later for each individual chat.")"/>
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
<ConfigurationProviderSelection Component="Components.CODING_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Coding.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.CODING_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">Close</MudButton>
|
||||
|
||||
@ -19,10 +19,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.GRAMMAR_SPELLING_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.GRAMMAR_SPELLING_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
|
||||
@ -19,10 +19,11 @@
|
||||
<ConfigurationSelect OptionDescription="@T("Language plugin used for comparision")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.I18N.PreselectedLanguagePluginId)" Data="@ConfigurationSelectDataFactory.GetLanguagesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.I18N.PreselectedLanguagePluginId = selectedValue)" OptionHelp="@T("Select the language plugin used for comparision.")"/>
|
||||
<ConfigurationProviderSelection Component="Components.I18N_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.I18N.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.I18N.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.I18N.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.I18N_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
|
||||
@ -15,10 +15,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.ICON_FINDER_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.IconFinder.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.IconFinder.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.ICON_FINDER_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
|
||||
@ -26,10 +26,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.JOB_POSTING_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.JobPostings.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.JOB_POSTING_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
<ConfigurationProviderSelection Component="Components.LEGAL_CHECK_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.LEGAL_CHECK_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.MY_TASKS_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.MY_TASKS_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
|
||||
@ -21,10 +21,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.REWRITE_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.REWRITE_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
<ConfigurationProviderSelection Component="Components.SLIDE_BUILDER_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.SlideBuilder.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.SlideBuilder.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.SLIDE_BUILDER_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
|
||||
@ -19,10 +19,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.SYNONYMS_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Synonyms.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Synonyms.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.SYNONYMS_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
|
||||
@ -29,10 +29,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.TEXT_SUMMARIZER_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.TEXT_SUMMARIZER_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
|
||||
@ -23,10 +23,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Translation.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Translation.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Translation.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.TRANSLATION_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Translation.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Translation.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Translation.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.TRANSLATION_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
<ConfigurationProviderSelection Component="Components.EMAIL_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.EMail.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.EMAIL_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
@using AIStudio.Tools.ToolCallingSystem
|
||||
@inherits SettingsDialogBase
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6" Class="d-flex align-center">
|
||||
<MudIcon Icon="@this.implementation?.Icon" Class="mr-2" />
|
||||
@(this.implementation?.GetDisplayName() ?? T("Tool Settings"))
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (this.toolDefinition is null)
|
||||
{
|
||||
<MudText Typo="Typo.body1">@T("The selected tool could not be loaded.")</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-4">
|
||||
@this.implementation?.GetDescription()
|
||||
</MudJustifiedText>
|
||||
|
||||
@if (!this.SettingsManager.IsToolActive(this.toolDefinition.Id))
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-4">@T("This tool has been disabled by your organization.")</MudAlert>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(this.validationMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mb-4">@this.validationMessage</MudAlert>
|
||||
}
|
||||
|
||||
<MudPaper Class="pa-3 mb-4 border-dashed border rounded-lg">
|
||||
@foreach (var property in this.toolDefinition.SettingsSchema.Properties)
|
||||
{
|
||||
var fieldName = property.Key;
|
||||
var field = property.Value;
|
||||
if (field.EnumValues.Count > 0)
|
||||
{
|
||||
<MudSelect T="string" Label="@this.GetFieldLabel(fieldName, field)" Value="@this.GetValue(fieldName)" ValueChanged="@(value => this.UpdateValue(fieldName, value))" Variant="Variant.Outlined" Margin="Margin.Dense" HelperText="@this.GetFieldDescription(fieldName, field)" Placeholder="@this.GetFieldPlaceholder(fieldName, field)" Class="mb-3" Disabled="@this.IsFieldDisabled(fieldName)">
|
||||
@if (!this.toolDefinition.SettingsSchema.Required.Contains(fieldName))
|
||||
{
|
||||
<MudSelectItem T="string" Value="@string.Empty">@T("Not set")</MudSelectItem>
|
||||
}
|
||||
@foreach (var option in field.EnumValues)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@option">@option</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTextField T="string" Label="@this.GetFieldLabel(fieldName, field)" Value="@this.GetValue(fieldName)" ValueChanged="@(value => this.UpdateValue(fieldName, value))" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3" HelperText="@this.GetFieldDescription(fieldName, field)" Placeholder="@this.GetFieldPlaceholder(fieldName, field)" InputType="@(field.Secret ? InputType.Password : InputType.Text)" Disabled="@this.IsFieldDisabled(fieldName)" />
|
||||
}
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Text">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Save" Variant="Variant.Filled" Disabled="@(this.toolDefinition is null)">
|
||||
@T("Save")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@ -0,0 +1,84 @@
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs.Settings;
|
||||
|
||||
public partial class ToolSettingsDialog : SettingsDialogBase
|
||||
{
|
||||
[Parameter]
|
||||
public string ToolId { get; set; } = string.Empty;
|
||||
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ToolSettingsService ToolSettingsService { get; init; } = null!;
|
||||
|
||||
private ToolDefinition? toolDefinition;
|
||||
private IToolImplementation? implementation;
|
||||
private Dictionary<string, string> values = new(StringComparer.Ordinal);
|
||||
private string validationMessage = string.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
this.toolDefinition = this.ToolRegistry.GetDefinition(this.ToolId);
|
||||
if (this.toolDefinition is not null)
|
||||
{
|
||||
this.implementation = this.ToolRegistry.GetImplementation(this.toolDefinition.ImplementationKey);
|
||||
this.values = await this.ToolSettingsService.GetSettingsAsync(this.toolDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetValue(string fieldName) => this.values.GetValueOrDefault(fieldName, string.Empty);
|
||||
|
||||
private string GetFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) =>
|
||||
this.implementation?.GetSettingsFieldLabel(fieldName, fieldDefinition) ?? fieldDefinition.Title;
|
||||
|
||||
private string GetFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) =>
|
||||
this.GetFieldDescriptionWithDefault(fieldName, fieldDefinition);
|
||||
|
||||
private string GetFieldDefaultValue(string fieldName, ToolSettingsFieldDefinition fieldDefinition) =>
|
||||
this.implementation?.GetSettingsFieldDefaultValue(fieldName, fieldDefinition) ?? string.Empty;
|
||||
|
||||
private string GetFieldDescriptionWithDefault(string fieldName, ToolSettingsFieldDefinition fieldDefinition)
|
||||
{
|
||||
var description = this.implementation?.GetSettingsFieldDescription(fieldName, fieldDefinition) ?? fieldDefinition.Description;
|
||||
var defaultValue = this.GetFieldDefaultValue(fieldName, fieldDefinition);
|
||||
if (string.IsNullOrWhiteSpace(defaultValue))
|
||||
return description;
|
||||
|
||||
return string.Format(T("{0} Default: {1}"), description, defaultValue);
|
||||
}
|
||||
|
||||
private bool IsFieldDisabled(string fieldName) =>
|
||||
this.toolDefinition is not null && this.ToolSettingsService.IsFieldLocked(this.toolDefinition, fieldName);
|
||||
|
||||
private string GetFieldPlaceholder(string fieldName, ToolSettingsFieldDefinition fieldDefinition) =>
|
||||
string.IsNullOrWhiteSpace(this.GetValue(fieldName)) ? this.GetFieldDefaultValue(fieldName, fieldDefinition) : string.Empty;
|
||||
|
||||
private void UpdateValue(string fieldName, string? value)
|
||||
{
|
||||
this.values[fieldName] = value ?? string.Empty;
|
||||
this.validationMessage = string.Empty;
|
||||
}
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
if (this.toolDefinition is null)
|
||||
return;
|
||||
|
||||
var validationState = await this.ToolSettingsService.ValidateSettingsAsync(this.toolDefinition, this.values, this.implementation);
|
||||
if (!validationState.IsConfigured)
|
||||
{
|
||||
this.validationMessage = !string.IsNullOrWhiteSpace(validationState.Message)
|
||||
? validationState.Message
|
||||
: string.Format(T("Please configure the required settings: {0}"), string.Join(", ", validationState.MissingRequiredFields));
|
||||
return;
|
||||
}
|
||||
|
||||
await this.ToolSettingsService.SaveSettingsAsync(this.toolDefinition, this.values);
|
||||
this.MudDialog.Close();
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,7 @@
|
||||
{
|
||||
<SettingsPanelEmbeddings AvailableLLMProvidersFunc="@(() => this.availableLLMProviders)" @bind-AvailableEmbeddingProviders="@this.availableEmbeddingProviders"/>
|
||||
}
|
||||
|
||||
|
||||
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<SettingsPanelTranscription AvailableLLMProvidersFunc="@(() => this.availableLLMProviders)" @bind-AvailableTranscriptionProviders="@this.availableTranscriptionProviders"/>
|
||||
@ -23,6 +23,8 @@
|
||||
|
||||
<SettingsPanelApp AvailableLLMProvidersFunc="@(() => this.availableLLMProviders)"/>
|
||||
|
||||
<SettingsPanelTools />
|
||||
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<SettingsPanelAgentDataSourceSelection AvailableLLMProvidersFunc="@(() => this.availableLLMProviders)"/>
|
||||
|
||||
@ -356,6 +356,67 @@ CONFIG["SETTINGS"] = {}
|
||||
-- Examples are: "CmdOrControl+Shift+D", "Alt+F9", "F8"
|
||||
-- CONFIG["SETTINGS"]["DataApp.ShortcutVoiceRecording"] = "CmdOrControl+1"
|
||||
|
||||
-- Configure whether tools are available at all. The default is true.
|
||||
-- When tools are disabled globally, tool selection is hidden in chats and assistants,
|
||||
-- but the global tool settings remain available to administrators.
|
||||
-- CONFIG["SETTINGS"]["DataTools.EnableTools"] = false
|
||||
|
||||
-- Disable individual tools by their stable tool ID. The default is an empty set.
|
||||
-- Unknown IDs are safely ignored and can be deployed before a future tool is installed.
|
||||
-- CONFIG["SETTINGS"]["DataTools.DisabledToolIds"] = { "web_search" }
|
||||
|
||||
-- Configure the minimum provider confidence level required for individual tools.
|
||||
-- Tool IDs include: web_search, read_web_page
|
||||
-- Allowed values are: NONE, UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
|
||||
-- Defaults: web_search = MEDIUM, read_web_page = MEDIUM, but higher confidence is recommended
|
||||
-- CONFIG["SETTINGS"]["DataTools.MinimumProviderConfidenceByToolId"] = {
|
||||
-- ["web_search"] = "MEDIUM",
|
||||
-- ["read_web_page"] = "MEDIUM"
|
||||
-- }
|
||||
|
||||
-- Configure the Web Search tool. All values are strings.
|
||||
-- WebSearchBaseUrl: required SearXNG HTTP(S) root URL or /search endpoint; no default.
|
||||
-- CONFIG["SETTINGS"]["DataTools.WebSearchBaseUrl"] = "https://searxng.website/"
|
||||
-- WebSearchDefaultLanguage: optional language code; default is empty.
|
||||
-- CONFIG["SETTINGS"]["DataTools.WebSearchDefaultLanguage"] = "de"
|
||||
-- WebSearchDefaultSafeSearch: optional SearXNG safe-search level "0", "1", or "2"; default is empty.
|
||||
-- CONFIG["SETTINGS"]["DataTools.WebSearchDefaultSafeSearch"] = "1"
|
||||
-- WebSearchDefaultCategories: optional comma-separated categories; default is empty.
|
||||
-- WebSearchDefaultEngines: optional comma-separated engines; default is empty.
|
||||
-- Categories and engines cannot both be configured.
|
||||
-- CONFIG["SETTINGS"]["DataTools.WebSearchDefaultCategories"] = "general, science"
|
||||
-- CONFIG["SETTINGS"]["DataTools.WebSearchDefaultEngines"] = ""
|
||||
-- WebSearchMaxResults: positive integer; default 5, effective maximum 20.
|
||||
-- CONFIG["SETTINGS"]["DataTools.WebSearchMaxResults"] = "5"
|
||||
-- WebSearchTimeoutSeconds: positive integer; default 20, effective maximum 60.
|
||||
-- CONFIG["SETTINGS"]["DataTools.WebSearchTimeoutSeconds"] = "20"
|
||||
-- WebSearchMaxTotalContentCharacters: positive integer; default and maximum 100000.
|
||||
-- maximum number of content characters per web search
|
||||
-- CONFIG["SETTINGS"]["DataTools.WebSearchMaxTotalContentCharacters"] = "100000"
|
||||
-- WebSearchMinContentCharactersPerResult: positive integer; default and maximum 3000.
|
||||
-- The total content budget must be at least this value multiplied by the hard limit of 20 results.
|
||||
-- CONFIG["SETTINGS"]["DataTools.WebSearchMinContentCharactersPerResult"] = "3000"
|
||||
-- WebSearchPageTimeoutSeconds: positive integer; default and maximum 30.
|
||||
-- CONFIG["SETTINGS"]["DataTools.WebSearchPageTimeoutSeconds"] = "30"
|
||||
-- WebSearchRetrievalTimeoutSeconds: positive integer; default and maximum 90.
|
||||
-- CONFIG["SETTINGS"]["DataTools.WebSearchRetrievalTimeoutSeconds"] = "90"
|
||||
|
||||
-- Configure the Read Web Page tool. All values are strings.
|
||||
-- ReadWebPageTimeoutSeconds: positive integer; default 30, effective maximum 60.
|
||||
-- CONFIG["SETTINGS"]["DataTools.ReadWebPageTimeoutSeconds"] = "30"
|
||||
-- ReadWebPageMaxContentCharacters: positive integer; default 30000, effective maximum 50000.
|
||||
-- CONFIG["SETTINGS"]["DataTools.ReadWebPageMaxContentCharacters"] = "30000"
|
||||
-- ReadWebPageAllowedPrivateHosts: optional comma-separated private or VPN host patterns; default is empty.
|
||||
-- Public pages do not need to be listed. Wildcards only match subdomains, so add the root domain separately.
|
||||
-- Allowed private hosts require a provider with HIGH confidence. AI Studio tries the current user's
|
||||
-- operating-system sign-in when integrated authentication is requested, but does not reuse browser cookies.
|
||||
-- CONFIG["SETTINGS"]["DataTools.ReadWebPageAllowedPrivateHosts"] = "dlr.de, *.dlr.de"
|
||||
|
||||
-- The 14 Web Search and Read Web Page settings are locked by default. Add
|
||||
-- ".AllowUserOverride" = true to any of them to provide an editable organization default instead.
|
||||
-- A saved local value then takes precedence.
|
||||
-- CONFIG["SETTINGS"]["DataTools.WebSearchBaseUrl.AllowUserOverride"] = true
|
||||
|
||||
-- Configure the HTTP timeout for external requests, in seconds.
|
||||
-- The default is 3600 (1 hour).
|
||||
-- CONFIG["SETTINGS"]["DataApp.HttpClientTimeoutSeconds"] = 3600
|
||||
|
||||
@ -2295,21 +2295,42 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T601166687"] = "KI"
|
||||
-- Edit Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1183581066"] = "Nachricht bearbeiten"
|
||||
|
||||
-- Result
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347088452"] = "Ergebnis"
|
||||
|
||||
-- Do you really want to remove this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Möchten Sie diese Nachricht wirklich löschen?"
|
||||
|
||||
-- Yes, remove the AI response and edit it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Ja, entferne die KI-Antwort und bearbeite sie."
|
||||
|
||||
-- Failed
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1434043348"] = "Fehlgeschlagen"
|
||||
|
||||
-- Tool Calls ({0})
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1493057571"] = "Werkzeugaufrufe"
|
||||
|
||||
-- Executed
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1564757972"] = "Ausgeführt"
|
||||
|
||||
-- Yes, regenerate it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Ja, neu generieren"
|
||||
|
||||
-- No result
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1684269223"] = "Kein Ergebnis"
|
||||
|
||||
-- Yes, remove it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1820166585"] = "Ja, entferne es"
|
||||
|
||||
-- Number of sources
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1848978959"] = "Anzahl der Quellen"
|
||||
|
||||
-- Show {0} tool calls
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1981771421"] = "{0} Werkzeugaufrufe anzeigen"
|
||||
|
||||
-- Show tool call for {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2004842583"] = "Werkzeugaufruf für {0}"
|
||||
|
||||
-- Do you really want to edit this message? In order to edit this message, the AI response will be deleted.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2018431076"] = "Möchten Sie diese Nachricht wirklich bearbeiten? Um die Nachricht zu bearbeiten, wird die Antwort der KI gelöscht."
|
||||
|
||||
@ -2319,6 +2340,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Entfern
|
||||
-- Regenerate Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Nachricht neu erstellen"
|
||||
|
||||
-- Arguments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2738624831"] = "Argumente"
|
||||
|
||||
-- Number of attachments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Anzahl der Anhänge"
|
||||
|
||||
@ -2328,9 +2352,15 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Der Inh
|
||||
-- Edit
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3267849393"] = "Bearbeiten"
|
||||
|
||||
-- Unknown
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3424652889"] = "Unbekannt"
|
||||
|
||||
-- Regenerate
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3587744975"] = "Neu generieren"
|
||||
|
||||
-- Blocked
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3816336467"] = "Blockiert"
|
||||
|
||||
-- Do you really want to regenerate this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3878878761"] = "Möchten Sie diese Nachricht wirklich neu generieren?"
|
||||
|
||||
@ -2340,9 +2370,15 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Nachric
|
||||
-- No, keep it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "Nein, behalten"
|
||||
|
||||
-- No tool calls
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4224149521"] = "Verstanden."
|
||||
|
||||
-- Export Chat to Microsoft Word
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T861873672"] = "Chat in Microsoft Word exportieren"
|
||||
|
||||
-- No arguments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T931993614"] = "Keine Argumente"
|
||||
|
||||
-- The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T3267850764"] = "Das ausgewählte Modell '{0}' ist bei '{1}' (Anbieter={2}) nicht mehr verfügbar. Bitte passen Sie Ihre Anbietereinstellungen an."
|
||||
|
||||
@ -2658,15 +2694,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T252
|
||||
-- Select a minimum confidence level
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T2579793544"] = "Wählen Sie ein minimales Vertrauensniveau aus"
|
||||
|
||||
-- You have selected 1 preview feature.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMULTISELECT::T1384241824"] = "Sie haben 1 Vorschaufunktion ausgewählt."
|
||||
|
||||
-- No preview features selected.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMULTISELECT::T2809641588"] = "Keine Vorschaufunktionen ausgewählt."
|
||||
|
||||
-- You have selected {0} preview features.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMULTISELECT::T3513450626"] = "Sie haben {0} Vorschaufunktionen ausgewählt."
|
||||
|
||||
-- Preselected provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONPROVIDERSELECTION::T1469984996"] = "Vorausgewählter Anbieter"
|
||||
|
||||
@ -3552,6 +3579,39 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237
|
||||
-- Export configuration
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T975426229"] = "Konfiguration exportieren"
|
||||
|
||||
-- Settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T1258653480"] = "Einstellungen"
|
||||
|
||||
-- Description
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T1725856265"] = "Beschreibung"
|
||||
|
||||
-- Icon
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T1759955728"] = "Symbol"
|
||||
|
||||
-- This tool still needs to be configured.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T1958939818"] = "Dieses Werkzeug muss noch konfiguriert werden."
|
||||
|
||||
-- Missing required settings: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T2588115579"] = "Fehlende erforderliche Einstellungen: {0}"
|
||||
|
||||
-- Name
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T266367750"] = "Name"
|
||||
|
||||
-- No minimum confidence level chosen
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T2828607242"] = "Kein Mindestvertrauensniveau ausgewählt"
|
||||
|
||||
-- Minimum provider confidence
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3461070436"] = "Minimale Anbieterzuverlässigkeit"
|
||||
|
||||
-- Configure global settings for each tool.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3728248397"] = "Konfiguriere globale Einstellungen für jedes Werkzeug."
|
||||
|
||||
-- Tool Settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3730473128"] = "Werkzeugeinstellungen"
|
||||
|
||||
-- Status
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T6222351"] = "Status"
|
||||
|
||||
-- No transcription provider configured yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1079350363"] = "Es ist bisher kein Anbieter für Transkriptionen konfiguriert."
|
||||
|
||||
@ -3624,6 +3684,66 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::THIRDPARTYCOMPONENT::T1392042694"] = "Rep
|
||||
-- License:
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::THIRDPARTYCOMPONENT::T1908172666"] = "Lizenz:"
|
||||
|
||||
-- Tool selection is hidden
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T2096103917"] = "Werkzeugauswahl ist ausgeblendet"
|
||||
|
||||
-- You have selected 1 tool.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T2493128368"] = "Sie haben 1 Werkzeug ausgewählt."
|
||||
|
||||
-- Choose which tools should be preselected for new runs of this assistant.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T2696618758"] = "Wählen Sie aus, welche Werkzeuge für neue Ausführungen dieses Assistenten standardmäßig vorausgewählt sein sollen."
|
||||
|
||||
-- Default tools for this assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3253667950"] = "Standardwerkzeuge für diesen Assistenten"
|
||||
|
||||
-- Tool selection is visible
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3384582069"] = "Die Werkzeugauswahl ist sichtbar"
|
||||
|
||||
-- Show tool selection in this assistant?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3494508870"] = "Werkzeugauswahl in diesem Assistenten anzeigen?"
|
||||
|
||||
-- You have selected {0} tools.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3729156356"] = "Sie haben {0} Werkzeuge ausgewählt."
|
||||
|
||||
-- No tools selected.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3934845540"] = "Keine Werkzeuge ausgewählt."
|
||||
|
||||
-- Default tools for chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T907403808"] = "Standardwerkzeuge für den Chat"
|
||||
|
||||
-- Choose which tools should be preselected for new chats.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T948842182"] = "Wählen Sie aus, welche Werkzeuge für neue Chats vorausgewählt sein sollen."
|
||||
|
||||
-- This tool is currently required because Web Search is enabled.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T1351725609"] = "Dieses Werkzeug ist derzeit erforderlich, da die Websuche aktiviert ist."
|
||||
|
||||
-- Tool changes are locked while a response is running. Your current selection is shown below and applies again from the next message once the run is finished.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T1688023907"] = "Werkzeugänderungen sind gesperrt, während eine Antwort ausgeführt wird. Ihre aktuelle Auswahl wird unten angezeigt und gilt nach Abschluss der Ausführung ab der nächsten Nachricht wieder."
|
||||
|
||||
-- Tools allow the LLM to perform targeted additional actions such as web searches or reading web pages.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T1944689297"] = "Werkzeuge ermöglichen es dem LLM, gezielte zusätzliche Aktionen auszuführen, wie z. B. Websuchen oder das Lesen von Webseiten."
|
||||
|
||||
-- Enabling this tool also enables Read Web Page.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3023833839"] = "Das Aktivieren dieses Werkzeugs aktiviert auch „Webseite lesen“."
|
||||
|
||||
-- Required settings are missing. Configure this tool before enabling it.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3119156561"] = "Erforderliche Einstellungen fehlen. Konfigurieren Sie dieses Werkzeug, bevor Sie es aktivieren."
|
||||
|
||||
-- Close
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3448155331"] = "Schließen"
|
||||
|
||||
-- No tools are available in this context.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3904490680"] = "Keine Werkzeuge sind in diesem Kontext verfügbar."
|
||||
|
||||
-- This tool requires provider confidence {0}. The selected provider has {1}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T4097602620"] = "Dieses Werkzeug erfordert Anbieter-Vertrauen {0}. Der ausgewählte Anbieter hat {1}."
|
||||
|
||||
-- Tool Selection
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T749664565"] = "Werkzeugauswahl"
|
||||
|
||||
-- Select tools
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T998515990"] = "Werkzeuge auswählen"
|
||||
|
||||
-- You'll interact with the AI systems using your voice. To achieve this, we want to integrate voice input (speech-to-text) and output (text-to-speech). However, later on, it should also have a natural conversation flow, i.e., seamless conversation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1015366320"] = "Sie werden mit den KI-Systemen über ihre Stimme interagieren. Dafür möchten wir Spracheingabe (Sprache-zu-Text) und Sprachausgabe (Text-zu-Sprache) integrieren. Später soll außerdem ein natürlicher Gesprächsfluss möglich sein, also eine nahtlose Unterhaltung."
|
||||
|
||||
@ -6411,6 +6531,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3547
|
||||
-- Preselect e-mail options?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3832719342"] = "E-Mail-Optionen vorauswählen?"
|
||||
|
||||
-- Save
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T1294818664"] = "Speichern"
|
||||
|
||||
-- Tool Settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3730473128"] = "Werkzeugeinstellungen"
|
||||
|
||||
-- The selected tool could not be loaded.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3907843187"] = "Das ausgewählte Werkzeug konnte nicht geladen werden."
|
||||
|
||||
-- {0} Default: {1}
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T403490413"] = "{0} Standard: {1}"
|
||||
|
||||
-- Cancel
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T900713019"] = "Abbrechen"
|
||||
|
||||
-- Save
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1294818664"] = "Speichern"
|
||||
|
||||
@ -7491,6 +7626,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3014737766"] = "Wir haben ve
|
||||
-- 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}'.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3049689432"] = "Wir haben versucht, mit dem LLM-Anbieter „{0}“ (Typ={1}) zu kommunizieren. Selbst nach {2} erneuten Versuchen gab es weiterhin Probleme mit der Anfrage. Die Meldung des Anbieters lautet: „{3}“."
|
||||
|
||||
-- The tool calling request failed with status code {0}. See the logs for details.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3117779001"] = "Die Werkzeuganfrage ist mit dem Statuscode {0} fehlgeschlagen. Details finden Sie in den Logs."
|
||||
|
||||
-- Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3573577433"] = "Es wurde versucht, mit dem LLM-Anbieter '{0}' zu kommunizieren. Dabei sind Probleme bei der Anfrage aufgetreten. Die Meldung des Anbieters lautet: '{1}'"
|
||||
|
||||
@ -7528,7 +7666,7 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T991875725"] = "Der Anbieter be
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCELEVELEXTENSIONS::T163471254"] = "Mittel"
|
||||
|
||||
-- Moderate
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCELEVELEXTENSIONS::T177463328"] = "Mäßig"
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCELEVELEXTENSIONS::T177463328"] = "Mittel"
|
||||
|
||||
-- Unknown confidence level
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCELEVELEXTENSIONS::T1811522309"] = "Unbekanntes Vertrauensniveau"
|
||||
@ -7581,6 +7719,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T37333904
|
||||
-- We could not load models from '{0}' due to an unknown error.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T3907712809"] = "Wir konnten die Modelle aus '{0}' aufgrund eines unbekannten Fehlers nicht laden."
|
||||
|
||||
-- The tool calling request failed with status code {0}. See the logs for details.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T3117779001"] = "Die Anfrage zum Aufruf des Werkzeugs ist mit dem Statuscode {0} fehlgeschlagen. Details findest du in den Protokollen."
|
||||
|
||||
-- It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "Anscheinend haben Sie bei OpenAI kein API-Guthaben mehr. Bitte fügen Sie Ihrem Konto Guthaben hinzu und versuchen Sie es erneut."
|
||||
|
||||
@ -9060,6 +9201,108 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Von den Dat
|
||||
-- Sources provided by the AI
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Von der KI bereitgestellte Quellen"
|
||||
|
||||
-- Tool
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T3517012711"] = "Werkzeug"
|
||||
|
||||
-- Tool description
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T4056470505"] = "Werkzeugbeschreibung"
|
||||
|
||||
-- Load a single web page and extract its main HTML content.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T204256540"] = "Eine einzelne Webseite laden und deren Haupt-HTML-Inhalt extrahieren."
|
||||
|
||||
-- Allowed private hosts must be host names only, without scheme or path.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2196457612"] = "Zulässige private Hosts dürfen nur Hostnamen enthalten, ohne Schema oder Pfad."
|
||||
|
||||
-- Maximum Content Characters
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2801581200"] = "Maximale Inhaltszeichen"
|
||||
|
||||
-- Optional HTTP timeout for loading a web page in seconds.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2941521561"] = "Optionales HTTP-Zeitlimit zum Laden einer Webseite in Sekunden."
|
||||
|
||||
-- Allowed private host '{0}' is not valid.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3089707139"] = "Der zulässige private Host „{0}“ ist ungültig."
|
||||
|
||||
-- Allowed Private Hosts
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3415515539"] = "Zulässige private Hosts"
|
||||
|
||||
-- Timeout Seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3567699845"] = "Zeitlimit in Sekunden"
|
||||
|
||||
-- Read Web Page
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3612587998"] = "Webseite lesen"
|
||||
|
||||
-- Optional global truncation limit for extracted characters returned to the model.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T364016543"] = "Optionale globale Begrenzung für extrahierte Zeichen, die an das Modell zurückgegeben werden."
|
||||
|
||||
-- The web page was not loaded because private or VPN web pages require a High-confidence provider.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3856267430"] = "Die Webseite wurde nicht geladen, da private Webseiten oder Webseiten über ein VPN einen Anbieter mit hoher Vertrauensstufe erfordern."
|
||||
|
||||
-- Optional host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T854695329"] = "Optionale Host-Allowlist für private oder VPN-Webseiten. Aus Sicherheitsgründen ist der Zugriff auf private oder VPN-Webseiten standardmäßig nicht erlaubt. Trennen Sie Host-Muster durch Kommas, z. B. example.de, *.example.de. Für erlaubte private Hosts ist ein Anbieter mit hohem Vertrauensniveau erforderlich. Bei erlaubten internen Hosts versucht AI Studio automatisch die Standardanmeldung des Betriebssystems, wenn der Server mit integrierter Authentifizierung antwortet."
|
||||
|
||||
-- Maximum Results
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1273024715"] = "Maximale Anzahl an Ergebnissen"
|
||||
|
||||
-- Optional comma-separated default categories. Do not set this together with default engines.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1342681591"] = "Optionale, durch Kommas getrennte Standardkategorien. Nicht zusammen mit Standard-Engines festlegen."
|
||||
|
||||
-- Default Safe Search
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1343180281"] = "Standard-SafeSearch"
|
||||
|
||||
-- Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1739312423"] = "Basis-URL der SearXNG-Instanz. Sie können entweder die Stamm-URL der Instanz oder den Endpunkt /search eingeben."
|
||||
|
||||
-- A SearXNG URL is required.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1746583720"] = "Eine SearXNG-URL ist erforderlich."
|
||||
|
||||
-- Default Engines
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1865580137"] = "Standard-Engines"
|
||||
|
||||
-- Optional fallback language code when the model does not provide a language.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1868101906"] = "Optionaler Fallback-Sprachcode, wenn das Modell keine Sprache angibt."
|
||||
|
||||
-- Default Categories
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T2053347010"] = "Standardkategorien"
|
||||
|
||||
-- Default Language
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T2526826120"] = "Standardsprache"
|
||||
|
||||
-- The configured SearXNG URL is not a valid absolute URL.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3038368943"] = "Die konfigurierte SearXNG-URL ist keine gültige absolute URL."
|
||||
|
||||
-- Optional HTTP timeout for the search request in seconds.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3078115445"] = "Optionales HTTP-Timeout für die Suchanfrage in Sekunden."
|
||||
|
||||
-- Timeout Seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3567699845"] = "Zeitüberschreitung in Sekunden"
|
||||
|
||||
-- Optional default maximum number of results returned to the model when the model does not provide a limit.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3603838271"] = "Optionale Standardhöchstzahl der an das Modell zurückgegebenen Ergebnisse, wenn das Modell kein Limit angibt."
|
||||
|
||||
-- Web Search
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3815068443"] = "Websuche"
|
||||
|
||||
-- Optional safe search policy sent to SearXNG when configured.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3967748757"] = "Optionale SafeSearch-Richtlinie, die bei entsprechender Konfiguration an SearXNG gesendet wird."
|
||||
|
||||
-- Default categories and default engines cannot both be set for the web search tool.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T4009446158"] = "Standardkategorien und Standard-Engines können für die Websuche nicht gleichzeitig festgelegt werden."
|
||||
|
||||
-- Optional comma-separated default engines. Do not set this together with default categories.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T4108908537"] = "Optionale, durch Kommas getrennte Standard-Engines. Nicht zusammen mit Standardkategorien festlegen."
|
||||
|
||||
-- The setting '{0}' must be a positive integer.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T4199432074"] = "Die Einstellung „{0}“ muss eine positive ganze Zahl sein."
|
||||
|
||||
-- Search the web with a configured SearXNG instance and return candidate URLs for the model. Use Read Web Page on relevant result URLs before answering factual or detailed web questions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T764865565"] = "Im Internet mit einer konfigurierten SearXNG-Instanz suchen und Kandidaten-URLs für das Modell zurückgeben. Verwende „Webseite lesen“ auf relevanten Ergebnis-URLs, bevor du faktische oder detaillierte Webfragen beantwortest."
|
||||
|
||||
-- The configured SearXNG URL must start with http:// or https://.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T944878454"] = "Die konfigurierte SearXNG-URL muss mit http:// oder https:// beginnen."
|
||||
|
||||
-- SearXNG URL
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T993547568"] = "SearXNG-URL"
|
||||
|
||||
-- Pandoc Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::USERFILE::T185447014"] = "Pandoc-Installation"
|
||||
|
||||
|
||||
@ -2295,21 +2295,42 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T601166687"] = "AI"
|
||||
-- Edit Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1183581066"] = "Edit Message"
|
||||
|
||||
-- Result
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347088452"] = "Result"
|
||||
|
||||
-- Do you really want to remove this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Do you really want to remove this message?"
|
||||
|
||||
-- Yes, remove the AI response and edit it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Yes, remove the AI response and edit it"
|
||||
|
||||
-- Failed
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1434043348"] = "Failed"
|
||||
|
||||
-- Tool Calls ({0})
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1493057571"] = "Tool Calls ({0})"
|
||||
|
||||
-- Executed
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1564757972"] = "Executed"
|
||||
|
||||
-- Yes, regenerate it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Yes, regenerate it"
|
||||
|
||||
-- No result
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1684269223"] = "No result"
|
||||
|
||||
-- Yes, remove it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1820166585"] = "Yes, remove it"
|
||||
|
||||
-- Number of sources
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1848978959"] = "Number of sources"
|
||||
|
||||
-- Show {0} tool calls
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1981771421"] = "Show {0} tool calls"
|
||||
|
||||
-- Show tool call for {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2004842583"] = "Show tool call for {0}"
|
||||
|
||||
-- Do you really want to edit this message? In order to edit this message, the AI response will be deleted.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2018431076"] = "Do you really want to edit this message? In order to edit this message, the AI response will be deleted."
|
||||
|
||||
@ -2319,6 +2340,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Removes
|
||||
-- Regenerate Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Regenerate Message"
|
||||
|
||||
-- Arguments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2738624831"] = "Arguments"
|
||||
|
||||
-- Number of attachments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Number of attachments"
|
||||
|
||||
@ -2328,9 +2352,15 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Cannot
|
||||
-- Edit
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3267849393"] = "Edit"
|
||||
|
||||
-- Unknown
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3424652889"] = "Unknown"
|
||||
|
||||
-- Regenerate
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3587744975"] = "Regenerate"
|
||||
|
||||
-- Blocked
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3816336467"] = "Blocked"
|
||||
|
||||
-- Do you really want to regenerate this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3878878761"] = "Do you really want to regenerate this message?"
|
||||
|
||||
@ -2340,9 +2370,15 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove
|
||||
-- No, keep it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "No, keep it"
|
||||
|
||||
-- No tool calls
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4224149521"] = "No tool calls"
|
||||
|
||||
-- Export Chat to Microsoft Word
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T861873672"] = "Export Chat to Microsoft Word"
|
||||
|
||||
-- No arguments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T931993614"] = "No arguments"
|
||||
|
||||
-- The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T3267850764"] = "The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings."
|
||||
|
||||
@ -2658,15 +2694,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T252
|
||||
-- Select a minimum confidence level
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T2579793544"] = "Select a minimum confidence level"
|
||||
|
||||
-- You have selected 1 preview feature.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMULTISELECT::T1384241824"] = "You have selected 1 preview feature."
|
||||
|
||||
-- No preview features selected.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMULTISELECT::T2809641588"] = "No preview features selected."
|
||||
|
||||
-- You have selected {0} preview features.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMULTISELECT::T3513450626"] = "You have selected {0} preview features."
|
||||
|
||||
-- Preselected provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONPROVIDERSELECTION::T1469984996"] = "Preselected provider"
|
||||
|
||||
@ -3552,6 +3579,39 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237
|
||||
-- Export configuration
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T975426229"] = "Export configuration"
|
||||
|
||||
-- Settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T1258653480"] = "Settings"
|
||||
|
||||
-- Description
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T1725856265"] = "Description"
|
||||
|
||||
-- Icon
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T1759955728"] = "Icon"
|
||||
|
||||
-- This tool still needs to be configured.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T1958939818"] = "This tool still needs to be configured."
|
||||
|
||||
-- Missing required settings: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T2588115579"] = "Missing required settings: {0}"
|
||||
|
||||
-- Name
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T266367750"] = "Name"
|
||||
|
||||
-- No minimum confidence level chosen
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T2828607242"] = "No minimum confidence level chosen"
|
||||
|
||||
-- Minimum provider confidence
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3461070436"] = "Minimum provider confidence"
|
||||
|
||||
-- Configure global settings for each tool.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3728248397"] = "Configure global settings for each tool."
|
||||
|
||||
-- Tool Settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3730473128"] = "Tool Settings"
|
||||
|
||||
-- Status
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T6222351"] = "Status"
|
||||
|
||||
-- No transcription provider configured yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1079350363"] = "No transcription provider configured yet."
|
||||
|
||||
@ -3624,6 +3684,66 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::THIRDPARTYCOMPONENT::T1392042694"] = "Ope
|
||||
-- License:
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::THIRDPARTYCOMPONENT::T1908172666"] = "License:"
|
||||
|
||||
-- Tool selection is hidden
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T2096103917"] = "Tool selection is hidden"
|
||||
|
||||
-- You have selected 1 tool.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T2493128368"] = "You have selected 1 tool."
|
||||
|
||||
-- Choose which tools should be preselected for new runs of this assistant.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T2696618758"] = "Choose which tools should be preselected for new runs of this assistant."
|
||||
|
||||
-- Default tools for this assistant
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3253667950"] = "Default tools for this assistant"
|
||||
|
||||
-- Tool selection is visible
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3384582069"] = "Tool selection is visible"
|
||||
|
||||
-- Show tool selection in this assistant?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3494508870"] = "Show tool selection in this assistant?"
|
||||
|
||||
-- You have selected {0} tools.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3729156356"] = "You have selected {0} tools."
|
||||
|
||||
-- No tools selected.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T3934845540"] = "No tools selected."
|
||||
|
||||
-- Default tools for chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T907403808"] = "Default tools for chat"
|
||||
|
||||
-- Choose which tools should be preselected for new chats.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T948842182"] = "Choose which tools should be preselected for new chats."
|
||||
|
||||
-- This tool is currently required because Web Search is enabled.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T1351725609"] = "This tool is currently required because Web Search is enabled."
|
||||
|
||||
-- Tool changes are locked while a response is running. Your current selection is shown below and applies again from the next message once the run is finished.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T1688023907"] = "Tool changes are locked while a response is running. Your current selection is shown below and applies again from the next message once the run is finished."
|
||||
|
||||
-- Tools allow the LLM to perform targeted additional actions such as web searches or reading web pages.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T1944689297"] = "Tools allow the LLM to perform targeted additional actions such as web searches or reading web pages."
|
||||
|
||||
-- Enabling this tool also enables Read Web Page.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3023833839"] = "Enabling this tool also enables Read Web Page."
|
||||
|
||||
-- Required settings are missing. Configure this tool before enabling it.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3119156561"] = "Required settings are missing. Configure this tool before enabling it."
|
||||
|
||||
-- Close
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3448155331"] = "Close"
|
||||
|
||||
-- No tools are available in this context.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3904490680"] = "No tools are available in this context."
|
||||
|
||||
-- This tool requires provider confidence {0}. The selected provider has {1}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T4097602620"] = "This tool requires provider confidence {0}. The selected provider has {1}."
|
||||
|
||||
-- Tool Selection
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T749664565"] = "Tool Selection"
|
||||
|
||||
-- Select tools
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T998515990"] = "Select tools"
|
||||
|
||||
-- You'll interact with the AI systems using your voice. To achieve this, we want to integrate voice input (speech-to-text) and output (text-to-speech). However, later on, it should also have a natural conversation flow, i.e., seamless conversation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T1015366320"] = "You'll interact with the AI systems using your voice. To achieve this, we want to integrate voice input (speech-to-text) and output (text-to-speech). However, later on, it should also have a natural conversation flow, i.e., seamless conversation."
|
||||
|
||||
@ -6411,6 +6531,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3547
|
||||
-- Preselect e-mail options?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3832719342"] = "Preselect e-mail options?"
|
||||
|
||||
-- Save
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T1294818664"] = "Save"
|
||||
|
||||
-- Tool Settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3730473128"] = "Tool Settings"
|
||||
|
||||
-- The selected tool could not be loaded.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3907843187"] = "The selected tool could not be loaded."
|
||||
|
||||
-- {0} Default: {1}
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T403490413"] = "{0} Default: {1}"
|
||||
|
||||
-- Cancel
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T900713019"] = "Cancel"
|
||||
|
||||
-- Save
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1294818664"] = "Save"
|
||||
|
||||
@ -7491,6 +7626,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3014737766"] = "We tried to
|
||||
-- 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}'.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3049689432"] = "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}'."
|
||||
|
||||
-- The tool calling request failed with status code {0}. See the logs for details.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3117779001"] = "The tool calling request failed with status code {0}. See the logs for details."
|
||||
|
||||
-- Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3573577433"] = "Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'"
|
||||
|
||||
@ -7581,6 +7719,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T37333904
|
||||
-- We could not load models from '{0}' due to an unknown error.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T3907712809"] = "We could not load models from '{0}' due to an unknown error."
|
||||
|
||||
-- The tool calling request failed with status code {0}. See the logs for details.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T3117779001"] = "The tool calling request failed with status code {0}. See the logs for details."
|
||||
|
||||
-- It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again."
|
||||
|
||||
@ -9060,6 +9201,108 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Sources pro
|
||||
-- Sources provided by the AI
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Sources provided by the AI"
|
||||
|
||||
-- Tool
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T3517012711"] = "Tool"
|
||||
|
||||
-- Tool description
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T4056470505"] = "Tool description"
|
||||
|
||||
-- Load a single web page and extract its main HTML content.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T204256540"] = "Load a single web page and extract its main HTML content."
|
||||
|
||||
-- Allowed private hosts must be host names only, without scheme or path.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2196457612"] = "Allowed private hosts must be host names only, without scheme or path."
|
||||
|
||||
-- Maximum Content Characters
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2801581200"] = "Maximum Content Characters"
|
||||
|
||||
-- Optional HTTP timeout for loading a web page in seconds.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2941521561"] = "Optional HTTP timeout for loading a web page in seconds."
|
||||
|
||||
-- Allowed private host '{0}' is not valid.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3089707139"] = "Allowed private host '{0}' is not valid."
|
||||
|
||||
-- Allowed Private Hosts
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3415515539"] = "Allowed Private Hosts"
|
||||
|
||||
-- Timeout Seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3567699845"] = "Timeout Seconds"
|
||||
|
||||
-- Read Web Page
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3612587998"] = "Read Web Page"
|
||||
|
||||
-- Optional global truncation limit for extracted characters returned to the model.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T364016543"] = "Optional global truncation limit for extracted characters returned to the model."
|
||||
|
||||
-- The web page was not loaded because private or VPN web pages require a High-confidence provider.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3856267430"] = "The web page was not loaded because private or VPN web pages require a High-confidence provider."
|
||||
|
||||
-- Optional host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T854695329"] = "Optional host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication."
|
||||
|
||||
-- Maximum Results
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1273024715"] = "Maximum Results"
|
||||
|
||||
-- Optional comma-separated default categories. Do not set this together with default engines.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1342681591"] = "Optional comma-separated default categories. Do not set this together with default engines."
|
||||
|
||||
-- Default Safe Search
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1343180281"] = "Default Safe Search"
|
||||
|
||||
-- Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1739312423"] = "Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint."
|
||||
|
||||
-- A SearXNG URL is required.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1746583720"] = "A SearXNG URL is required."
|
||||
|
||||
-- Default Engines
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1865580137"] = "Default Engines"
|
||||
|
||||
-- Optional fallback language code when the model does not provide a language.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1868101906"] = "Optional fallback language code when the model does not provide a language."
|
||||
|
||||
-- Default Categories
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T2053347010"] = "Default Categories"
|
||||
|
||||
-- Default Language
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T2526826120"] = "Default Language"
|
||||
|
||||
-- The configured SearXNG URL is not a valid absolute URL.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3038368943"] = "The configured SearXNG URL is not a valid absolute URL."
|
||||
|
||||
-- Optional HTTP timeout for the search request in seconds.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3078115445"] = "Optional HTTP timeout for the search request in seconds."
|
||||
|
||||
-- Timeout Seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3567699845"] = "Timeout Seconds"
|
||||
|
||||
-- Optional default maximum number of results returned to the model when the model does not provide a limit.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3603838271"] = "Optional default maximum number of results returned to the model when the model does not provide a limit."
|
||||
|
||||
-- Web Search
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3815068443"] = "Web Search"
|
||||
|
||||
-- Optional safe search policy sent to SearXNG when configured.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3967748757"] = "Optional safe search policy sent to SearXNG when configured."
|
||||
|
||||
-- Default categories and default engines cannot both be set for the web search tool.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T4009446158"] = "Default categories and default engines cannot both be set for the web search tool."
|
||||
|
||||
-- Optional comma-separated default engines. Do not set this together with default categories.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T4108908537"] = "Optional comma-separated default engines. Do not set this together with default categories."
|
||||
|
||||
-- The setting '{0}' must be a positive integer.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T4199432074"] = "The setting '{0}' must be a positive integer."
|
||||
|
||||
-- Search the web with a configured SearXNG instance and return candidate URLs for the model. Use Read Web Page on relevant result URLs before answering factual or detailed web questions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T764865565"] = "Search the web with a configured SearXNG instance and return candidate URLs for the model. Use Read Web Page on relevant result URLs before answering factual or detailed web questions."
|
||||
|
||||
-- The configured SearXNG URL must start with http:// or https://.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T944878454"] = "The configured SearXNG URL must start with http:// or https://."
|
||||
|
||||
-- SearXNG URL
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T993547568"] = "SearXNG URL"
|
||||
|
||||
-- Pandoc Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::USERFILE::T185447014"] = "Pandoc Installation"
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using AIStudio.Agents;
|
||||
using AIStudio.Agents.AssistantAudit;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
using AIStudio.Tools.Databases;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
using AIStudio.Tools.AssistantSessions;
|
||||
@ -8,6 +9,8 @@ using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
using AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations;
|
||||
using AIStudio.Tools.Web;
|
||||
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
@ -159,6 +162,12 @@ internal sealed class Program
|
||||
builder.Services.AddSingleton(typeof(RuntimeInfoResponse), runtimeInfo);
|
||||
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
|
||||
builder.Services.AddSingleton<SettingsManager>();
|
||||
builder.Services.AddSingleton<ToolSettingsService>();
|
||||
builder.Services.AddSingleton<WebPageRetrievalService>();
|
||||
builder.Services.AddSingleton<IToolImplementation, ReadWebPageTool>();
|
||||
builder.Services.AddSingleton<IToolImplementation, SearXNGWebSearchTool>();
|
||||
builder.Services.AddSingleton<ToolRegistry>();
|
||||
builder.Services.AddSingleton<ToolExecutor>();
|
||||
builder.Services.AddSingleton<ThreadSafeRandom>();
|
||||
builder.Services.AddSingleton<AIJobService>();
|
||||
builder.Services.AddSingleton<AssistantSessionService>();
|
||||
|
||||
@ -29,7 +29,7 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
@ -44,6 +44,8 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
@ -153,4 +155,4 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
|
||||
apiKeyProvisional);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,4 +21,4 @@ public readonly record struct ChatRequest(
|
||||
// Attention: The "required" modifier is not supported for [JsonExtensionData].
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ namespace AIStudio.Provider.Anthropic;
|
||||
public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, new Uri("https://api.anthropic.com/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
|
||||
{
|
||||
private static readonly ILogger<ProviderAnthropic> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderAnthropic>();
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -70,7 +69,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Prepare the Anthropic HTTP chat request:
|
||||
var chatRequest = JsonSerializer.Serialize(new ChatRequest
|
||||
{
|
||||
@ -164,9 +163,8 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
|
||||
{
|
||||
return Task.FromResult(ModelLoadResult.FromModels([]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
private Task<ModelLoadResult> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
return this.LoadModelsResponse<ModelsResponse>(
|
||||
@ -189,4 +187,4 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
|
||||
},
|
||||
jsonSerializerOptions: JSON_SERIALIZER_OPTIONS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,10 +10,13 @@ using AIStudio.Provider.Anthropic;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Provider.SelfHosted;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
using AIStudio.Tools.MIME;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Host = AIStudio.Provider.SelfHosted.Host;
|
||||
|
||||
namespace AIStudio.Provider;
|
||||
@ -962,13 +965,14 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
Model chatModel,
|
||||
ChatThread chatThread,
|
||||
SettingsManager settingsManager,
|
||||
Func<TextMessage, IDictionary<string, object>, Task<TRequest>> requestFactory,
|
||||
Func<TextMessage, IDictionary<string, object>, IList<object>?, Task<TRequest>> requestFactory,
|
||||
SecretStoreType storeType = SecretStoreType.LLM_PROVIDER,
|
||||
bool isTryingSecret = false,
|
||||
string systemPromptRole = "system",
|
||||
string requestPath = "chat/completions",
|
||||
Action<HttpRequestHeaders>? headersAction = null,
|
||||
[EnumeratorCancellation] CancellationToken token = default)
|
||||
where TRequest : ChatCompletionAPIRequest
|
||||
where TDelta : IResponseStreamLine
|
||||
where TAnnotation : IAnnotationStreamLine
|
||||
{
|
||||
@ -977,18 +981,171 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
if(!requestedSecret.Success && !isTryingSecret)
|
||||
yield break;
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = systemPromptRole,
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters();
|
||||
|
||||
var toolRegistry = Program.SERVICE_PROVIDER.GetService<ToolRegistry>();
|
||||
var toolExecutor = Program.SERVICE_PROVIDER.GetService<ToolExecutor>();
|
||||
var currentAssistantContent = chatThread.Blocks.LastOrDefault(x => x.Role is ChatRole.AI)?.Content as ContentText;
|
||||
currentAssistantContent?.ToolInvocations.Clear();
|
||||
|
||||
async Task ResetToolRuntimeStatusAsync()
|
||||
{
|
||||
if (currentAssistantContent is null)
|
||||
return;
|
||||
|
||||
currentAssistantContent.ToolRuntimeStatus = new();
|
||||
await currentAssistantContent.StreamingEvent();
|
||||
}
|
||||
|
||||
async Task ShowToolRuntimeStatusAsync(IEnumerable<string> toolNames)
|
||||
{
|
||||
if (currentAssistantContent is null)
|
||||
return;
|
||||
|
||||
currentAssistantContent.ToolRuntimeStatus = new ToolRuntimeStatus
|
||||
{
|
||||
IsRunning = true,
|
||||
ToolNames = toolNames.ToList(),
|
||||
};
|
||||
await currentAssistantContent.StreamingEvent();
|
||||
}
|
||||
|
||||
TextMessage systemPrompt;
|
||||
if (toolRegistry is not null && toolExecutor is not null)
|
||||
{
|
||||
var runnableTools = await toolRegistry.GetRunnableToolsAsync(
|
||||
this.CreateSettingsProvider(chatModel),
|
||||
chatThread.RuntimeComponent,
|
||||
chatThread.RuntimeSelectedToolIds,
|
||||
this.Provider.GetModelCapabilities(chatModel),
|
||||
this.Provider.GetConfidence(settingsManager).Level,
|
||||
settingsManager.IsToolSelectionVisible(chatThread.RuntimeComponent));
|
||||
|
||||
systemPrompt = new TextMessage
|
||||
{
|
||||
Role = systemPromptRole,
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager, runnableTools.Select(x => x.Definition)),
|
||||
};
|
||||
|
||||
if (runnableTools.Count > 0)
|
||||
{
|
||||
var providerTools = runnableTools.Select(x => ProviderToolAdapters.ToChatCompletionTool(x.Definition)).ToList();
|
||||
|
||||
var internalMessages = new List<IMessageBase>();
|
||||
var toolCallCount = 0;
|
||||
while (true)
|
||||
{
|
||||
var finalResponseRequired = toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS;
|
||||
var requestSystemPrompt = finalResponseRequired
|
||||
? systemPrompt with
|
||||
{
|
||||
Content = $"{systemPrompt.Content}{Environment.NewLine}{Environment.NewLine}{ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction()}",
|
||||
}
|
||||
: systemPrompt;
|
||||
ChatCompletionAPIRequest requestDtoBase = await requestFactory(
|
||||
requestSystemPrompt,
|
||||
apiParameters,
|
||||
finalResponseRequired ? null : providerTools);
|
||||
var requestDto = requestDtoBase with
|
||||
{
|
||||
Messages = [..requestDtoBase.Messages, ..internalMessages],
|
||||
Stream = false,
|
||||
};
|
||||
var response = await this.ExecuteChatCompletionRequest(requestDto, requestPath, requestedSecret, headersAction, token);
|
||||
var responseMessage = response?.Choices.FirstOrDefault()?.Message;
|
||||
if (responseMessage is null)
|
||||
{
|
||||
await ResetToolRuntimeStatusAsync();
|
||||
yield break;
|
||||
}
|
||||
|
||||
var toolCalls = this.CanonicalizeToolCallNames(responseMessage.ToolCalls ?? [], runnableTools);
|
||||
if (toolCalls.Count == 0)
|
||||
{
|
||||
await ResetToolRuntimeStatusAsync();
|
||||
if (!string.IsNullOrWhiteSpace(responseMessage.Content))
|
||||
yield return new ContentStreamChunk(responseMessage.Content, []);
|
||||
else if (toolCallCount > 0)
|
||||
yield return new ContentStreamChunk("The model completed the tool call but did not return a final answer.", []);
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (finalResponseRequired)
|
||||
{
|
||||
await ResetToolRuntimeStatusAsync();
|
||||
if (!string.IsNullOrWhiteSpace(responseMessage.Content))
|
||||
yield return new ContentStreamChunk(responseMessage.Content, []);
|
||||
else
|
||||
yield return new ContentStreamChunk("The model did not return a final answer after completing the available tool calls.", []);
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await ShowToolRuntimeStatusAsync(toolCalls
|
||||
.Select(x => runnableTools.FirstOrDefault(tool => tool.Definition.Function.Name.Equals(x.Function.Name, StringComparison.Ordinal)).Implementation?.GetDisplayName() ?? x.Function.Name));
|
||||
|
||||
internalMessages.Add(new AssistantToolCallMessage
|
||||
{
|
||||
Content = responseMessage.Content,
|
||||
ToolCalls = toolCalls,
|
||||
});
|
||||
|
||||
foreach (var toolCall in toolCalls)
|
||||
{
|
||||
if (toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS)
|
||||
{
|
||||
var finalResponseInstruction = ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction();
|
||||
internalMessages.Add(new ToolResultMessage
|
||||
{
|
||||
Content = finalResponseInstruction,
|
||||
ToolCallId = toolCall.Id,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
toolCallCount++;
|
||||
var (toolContent, trace, requiredProviderConfidence) = await toolExecutor.ExecuteAsync(
|
||||
toolCall.Id,
|
||||
toolCall.Function.Name,
|
||||
toolCall.Function.Arguments,
|
||||
runnableTools,
|
||||
this.Provider.GetConfidence(settingsManager).Level,
|
||||
toolCallCount,
|
||||
token);
|
||||
|
||||
chatThread.RequireProviderConfidence(requiredProviderConfidence);
|
||||
currentAssistantContent?.ToolInvocations.Add(trace);
|
||||
internalMessages.Add(new ToolResultMessage
|
||||
{
|
||||
Content = toolContent,
|
||||
ToolCallId = toolCall.Id,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ResetToolRuntimeStatusAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
systemPrompt = new TextMessage
|
||||
{
|
||||
Role = systemPromptRole,
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
}
|
||||
|
||||
// Prepare the provider HTTP chat request:
|
||||
var providerChatRequest = JsonSerializer.Serialize(await requestFactory(systemPrompt, apiParameters), JSON_SERIALIZER_OPTIONS);
|
||||
var providerChatRequest = JsonSerializer.Serialize(await requestFactory(systemPrompt, apiParameters, null), JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
@ -1011,6 +1168,64 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
yield return content;
|
||||
}
|
||||
|
||||
private AIStudio.Settings.Provider CreateSettingsProvider(Model chatModel) => new()
|
||||
{
|
||||
UsedLLMProvider = this.Provider,
|
||||
Model = chatModel,
|
||||
InstanceName = this.InstanceName,
|
||||
};
|
||||
|
||||
private IList<ChatCompletionToolCall> CanonicalizeToolCallNames(
|
||||
IEnumerable<ChatCompletionToolCall> toolCalls,
|
||||
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools) => toolCalls
|
||||
.Select(toolCall =>
|
||||
{
|
||||
var returnedName = toolCall.Function.Name;
|
||||
var canonicalName = runnableTools
|
||||
.Select(x => x.Definition.Function.Name)
|
||||
.FirstOrDefault(x => x.Equals(returnedName.Trim(), StringComparison.Ordinal));
|
||||
if (canonicalName is null || canonicalName.Equals(returnedName, StringComparison.Ordinal))
|
||||
return toolCall;
|
||||
|
||||
this.logger.LogWarning("Canonicalized tool call function name '{ReturnedFunctionName}' to '{CanonicalFunctionName}'.", returnedName, canonicalName);
|
||||
return toolCall with
|
||||
{
|
||||
Function = toolCall.Function with
|
||||
{
|
||||
Name = canonicalName,
|
||||
},
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
private async Task<ChatCompletionResponse?> ExecuteChatCompletionRequest(
|
||||
ChatCompletionAPIRequest requestDto,
|
||||
string requestPath,
|
||||
RequestedSecret requestedSecret,
|
||||
Action<HttpRequestHeaders>? headersAction,
|
||||
CancellationToken token)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, requestPath);
|
||||
if (requestedSecret.Success)
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION));
|
||||
|
||||
headersAction?.Invoke(request.Headers);
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(requestDto, JSON_SERIALIZER_OPTIONS), Encoding.UTF8, "application/json");
|
||||
|
||||
using var response = await this.HttpClient.SendAsync(request, token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var responseBody = await response.Content.ReadAsStringAsync(token);
|
||||
this.logger.LogError("Tool calling chat completion request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody);
|
||||
await MessageBus.INSTANCE.SendError(new(
|
||||
Icons.Material.Filled.Build,
|
||||
string.Format(TB("The tool calling request failed with status code {0}. See the logs for details."), (int)response.StatusCode)));
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<ChatCompletionResponse>(JSON_SERIALIZER_OPTIONS, token);
|
||||
}
|
||||
|
||||
protected async Task<TranscriptionResult> PerformStandardTranscriptionRequest(RequestedSecret requestedSecret, Model transcriptionModel, string audioFilePath, Host host = Host.NONE, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
@ -1289,4 +1504,4 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, ne
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel);
|
||||
@ -44,6 +44,8 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, ne
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
@ -106,4 +108,4 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, ne
|
||||
token,
|
||||
apiKeyProvisional);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, new Uri(
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
@ -45,6 +45,8 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, new Uri(
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
|
||||
@ -29,7 +29,7 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
@ -44,6 +44,8 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
|
||||
@ -31,7 +31,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
@ -47,6 +47,8 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
|
||||
@ -29,7 +29,7 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, new Uri("https://a
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
if (TryPopIntParameter(apiParameters, "seed", out var parsedSeed))
|
||||
apiParameters["seed"] = parsedSeed;
|
||||
@ -48,6 +48,8 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, new Uri("https://a
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
@ -113,4 +115,4 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, new Uri("https://a
|
||||
token,
|
||||
apiKeyProvisional);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
@ -46,6 +46,8 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
@ -161,4 +163,4 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.PROVIDER_UNAVAILABLE, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ public sealed class ProviderHuggingFace : BaseProvider
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
@ -49,6 +49,8 @@ public sealed class ProviderHuggingFace : BaseProvider
|
||||
Messages = [systemPrompt, ..messages],
|
||||
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
|
||||
@ -29,7 +29,7 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
if (TryPopBoolParameter(apiParameters, "safe_prompt", out var parsedSafePrompt))
|
||||
apiParameters["safe_prompt"] = parsedSafePrompt;
|
||||
@ -51,6 +51,8 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
public sealed record AssistantToolCallMessage : IMessageBase
|
||||
{
|
||||
public string Role { get; init; } = "assistant";
|
||||
|
||||
public string? Content { get; init; }
|
||||
|
||||
public IList<ChatCompletionToolCall> ToolCalls { get; init; } = [];
|
||||
}
|
||||
@ -17,8 +17,14 @@ public record ChatCompletionAPIRequest(
|
||||
public ChatCompletionAPIRequest() : this(string.Empty, [], true)
|
||||
{
|
||||
}
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public IList<object>? Tools { get; init; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public bool? ParallelToolCalls { get; init; }
|
||||
|
||||
// Attention: The "required" modifier is not supported for [JsonExtensionData].
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
public sealed record ChatCompletionResponse
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
public string Model { get; init; } = string.Empty;
|
||||
|
||||
public IList<ChatCompletionResponseChoice> Choices { get; init; } = [];
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
public sealed record ChatCompletionResponseChoice
|
||||
{
|
||||
public int Index { get; init; }
|
||||
|
||||
public string FinishReason { get; init; } = string.Empty;
|
||||
|
||||
public ChatCompletionResponseMessage Message { get; init; } = new();
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
public sealed record ChatCompletionResponseMessage
|
||||
{
|
||||
public string Role { get; init; } = string.Empty;
|
||||
|
||||
public string? Content { get; init; }
|
||||
|
||||
public IList<ChatCompletionToolCall>? ToolCalls { get; init; }
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
public sealed record ChatCompletionToolCall
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
public string Type { get; init; } = "function";
|
||||
|
||||
public ChatCompletionToolFunction Function { get; init; } = new();
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
public sealed record ChatCompletionToolFunction
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
public string Arguments { get; init; } = string.Empty;
|
||||
}
|
||||
@ -7,6 +7,11 @@ using System.Text.Json;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
@ -16,7 +21,6 @@ namespace AIStudio.Provider.OpenAI;
|
||||
public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Uri("https://api.openai.com/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER)
|
||||
{
|
||||
private static readonly ILogger<ProviderOpenAI> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderOpenAI>();
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderOpenAI).Namespace, nameof(ProviderOpenAI));
|
||||
|
||||
#region Implementation of IProvider
|
||||
@ -98,81 +102,148 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
||||
|
||||
LOGGER.LogInformation("Using the system prompt role '{SystemPromptRole}' and the '{RequestPath}' API for model '{ChatModelId}'.", systemPromptRole, requestPath, chatModel.Id);
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = systemPromptRole,
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager),
|
||||
};
|
||||
|
||||
//
|
||||
// Prepare the tools we want to use:
|
||||
//
|
||||
IList<ProviderTool> providerTools = modelCapabilities.Contains(Capability.WEB_SEARCH) switch
|
||||
{
|
||||
true => [ ProviderTools.WEB_SEARCH ],
|
||||
_ => []
|
||||
};
|
||||
var providerConfidence = this.Provider.GetConfidence(settingsManager).Level;
|
||||
var minimumWebSearchConfidence = settingsManager.GetMinimumProviderConfidenceForTool(ToolSelectionRules.WEB_SEARCH_TOOL_ID);
|
||||
var isWebSearchAllowed = settingsManager.IsToolActive(ToolSelectionRules.WEB_SEARCH_TOOL_ID) &&
|
||||
ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumWebSearchConfidence);
|
||||
IList<object> providerTools = modelCapabilities.Contains(Capability.WEB_SEARCH) && isWebSearchAllowed
|
||||
? [ ProviderTools.WEB_SEARCH ]
|
||||
: [];
|
||||
|
||||
|
||||
// Parse the API parameters:
|
||||
var apiParameters = this.ParseAdditionalApiParameters("input", "store", "tools");
|
||||
|
||||
if (!usingResponsesAPI)
|
||||
{
|
||||
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
|
||||
"OpenAI",
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
var messages = await chatThread.Blocks.BuildMessagesAsync(
|
||||
this.Provider,
|
||||
chatModel,
|
||||
role => role switch
|
||||
{
|
||||
ChatRole.USER => "user",
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => systemPromptRole,
|
||||
_ => "user",
|
||||
},
|
||||
text => new SubContentText
|
||||
{
|
||||
Text = text,
|
||||
},
|
||||
async attachment => new SubContentImageUrlNested
|
||||
{
|
||||
ImageUrl = new SubContentImageUrlData
|
||||
{
|
||||
Url = await attachment.TryAsBase64(token: token) is (true, var base64Content)
|
||||
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
|
||||
: string.Empty,
|
||||
},
|
||||
});
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
Messages = [systemPrompt, ..messages],
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters,
|
||||
};
|
||||
},
|
||||
systemPromptRole: systemPromptRole,
|
||||
requestPath: "chat/completions",
|
||||
token: token))
|
||||
yield return content;
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
var toolRegistry = Program.SERVICE_PROVIDER.GetService<ToolRegistry>();
|
||||
var toolExecutor = Program.SERVICE_PROVIDER.GetService<ToolExecutor>();
|
||||
var currentAssistantContent = chatThread.Blocks.LastOrDefault(x => x.Role is ChatRole.AI)?.Content as ContentText;
|
||||
currentAssistantContent?.ToolInvocations.Clear();
|
||||
|
||||
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools = toolRegistry is null
|
||||
? []
|
||||
: await toolRegistry.GetRunnableToolsAsync(
|
||||
new AIStudio.Settings.Provider
|
||||
{
|
||||
UsedLLMProvider = this.Provider,
|
||||
Model = chatModel,
|
||||
InstanceName = this.InstanceName,
|
||||
},
|
||||
chatThread.RuntimeComponent,
|
||||
chatThread.RuntimeSelectedToolIds,
|
||||
modelCapabilities,
|
||||
providerConfidence,
|
||||
settingsManager.IsToolSelectionVisible(chatThread.RuntimeComponent));
|
||||
|
||||
var toolAwareDefinitions = toolExecutor is null
|
||||
? Enumerable.Empty<ToolDefinition>()
|
||||
: runnableTools.Select(x => x.Definition);
|
||||
var systemPrompt = new TextMessage
|
||||
{
|
||||
Role = systemPromptRole,
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager, toolAwareDefinitions),
|
||||
};
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesAsync(
|
||||
this.Provider, chatModel,
|
||||
|
||||
// OpenAI-specific role mapping:
|
||||
role => role switch
|
||||
{
|
||||
ChatRole.USER => "user",
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => systemPromptRole,
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
// OpenAI's text sub-content depends on the model, whether we are using
|
||||
// the Responses API or the Chat Completion API:
|
||||
text => usingResponsesAPI switch
|
||||
text => new SubContentInputText
|
||||
{
|
||||
// Responses API uses INPUT_TEXT:
|
||||
true => new SubContentInputText
|
||||
{
|
||||
Text = text,
|
||||
},
|
||||
|
||||
// Chat Completion API uses TEXT:
|
||||
false => new SubContentText
|
||||
{
|
||||
Text = text,
|
||||
},
|
||||
Text = text,
|
||||
},
|
||||
|
||||
// OpenAI's image sub-content depends on the model as well,
|
||||
// whether we are using the Responses API or the Chat Completion API:
|
||||
async attachment => usingResponsesAPI switch
|
||||
async attachment => new SubContentInputImage
|
||||
{
|
||||
// Responses API uses INPUT_IMAGE:
|
||||
true => new SubContentInputImage
|
||||
{
|
||||
ImageUrl = await attachment.TryAsBase64(token: token) is (true, var base64Content)
|
||||
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
|
||||
: string.Empty,
|
||||
},
|
||||
|
||||
// Chat Completion API uses IMAGE_URL:
|
||||
false => new SubContentImageUrlNested
|
||||
{
|
||||
ImageUrl = new SubContentImageUrlData
|
||||
{
|
||||
Url = await attachment.TryAsBase64(token: token) is (true, var base64Content)
|
||||
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
|
||||
: string.Empty,
|
||||
},
|
||||
}
|
||||
ImageUrl = await attachment.TryAsBase64(token: token) is (true, var base64Content)
|
||||
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
|
||||
: string.Empty,
|
||||
});
|
||||
|
||||
var baseInput = new List<object> { systemPrompt };
|
||||
baseInput.AddRange(messages.Cast<object>());
|
||||
|
||||
if (usingResponsesAPI && toolExecutor is not null && runnableTools.Count > 0)
|
||||
{
|
||||
await foreach (var content in this.StreamResponsesWithLocalTools(
|
||||
chatModel,
|
||||
chatThread,
|
||||
baseInput,
|
||||
apiParameters,
|
||||
providerTools,
|
||||
runnableTools,
|
||||
toolExecutor,
|
||||
currentAssistantContent,
|
||||
requestedSecret,
|
||||
providerConfidence,
|
||||
token))
|
||||
yield return content;
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (runnableTools.Count > 0)
|
||||
providerTools = [];
|
||||
|
||||
//
|
||||
// Create the request: either for the Responses API or the Chat Completion API
|
||||
@ -198,7 +269,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
||||
Model = chatModel.Id,
|
||||
|
||||
// All messages go into the input field:
|
||||
Input = [systemPrompt, ..messages],
|
||||
Input = baseInput,
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
@ -207,7 +278,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
||||
Store = false,
|
||||
|
||||
// Tools we want to use:
|
||||
ProviderTools = providerTools,
|
||||
Tools = providerTools,
|
||||
|
||||
// Additional API parameters:
|
||||
AdditionalApiParameters = apiParameters
|
||||
@ -237,6 +308,180 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
||||
yield return content;
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<ContentStreamChunk> StreamResponsesWithLocalTools(
|
||||
Model chatModel,
|
||||
ChatThread chatThread,
|
||||
IList<object> baseInput,
|
||||
IDictionary<string, object> apiParameters,
|
||||
IList<object> providerTools,
|
||||
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools,
|
||||
ToolExecutor toolExecutor,
|
||||
ContentText? currentAssistantContent,
|
||||
RequestedSecret requestedSecret,
|
||||
ConfidenceLevel providerConfidence,
|
||||
[EnumeratorCancellation] CancellationToken token)
|
||||
{
|
||||
var localProviderTools = runnableTools
|
||||
.Select(x => (object)ProviderToolAdapters.ToResponsesTool(x.Definition))
|
||||
.ToList();
|
||||
var localFunctionNames = runnableTools
|
||||
.Select(x => x.Definition.Function.Name)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var effectiveProviderTools = providerTools
|
||||
.Where(x => x is not ProviderTool providerTool || !localFunctionNames.Contains(providerTool.Type))
|
||||
.Concat(localProviderTools)
|
||||
.ToList();
|
||||
// Preserve every output item required to continue the response, including
|
||||
// reasoning items emitted alongside function calls.
|
||||
var internalItems = new List<object>();
|
||||
var toolCallCount = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var finalResponseRequired = toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS;
|
||||
var requestInput = new List<object>(baseInput);
|
||||
if (finalResponseRequired && requestInput.FirstOrDefault() is TextMessage systemPrompt)
|
||||
{
|
||||
requestInput[0] = systemPrompt with
|
||||
{
|
||||
Content = $"{systemPrompt.Content}{Environment.NewLine}{Environment.NewLine}{ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction()}",
|
||||
};
|
||||
}
|
||||
requestInput.AddRange(internalItems);
|
||||
|
||||
var requestDto = new ResponsesAPIRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
Input = requestInput,
|
||||
Stream = false,
|
||||
Store = false,
|
||||
Tools = finalResponseRequired ? [] : effectiveProviderTools,
|
||||
AdditionalApiParameters = apiParameters,
|
||||
};
|
||||
var response = await this.ExecuteResponsesRequest(requestDto, requestedSecret, token);
|
||||
if (response is null)
|
||||
{
|
||||
await ResetToolRuntimeStatusAsync(currentAssistantContent);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (finalResponseRequired)
|
||||
{
|
||||
await ResetToolRuntimeStatusAsync(currentAssistantContent);
|
||||
|
||||
var textOutput = response.GetTextOutput();
|
||||
if (!string.IsNullOrWhiteSpace(textOutput))
|
||||
yield return new ContentStreamChunk(textOutput, []);
|
||||
else
|
||||
yield return new ContentStreamChunk("The model did not return a final answer after completing the available tool calls.", []);
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
var functionCalls = response.GetFunctionCalls();
|
||||
if (functionCalls.Count == 0)
|
||||
{
|
||||
await ResetToolRuntimeStatusAsync(currentAssistantContent);
|
||||
|
||||
var textOutput = response.GetTextOutput();
|
||||
if (!string.IsNullOrWhiteSpace(textOutput))
|
||||
yield return new ContentStreamChunk(textOutput, []);
|
||||
else if (toolCallCount > 0)
|
||||
yield return new ContentStreamChunk("The model completed the tool call but did not return a final answer.", []);
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await ShowToolRuntimeStatusAsync(currentAssistantContent, functionCalls
|
||||
.Select(x => runnableTools.FirstOrDefault(tool => tool.Definition.Function.Name.Equals(x.Name, StringComparison.Ordinal)).Implementation?.GetDisplayName() ?? x.Name));
|
||||
|
||||
foreach (var outputItem in response.Output)
|
||||
internalItems.Add(outputItem);
|
||||
|
||||
foreach (var functionCall in functionCalls)
|
||||
{
|
||||
if (toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS)
|
||||
{
|
||||
var finalResponseInstruction = ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction();
|
||||
internalItems.Add(new ResponsesFunctionCallOutputItem
|
||||
{
|
||||
CallId = functionCall.CallId,
|
||||
Output = finalResponseInstruction,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
toolCallCount++;
|
||||
var (toolContent, trace, requiredProviderConfidence) = await toolExecutor.ExecuteAsync(
|
||||
functionCall.CallId,
|
||||
functionCall.Name,
|
||||
functionCall.Arguments,
|
||||
runnableTools,
|
||||
providerConfidence,
|
||||
toolCallCount,
|
||||
token);
|
||||
|
||||
chatThread.RequireProviderConfidence(requiredProviderConfidence);
|
||||
currentAssistantContent?.ToolInvocations.Add(trace);
|
||||
internalItems.Add(new ResponsesFunctionCallOutputItem
|
||||
{
|
||||
CallId = functionCall.CallId,
|
||||
Output = toolContent,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ResetToolRuntimeStatusAsync(currentAssistantContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ResetToolRuntimeStatusAsync(ContentText? currentAssistantContent)
|
||||
{
|
||||
if (currentAssistantContent is null)
|
||||
return;
|
||||
|
||||
currentAssistantContent.ToolRuntimeStatus = new();
|
||||
await currentAssistantContent.StreamingEvent();
|
||||
}
|
||||
|
||||
private static async Task ShowToolRuntimeStatusAsync(ContentText? currentAssistantContent, IEnumerable<string> toolNames)
|
||||
{
|
||||
if (currentAssistantContent is null)
|
||||
return;
|
||||
|
||||
currentAssistantContent.ToolRuntimeStatus = new ToolRuntimeStatus
|
||||
{
|
||||
IsRunning = true,
|
||||
ToolNames = toolNames.ToList(),
|
||||
};
|
||||
await currentAssistantContent.StreamingEvent();
|
||||
}
|
||||
|
||||
private async Task<ResponsesResponse?> ExecuteResponsesRequest(ResponsesAPIRequest requestDto, RequestedSecret requestedSecret, CancellationToken token)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "responses");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION));
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(requestDto, JSON_SERIALIZER_OPTIONS), Encoding.UTF8, "application/json");
|
||||
|
||||
using var response = await this.HttpClient.SendAsync(request, token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var responseBody = await response.Content.ReadAsStringAsync(token);
|
||||
LOGGER.LogError("Tool calling Responses API request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody);
|
||||
await MessageBus.INSTANCE.SendError(new(
|
||||
Icons.Material.Filled.Build,
|
||||
string.Format(TB("The tool calling request failed with status code {0}. See the logs for details."), (int)response.StatusCode)));
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<ResponsesResponse>(JSON_SERIALIZER_OPTIONS, token);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -370,4 +615,4 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
||||
propertyElement.ValueKind is JsonValueKind.String &&
|
||||
string.Equals(propertyElement.GetString(), expectedValue, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Converts the canonical AI Studio tool definition into provider-specific wire shapes.
|
||||
/// </summary>
|
||||
public static class ProviderToolAdapters
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the nested function tool shape used by Chat Completions compatible APIs.
|
||||
/// </summary>
|
||||
public static object ToChatCompletionTool(ToolDefinition definition) => new
|
||||
{
|
||||
type = "function",
|
||||
function = new
|
||||
{
|
||||
name = definition.Function.Name,
|
||||
description = definition.Function.DescriptionForLLM,
|
||||
parameters = definition.Function.Parameters,
|
||||
strict = definition.Function.Strict,
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Builds the flat function tool shape used by the OpenAI Responses API.
|
||||
/// </summary>
|
||||
public static ResponsesFunctionTool ToResponsesTool(ToolDefinition definition) => new()
|
||||
{
|
||||
Name = definition.Function.Name,
|
||||
Description = definition.Function.DescriptionForLLM,
|
||||
Parameters = definition.Function.Parameters,
|
||||
Strict = definition.Function.Strict,
|
||||
};
|
||||
}
|
||||
@ -6,16 +6,16 @@ namespace AIStudio.Provider.OpenAI;
|
||||
/// The request body for the Responses API.
|
||||
/// </summary>
|
||||
/// <param name="Model">Which model to use.</param>
|
||||
/// <param name="Input">The chat messages.</param>
|
||||
/// <param name="Input">The chat messages and Responses API input items.</param>
|
||||
/// <param name="Stream">Whether to stream the response.</param>
|
||||
/// <param name="Store">Whether to store the response on the server (usually OpenAI's infrastructure).</param>
|
||||
/// <param name="ProviderTools">The provider-side tools to use for the request.</param>
|
||||
/// <param name="Tools">The provider-side tools and local function tools to use for the request.</param>
|
||||
public record ResponsesAPIRequest(
|
||||
string Model,
|
||||
IList<IMessageBase> Input,
|
||||
IList<object> Input,
|
||||
bool Stream,
|
||||
bool Store,
|
||||
[property: JsonPropertyName("tools")] IList<ProviderTool> ProviderTools)
|
||||
IList<object> Tools)
|
||||
{
|
||||
public ResponsesAPIRequest() : this(string.Empty, [], true, false, [])
|
||||
{
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// A function call item returned by the OpenAI Responses API.
|
||||
/// </summary>
|
||||
public sealed record ResponsesFunctionCallItem
|
||||
{
|
||||
public string Type { get; init; } = string.Empty;
|
||||
|
||||
public string CallId { get; init; } = string.Empty;
|
||||
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
public string Arguments { get; init; } = string.Empty;
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// A local function result item sent back to the OpenAI Responses API.
|
||||
/// </summary>
|
||||
public sealed record ResponsesFunctionCallOutputItem
|
||||
{
|
||||
public string Type { get; init; } = "function_call_output";
|
||||
|
||||
public string CallId { get; init; } = string.Empty;
|
||||
|
||||
public string Output { get; init; } = string.Empty;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// The flat function tool definition shape expected by the OpenAI Responses API.
|
||||
/// </summary>
|
||||
public sealed record ResponsesFunctionTool
|
||||
{
|
||||
public string Type { get; init; } = "function";
|
||||
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
public JsonElement Parameters { get; init; }
|
||||
|
||||
public bool Strict { get; init; }
|
||||
}
|
||||
62
app/MindWork AI Studio/Provider/OpenAI/ResponsesResponse.cs
Normal file
62
app/MindWork AI Studio/Provider/OpenAI/ResponsesResponse.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Non-streaming OpenAI Responses API result used during local tool execution.
|
||||
/// </summary>
|
||||
public sealed record ResponsesResponse
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
public string Model { get; init; } = string.Empty;
|
||||
|
||||
public string? OutputText { get; init; }
|
||||
|
||||
public IList<JsonElement> Output { get; init; } = [];
|
||||
|
||||
public IReadOnlyList<ResponsesFunctionCallItem> GetFunctionCalls() => this.Output
|
||||
.Where(x => ReadString(x, "type").Equals("function_call", StringComparison.Ordinal))
|
||||
.Select(x => new ResponsesFunctionCallItem
|
||||
{
|
||||
Type = ReadString(x, "type"),
|
||||
CallId = ReadString(x, "call_id"),
|
||||
Name = ReadString(x, "name"),
|
||||
Arguments = ReadString(x, "arguments"),
|
||||
})
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.CallId) && !string.IsNullOrWhiteSpace(x.Name))
|
||||
.ToList();
|
||||
|
||||
public string GetTextOutput()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(this.OutputText))
|
||||
return this.OutputText;
|
||||
|
||||
return string.Concat(this.Output
|
||||
.Where(x => ReadString(x, "type").Equals("message", StringComparison.Ordinal))
|
||||
.SelectMany(ReadContentItems)
|
||||
.Where(x => ReadString(x, "type").Equals("output_text", StringComparison.Ordinal))
|
||||
.Select(x => ReadString(x, "text")));
|
||||
}
|
||||
|
||||
private static IEnumerable<JsonElement> ReadContentItems(JsonElement outputItem)
|
||||
{
|
||||
if (outputItem.ValueKind is not JsonValueKind.Object ||
|
||||
!outputItem.TryGetProperty("content", out var content) ||
|
||||
content.ValueKind is not JsonValueKind.Array)
|
||||
yield break;
|
||||
|
||||
foreach (var contentItem in content.EnumerateArray())
|
||||
yield return contentItem;
|
||||
}
|
||||
|
||||
private static string ReadString(JsonElement item, string propertyName)
|
||||
{
|
||||
if (item.ValueKind is not JsonValueKind.Object ||
|
||||
!item.TryGetProperty(propertyName, out var property) ||
|
||||
property.ValueKind is not JsonValueKind.String)
|
||||
return string.Empty;
|
||||
|
||||
return property.GetString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
10
app/MindWork AI Studio/Provider/OpenAI/ToolResultMessage.cs
Normal file
10
app/MindWork AI Studio/Provider/OpenAI/ToolResultMessage.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
public sealed record ToolResultMessage : IMessage<string>
|
||||
{
|
||||
public string Role { get; init; } = "tool";
|
||||
|
||||
public string Content { get; init; } = string.Empty;
|
||||
|
||||
public string ToolCallId { get; init; } = string.Empty;
|
||||
}
|
||||
@ -33,7 +33,7 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
@ -49,6 +49,8 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
|
||||
@ -38,7 +38,7 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
@ -52,6 +52,8 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..messages],
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
@ -106,4 +108,4 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
|
||||
#endregion
|
||||
|
||||
private Task<ModelLoadResult> LoadModels() => Task.FromResult(ModelLoadResult.FromModels(KNOWN_MODELS));
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
effectiveChatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages. The image format depends on the host:
|
||||
// - Ollama uses the direct image URL format: { "type": "image_url", "image_url": "data:..." }
|
||||
@ -57,6 +57,8 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
@ -202,7 +204,6 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.PROVIDER_UNAVAILABLE, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Provider.Model> ResolveChatModelForRequest(Provider.Model chatModel, CancellationToken token)
|
||||
{
|
||||
if (host is not Host.LLAMA_CPP || !chatModel.IsSystemModel)
|
||||
@ -325,4 +326,4 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
{
|
||||
return ModelLoadResult.FromModels([ AIStudio.Provider.Model.SYSTEM_MODEL ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, new Uri("https://
|
||||
chatModel,
|
||||
chatThread,
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters) =>
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
@ -45,6 +45,8 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, new Uri("https://
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
Tools = tools,
|
||||
ParallelToolCalls = tools is null ? null : true,
|
||||
AdditionalApiParameters = apiParameters
|
||||
};
|
||||
},
|
||||
@ -118,4 +120,4 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, new Uri("https://
|
||||
token,
|
||||
apiKeyProvisional);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,4 +154,6 @@ public sealed class Data
|
||||
public DataBiasOfTheDay BiasOfTheDay { get; init; } = new();
|
||||
|
||||
public DataI18N I18N { get; init; } = new();
|
||||
|
||||
public DataTools Tools { get; init; } = new(x => x.Tools);
|
||||
}
|
||||
|
||||
103
app/MindWork AI Studio/Settings/DataModel/DataTools.cs
Normal file
103
app/MindWork AI Studio/Settings/DataModel/DataTools.cs
Normal file
@ -0,0 +1,103 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
public sealed class DataTools(Expression<Func<Data, DataTools>>? configSelection = null)
|
||||
{
|
||||
public DataTools() : this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public Dictionary<string, Dictionary<string, string>> Settings { get; set; } = [];
|
||||
|
||||
public Dictionary<string, HashSet<string>> DefaultToolIdsByComponent { get; set; } = [];
|
||||
|
||||
public HashSet<string> VisibleToolSelectionComponents { get; set; } = [];
|
||||
|
||||
public bool EnableTools { get; set; } = ManagedConfiguration.Register(
|
||||
configSelection,
|
||||
x => x.EnableTools,
|
||||
true);
|
||||
|
||||
public HashSet<string> DisabledToolIds { get; set; } = ManagedConfiguration.Register(
|
||||
configSelection,
|
||||
x => x.DisabledToolIds,
|
||||
[]);
|
||||
|
||||
public Dictionary<string, string> MinimumProviderConfidenceByToolId { get; set; } = ManagedConfiguration.Register<DataTools, Dictionary<string, string>>(
|
||||
configSelection,
|
||||
x => x.MinimumProviderConfidenceByToolId,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal));
|
||||
|
||||
public string WebSearchBaseUrl { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.WebSearchBaseUrl,
|
||||
string.Empty);
|
||||
|
||||
public string WebSearchDefaultLanguage { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.WebSearchDefaultLanguage,
|
||||
string.Empty);
|
||||
|
||||
public string WebSearchDefaultSafeSearch { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.WebSearchDefaultSafeSearch,
|
||||
string.Empty);
|
||||
|
||||
public string WebSearchDefaultCategories { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.WebSearchDefaultCategories,
|
||||
string.Empty);
|
||||
|
||||
public string WebSearchDefaultEngines { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.WebSearchDefaultEngines,
|
||||
string.Empty);
|
||||
|
||||
public string WebSearchMaxResults { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.WebSearchMaxResults,
|
||||
string.Empty);
|
||||
|
||||
public string WebSearchTimeoutSeconds { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.WebSearchTimeoutSeconds,
|
||||
string.Empty);
|
||||
|
||||
public string WebSearchMaxTotalContentCharacters { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.WebSearchMaxTotalContentCharacters,
|
||||
string.Empty);
|
||||
|
||||
public string WebSearchMinContentCharactersPerResult { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.WebSearchMinContentCharactersPerResult,
|
||||
string.Empty);
|
||||
|
||||
public string WebSearchPageTimeoutSeconds { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.WebSearchPageTimeoutSeconds,
|
||||
string.Empty);
|
||||
|
||||
public string WebSearchRetrievalTimeoutSeconds { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.WebSearchRetrievalTimeoutSeconds,
|
||||
string.Empty);
|
||||
|
||||
public string ReadWebPageTimeoutSeconds { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.ReadWebPageTimeoutSeconds,
|
||||
string.Empty);
|
||||
|
||||
public string ReadWebPageMaxContentCharacters { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.ReadWebPageMaxContentCharacters,
|
||||
string.Empty);
|
||||
|
||||
public string ReadWebPageAllowedPrivateHosts { get; set; } = ManagedConfiguration.Register<DataTools>(
|
||||
configSelection,
|
||||
x => x.ReadWebPageAllowedPrivateHosts,
|
||||
string.Empty);
|
||||
}
|
||||
@ -768,7 +768,7 @@ public static partial class ManagedConfiguration
|
||||
return false;
|
||||
|
||||
var successful = false;
|
||||
var configuredValue = configMeta.Default;
|
||||
var configuredValue = CloneStringDictionary(configMeta.Default);
|
||||
|
||||
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
|
||||
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
|
||||
@ -805,7 +805,9 @@ public static partial class ManagedConfiguration
|
||||
if(dryRun)
|
||||
return successful;
|
||||
|
||||
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
|
||||
var settingName = SettingName(propertyExpression);
|
||||
var managedMode = ReadManagedConfigurationMode(propertyExpression, settings);
|
||||
return HandleParsedDictionaryValue(configPluginId, dryRun, successful, configMeta, configuredValue, managedMode, settingName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -1003,6 +1005,67 @@ public static partial class ManagedConfiguration
|
||||
return successful;
|
||||
}
|
||||
|
||||
private static bool HandleParsedDictionaryValue<TClass>(
|
||||
Guid configPluginId,
|
||||
bool dryRun,
|
||||
bool successful,
|
||||
ConfigMeta<TClass, IDictionary<string, string>> configMeta,
|
||||
IDictionary<string, string> configuredValue,
|
||||
ManagedConfigurationMode managedMode,
|
||||
string settingName)
|
||||
{
|
||||
if (dryRun)
|
||||
return successful;
|
||||
|
||||
switch (successful)
|
||||
{
|
||||
case true when managedMode is ManagedConfigurationMode.LOCKED:
|
||||
ClearEditableDefaultState(settingName);
|
||||
configMeta.ClearEditableDefaultConfiguration();
|
||||
configMeta.SetValue(CloneStringDictionary(configuredValue));
|
||||
configMeta.LockConfiguration(configPluginId);
|
||||
break;
|
||||
|
||||
case true when managedMode is ManagedConfigurationMode.EDITABLE_DEFAULT:
|
||||
var currentValueSerialized = SerializeManagedStringDictionaryValue(configMeta.GetValue());
|
||||
var configuredValueSerialized = SerializeManagedStringDictionaryValue(configuredValue);
|
||||
|
||||
string lastAppliedValue;
|
||||
if (!TryGetEditableDefaultState(settingName, out var editableDefaultState))
|
||||
{
|
||||
configMeta.SetValue(CloneStringDictionary(configuredValue));
|
||||
lastAppliedValue = configuredValueSerialized;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastAppliedValue = editableDefaultState.LastAppliedValue;
|
||||
if (string.Equals(currentValueSerialized, lastAppliedValue, StringComparison.Ordinal))
|
||||
{
|
||||
configMeta.SetValue(CloneStringDictionary(configuredValue));
|
||||
lastAppliedValue = configuredValueSerialized;
|
||||
}
|
||||
}
|
||||
|
||||
SetEditableDefaultState(settingName, configPluginId, lastAppliedValue);
|
||||
configMeta.UnlockConfiguration();
|
||||
configMeta.SetEditableDefaultConfiguration(configPluginId);
|
||||
break;
|
||||
|
||||
case false when configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId:
|
||||
configMeta.ResetLockedConfiguration();
|
||||
break;
|
||||
|
||||
case false when configMeta.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT
|
||||
&& TryGetEditableDefaultState(settingName, out var editableDefaultStateToRemove)
|
||||
&& editableDefaultStateToRemove.ConfigPluginId == configPluginId:
|
||||
configMeta.ClearEditableDefaultConfiguration();
|
||||
ClearEditableDefaultState(settingName);
|
||||
break;
|
||||
}
|
||||
|
||||
return successful;
|
||||
}
|
||||
|
||||
private static ManagedConfigurationMode ReadManagedConfigurationMode<TClass, TValue>(
|
||||
Expression<Func<TClass, TValue>> propertyExpression,
|
||||
LuaTable settings)
|
||||
@ -1036,4 +1099,12 @@ public static partial class ManagedConfiguration
|
||||
|
||||
_ => value.ToString() ?? string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> CloneStringDictionary(IDictionary<string, string> values) => new(values, StringComparer.Ordinal);
|
||||
|
||||
private static string SerializeManagedStringDictionaryValue(IDictionary<string, string> values) => string.Join(
|
||||
"\n",
|
||||
values
|
||||
.OrderBy(pair => pair.Key, StringComparer.Ordinal)
|
||||
.Select(pair => $"{pair.Key}={pair.Value}"));
|
||||
}
|
||||
|
||||
@ -426,17 +426,17 @@ public static partial class ManagedConfiguration
|
||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||
return false;
|
||||
|
||||
if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||
return false;
|
||||
|
||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
||||
if (plugin is null)
|
||||
if (configMeta.LockedByConfigPluginId != Guid.Empty && configMeta.IsLocked)
|
||||
{
|
||||
configMeta.ResetLockedConfiguration();
|
||||
return true;
|
||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
||||
if (plugin is null)
|
||||
{
|
||||
configMeta.ResetLockedConfiguration();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return CleanupEditableDefaultState(configMeta, SettingName(propertyExpression), [..availablePlugins]);
|
||||
}
|
||||
|
||||
public static bool IsConfigurationLeftOver<TClass, TKey, TValue>(
|
||||
@ -528,4 +528,4 @@ public static partial class ManagedConfiguration
|
||||
configMeta.ClearEditableDefaultConfiguration();
|
||||
return ClearEditableDefaultState(settingName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ using System.Text.Json;
|
||||
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
@ -16,6 +18,8 @@ namespace AIStudio.Settings;
|
||||
/// </summary>
|
||||
public sealed class SettingsManager
|
||||
{
|
||||
public readonly record struct ToolMinimumProviderConfidenceResolution(ConfidenceLevel ConfidenceLevel, string Source);
|
||||
|
||||
private const string SETTINGS_FILENAME = "settings.json";
|
||||
private const Version CURRENT_SETTINGS_VERSION = Version.V6;
|
||||
|
||||
@ -579,6 +583,122 @@ public sealed class SettingsManager
|
||||
return this.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id.Equals(chatTemplateId, StringComparison.OrdinalIgnoreCase)) ?? ChatTemplate.NO_CHAT_TEMPLATE;
|
||||
}
|
||||
|
||||
public HashSet<string> GetDefaultToolIds(AIStudio.Tools.Components component)
|
||||
{
|
||||
var key = component.ToString();
|
||||
if (this.ConfigurationData.Tools.DefaultToolIdsByComponent.TryGetValue(key, out var toolIds))
|
||||
return ToolSelectionRules.NormalizeSelection(toolIds);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public HashSet<string> FilterToolIdsForProvider(AIStudio.Settings.Provider provider, IEnumerable<string> selectedToolIds)
|
||||
{
|
||||
if (!this.AreToolsEnabled())
|
||||
return [];
|
||||
|
||||
var toolCallingAvailability = provider.GetToolCallingAvailability();
|
||||
if (!toolCallingAvailability.IsAvailable)
|
||||
return [];
|
||||
|
||||
var modelCapabilities = provider.GetModelCapabilities();
|
||||
var supportsRequiredApis =
|
||||
modelCapabilities.Contains(Capability.CHAT_COMPLETION_API) ||
|
||||
modelCapabilities.Contains(Capability.RESPONSES_API);
|
||||
if (!supportsRequiredApis || !modelCapabilities.Contains(Capability.FUNCTION_CALLING))
|
||||
return [];
|
||||
|
||||
var providerConfidence = provider.UsedLLMProvider.GetConfidence(this).Level;
|
||||
var filtered = ToolSelectionRules.NormalizeSelection(selectedToolIds);
|
||||
|
||||
foreach (var toolId in filtered.ToList())
|
||||
{
|
||||
if (!this.IsToolActive(toolId))
|
||||
{
|
||||
filtered.Remove(toolId);
|
||||
continue;
|
||||
}
|
||||
|
||||
var minimumToolConfidence = this.GetMinimumProviderConfidenceForTool(toolId);
|
||||
if (!ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumToolConfidence))
|
||||
filtered.Remove(toolId);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
public bool AreToolsEnabled() => this.ConfigurationData.Tools.EnableTools;
|
||||
|
||||
public bool IsToolActive(string toolId) =>
|
||||
this.AreToolsEnabled() &&
|
||||
!this.ConfigurationData.Tools.DisabledToolIds.Contains(toolId);
|
||||
|
||||
public bool IsToolSelectionVisible(AIStudio.Tools.Components component) => component switch
|
||||
{
|
||||
AIStudio.Tools.Components.CHAT => true,
|
||||
_ => this.ConfigurationData.Tools.VisibleToolSelectionComponents.Contains(component.ToString()),
|
||||
};
|
||||
|
||||
public void SetToolSelectionVisibility(AIStudio.Tools.Components component, bool isVisible)
|
||||
{
|
||||
if (component is AIStudio.Tools.Components.CHAT)
|
||||
return;
|
||||
|
||||
var key = component.ToString();
|
||||
if (isVisible)
|
||||
this.ConfigurationData.Tools.VisibleToolSelectionComponents.Add(key);
|
||||
else
|
||||
this.ConfigurationData.Tools.VisibleToolSelectionComponents.Remove(key);
|
||||
}
|
||||
|
||||
public ToolMinimumProviderConfidenceResolution GetMinimumProviderConfidenceResolutionForTool(string toolId)
|
||||
{
|
||||
if (ManagedConfiguration.TryGet(x => x.Tools, x => x.MinimumProviderConfidenceByToolId, out var configMeta) && configMeta.IsLocked)
|
||||
{
|
||||
var managedValues = configMeta.GetValue();
|
||||
if (managedValues.TryGetValue(toolId, out var configuredManagedLevel) &&
|
||||
Enum.TryParse<ConfidenceLevel>(configuredManagedLevel, true, out var managedConfidenceLevel) &&
|
||||
Enum.IsDefined(managedConfidenceLevel) &&
|
||||
managedConfidenceLevel is not ConfidenceLevel.UNKNOWN)
|
||||
{
|
||||
return new(managedConfidenceLevel, "managed config");
|
||||
}
|
||||
|
||||
if (managedValues.ContainsKey(toolId))
|
||||
{
|
||||
this.logger.LogError(
|
||||
"Managed minimum provider confidence '{ConfiguredLevel}' for tool '{ToolId}' is invalid. Requiring HIGH as a safe fallback.",
|
||||
configuredManagedLevel,
|
||||
toolId);
|
||||
return new(ConfidenceLevel.HIGH, "invalid managed config; safe fallback");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.ConfigurationData.Tools.MinimumProviderConfidenceByToolId.TryGetValue(toolId, out var configuredLevel) &&
|
||||
Enum.TryParse<ConfidenceLevel>(configuredLevel, true, out var confidenceLevel) &&
|
||||
Enum.IsDefined(confidenceLevel) &&
|
||||
confidenceLevel is not ConfidenceLevel.UNKNOWN)
|
||||
{
|
||||
return new(confidenceLevel, "stored override");
|
||||
}
|
||||
|
||||
return new(ToolSelectionRules.GetDefaultMinimumProviderConfidence(toolId), "default fallback");
|
||||
}
|
||||
|
||||
public ConfidenceLevel GetMinimumProviderConfidenceForTool(string toolId) => this.GetMinimumProviderConfidenceResolutionForTool(toolId).ConfidenceLevel;
|
||||
|
||||
public void SetMinimumProviderConfidenceForTool(string toolId, ConfidenceLevel confidenceLevel)
|
||||
{
|
||||
var defaultLevel = ToolSelectionRules.GetDefaultMinimumProviderConfidence(toolId);
|
||||
if (confidenceLevel == defaultLevel)
|
||||
{
|
||||
this.ConfigurationData.Tools.MinimumProviderConfidenceByToolId.Remove(toolId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.ConfigurationData.Tools.MinimumProviderConfidenceByToolId[toolId] = confidenceLevel.ToString();
|
||||
}
|
||||
|
||||
public ConfidenceLevel GetConfiguredConfidenceLevel(LLMProviders llmProvider)
|
||||
{
|
||||
if(llmProvider is LLMProviders.NONE)
|
||||
@ -667,4 +787,4 @@ public sealed class SettingsManager
|
||||
// Return the full name of the property, including the class name:
|
||||
return $"{typeof(TIn).Name}.{memberExpr.Member.Name}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,16 @@ public static class ExternalHttpClientTimeout
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
public static void ConfigureSocketsHttpHandler(SocketsHttpHandler handler, string host, ExternalHttpTrustPolicy trustPolicy)
|
||||
{
|
||||
var customRootCertificateCache = GetCustomRootCertificateCache();
|
||||
if (!customRootCertificateCache.State.IsUsable)
|
||||
return;
|
||||
|
||||
handler.SslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, sslPolicyErrors) =>
|
||||
ValidateServerCertificateWithCustomRootCertificates(host, certificate, chain, sslPolicyErrors, customRootCertificateCache, trustPolicy);
|
||||
}
|
||||
|
||||
public static ExternalHttpCustomRootCertificateState CustomRootCertificateState => GetCustomRootCertificateCache().State;
|
||||
|
||||
public static string GetTimeoutDescription()
|
||||
@ -355,11 +365,27 @@ public static class ExternalHttpClientTimeout
|
||||
SslPolicyErrors sslPolicyErrors,
|
||||
CustomRootCertificateCache customRootCertificateCache,
|
||||
ExternalHttpTrustPolicy trustPolicy)
|
||||
{
|
||||
return ValidateServerCertificateWithCustomRootCertificates(
|
||||
ReadRequestHost(request),
|
||||
certificate,
|
||||
originalChain,
|
||||
sslPolicyErrors,
|
||||
customRootCertificateCache,
|
||||
trustPolicy);
|
||||
}
|
||||
|
||||
private static bool ValidateServerCertificateWithCustomRootCertificates(
|
||||
string host,
|
||||
X509Certificate? certificate,
|
||||
X509Chain? originalChain,
|
||||
SslPolicyErrors sslPolicyErrors,
|
||||
CustomRootCertificateCache customRootCertificateCache,
|
||||
ExternalHttpTrustPolicy trustPolicy)
|
||||
{
|
||||
if (sslPolicyErrors is SslPolicyErrors.None)
|
||||
return true;
|
||||
|
||||
var host = ReadRequestHost(request);
|
||||
if (certificate is null)
|
||||
{
|
||||
LOGGER.Value.LogError($"Rejected external HTTPS certificate for '{HostForLog(host)}' because the TLS stack did not provide a server certificate. TLS policy errors: {sslPolicyErrors}.");
|
||||
@ -392,7 +418,7 @@ public static class ExternalHttpClientTimeout
|
||||
customChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
|
||||
customChain.ChainPolicy.CustomTrustStore.AddRange(customRootCertificateCache.Certificates);
|
||||
customChain.ChainPolicy.ApplicationPolicy.Add(new Oid(TLS_SERVER_AUTHENTICATION_EKU_OID));
|
||||
|
||||
|
||||
// Match the .NET 9 HttpClient default used for the initial system-trust validation.
|
||||
// Hostname, signature, validity, EKU, and root trust checks remain enabled.
|
||||
customChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
|
||||
@ -410,9 +436,9 @@ public static class ExternalHttpClientTimeout
|
||||
|
||||
var isValid = customChain.Build(serverCertificate);
|
||||
if (isValid)
|
||||
LogCustomRootCertificateAccepted(request);
|
||||
LogCustomRootCertificateAccepted(host);
|
||||
else
|
||||
LogCustomRootCertificateValidationFailure(request, sslPolicyErrors, customChain);
|
||||
LogCustomRootCertificateValidationFailure(host, sslPolicyErrors, customChain);
|
||||
|
||||
return isValid;
|
||||
}
|
||||
@ -468,20 +494,15 @@ public static class ExternalHttpClientTimeout
|
||||
LOGGER.Value.LogWarning($"External HTTP custom root certificates are enabled from {state.Source}, but no additional root certificates are usable. Bundle path: '{state.BundlePath}'. Issue: {state.Issue}");
|
||||
}
|
||||
|
||||
private static void LogCustomRootCertificateAccepted(HttpRequestMessage request)
|
||||
{
|
||||
var host = ReadRequestHost(request);
|
||||
LOGGER.Value.LogWarning($"Accepted an external HTTPS certificate for '{host}' using configured custom root certificates.");
|
||||
}
|
||||
private static void LogCustomRootCertificateAccepted(string host) => LOGGER.Value.LogWarning($"Accepted an external HTTPS certificate for '{host}' using configured custom root certificates.");
|
||||
|
||||
private static void LogCustomRootCertificateValidationFailure(HttpRequestMessage request, SslPolicyErrors sslPolicyErrors, X509Chain chain)
|
||||
private static void LogCustomRootCertificateValidationFailure(string host, SslPolicyErrors sslPolicyErrors, X509Chain chain)
|
||||
{
|
||||
var chainStatuses = FormatChainStatusesForLog(chain.ChainStatus);
|
||||
var elementStatuses = chain.ChainElements
|
||||
.Cast<X509ChainElement>()
|
||||
.Select((element, index) => $"element {index}: {FormatChainStatusesForLog(element.ChainElementStatus)}")
|
||||
.ToList();
|
||||
var host = ReadRequestHost(request);
|
||||
LOGGER.Value.LogError($"Rejected external HTTPS certificate for '{HostForLog(host)}' after validation with configured custom root certificates. TLS policy errors: {sslPolicyErrors}. Chain statuses: {chainStatuses}. Chain element statuses: {string.Join("; ", elementStatuses)}");
|
||||
}
|
||||
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
using HtmlAgilityPack;
|
||||
|
||||
using ReverseMarkdown;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed class HTMLParser
|
||||
{
|
||||
private const string USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) MindWorkAIStudio/1.0";
|
||||
private const int MAX_REDIRECTS = 10;
|
||||
private const int DEFAULT_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
private static readonly Config MARKDOWN_PARSER_CONFIG = new()
|
||||
{
|
||||
UnknownTags = Config.UnknownTagsOption.Bypass,
|
||||
@ -23,10 +27,8 @@ public sealed class HTMLParser
|
||||
/// <returns>The web content as text.</returns>
|
||||
public async Task<string> LoadWebContentText(Uri url)
|
||||
{
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
var parser = new HtmlWeb();
|
||||
var doc = await parser.LoadFromWebAsync(url, Encoding.UTF8, new NetworkCredential(), cts.Token);
|
||||
return doc.ParsedText;
|
||||
var response = await this.LoadWebPageAsync(url);
|
||||
return response.Document.ParsedText;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -36,14 +38,236 @@ public sealed class HTMLParser
|
||||
/// <returns>The web content as an HTML string.</returns>
|
||||
public async Task<string> LoadWebContentHTML(Uri url)
|
||||
{
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
var parser = new HtmlWeb();
|
||||
var doc = await parser.LoadFromWebAsync(url, Encoding.UTF8, new NetworkCredential(), cts.Token);
|
||||
var innerHtml = doc.DocumentNode.InnerHtml;
|
||||
var response = await this.LoadWebPageAsync(url);
|
||||
var innerHtml = response.Document.DocumentNode.InnerHtml;
|
||||
|
||||
return innerHtml;
|
||||
}
|
||||
|
||||
public async Task<HTMLParserWebPage> LoadWebPageAsync(
|
||||
Uri url,
|
||||
CancellationToken token = default,
|
||||
int timeoutSeconds = 30,
|
||||
Func<Uri, CancellationToken, Task<IReadOnlyList<IPAddress>>>? resolveUrlAddressesAsync = null,
|
||||
int maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES,
|
||||
ExternalWebAuthenticationMode authenticationMode = ExternalWebAuthenticationMode.NONE,
|
||||
ExternalHttpTrustPolicy trustPolicy = ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED,
|
||||
Func<Uri, IReadOnlyList<IPAddress>, bool>? shouldUseDefaultCredentials = null)
|
||||
{
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
|
||||
var cookieContainer = new CookieContainer();
|
||||
|
||||
var currentUrl = url;
|
||||
for (var redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount++)
|
||||
{
|
||||
ValidateHttpOrHttpsUrl(currentUrl);
|
||||
var resolvedAddresses = resolveUrlAddressesAsync is null
|
||||
? null
|
||||
: await resolveUrlAddressesAsync(currentUrl, timeoutCts.Token);
|
||||
var useDefaultCredentials = authenticationMode is ExternalWebAuthenticationMode.OS_DEFAULT_CREDENTIALS &&
|
||||
resolvedAddresses is not null &&
|
||||
shouldUseDefaultCredentials?.Invoke(currentUrl, resolvedAddresses) is true;
|
||||
using var handler = CreateHandler(currentUrl, resolvedAddresses, useDefaultCredentials, trustPolicy, cookieContainer);
|
||||
using var httpClient = new HttpClient(handler)
|
||||
{
|
||||
Timeout = Timeout.InfiniteTimeSpan,
|
||||
};
|
||||
|
||||
using var request = CreateRequest(currentUrl);
|
||||
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeoutCts.Token);
|
||||
if (IsRedirect(response.StatusCode))
|
||||
{
|
||||
if (response.Headers.Location is null)
|
||||
throw new HttpRequestException($"The server returned a redirect without a Location header for '{currentUrl}'.", null, response.StatusCode);
|
||||
|
||||
currentUrl = response.Headers.Location.IsAbsoluteUri
|
||||
? response.Headers.Location
|
||||
: new Uri(currentUrl, response.Headers.Location);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var statusCode = (int)response.StatusCode;
|
||||
var reasonPhrase = string.IsNullOrWhiteSpace(response.ReasonPhrase) ? "Unknown" : response.ReasonPhrase;
|
||||
throw new HttpRequestException($"The server returned HTTP {statusCode} ({reasonPhrase}) for '{currentUrl}'.", null, response.StatusCode);
|
||||
}
|
||||
|
||||
var html = await ReadContentAsStringWithLimitAsync(response.Content, maxResponseBytes, timeoutCts.Token);
|
||||
var document = new HtmlDocument();
|
||||
document.LoadHtml(html);
|
||||
|
||||
return new HTMLParserWebPage
|
||||
{
|
||||
RequestedUrl = url,
|
||||
FinalUrl = response.RequestMessage?.RequestUri ?? currentUrl,
|
||||
ContentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty,
|
||||
Document = document,
|
||||
};
|
||||
}
|
||||
|
||||
throw new HttpRequestException($"The server returned more than {MAX_REDIRECTS} redirects for '{url}'.");
|
||||
}
|
||||
|
||||
private static SocketsHttpHandler CreateHandler(
|
||||
Uri url,
|
||||
IReadOnlyList<IPAddress>? resolvedAddresses,
|
||||
bool useDefaultCredentials,
|
||||
ExternalHttpTrustPolicy trustPolicy,
|
||||
CookieContainer cookieContainer)
|
||||
{
|
||||
var handler = new SocketsHttpHandler
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
|
||||
AllowAutoRedirect = false,
|
||||
UseCookies = true,
|
||||
CookieContainer = cookieContainer,
|
||||
};
|
||||
ExternalHttpClientTimeout.ConfigureSocketsHttpHandler(handler, url.Host, trustPolicy);
|
||||
|
||||
if (useDefaultCredentials)
|
||||
handler.Credentials = CreateDefaultCredentialCache(url);
|
||||
|
||||
if (resolvedAddresses is not null)
|
||||
{
|
||||
// The callback binds the request to a vetted target IP; a proxy would change the endpoint being connected to.
|
||||
handler.UseProxy = false;
|
||||
handler.ConnectCallback = (context, connectionToken) => ConnectToResolvedAddressAsync(context, resolvedAddresses, connectionToken);
|
||||
}
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
private static CredentialCache CreateDefaultCredentialCache(Uri url)
|
||||
{
|
||||
var credentialCache = new CredentialCache();
|
||||
var uriPrefix = new UriBuilder(url.Scheme, url.Host, url.Port).Uri;
|
||||
credentialCache.Add(uriPrefix, "Negotiate", CredentialCache.DefaultNetworkCredentials);
|
||||
credentialCache.Add(uriPrefix, "NTLM", CredentialCache.DefaultNetworkCredentials);
|
||||
credentialCache.Add(uriPrefix, "Kerberos", CredentialCache.DefaultNetworkCredentials);
|
||||
return credentialCache;
|
||||
}
|
||||
|
||||
private static void ValidateHttpOrHttpsUrl(Uri url)
|
||||
{
|
||||
if (url.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
|
||||
url.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
throw new HttpRequestException($"Unsupported URL scheme '{url.Scheme}' for '{url}'.");
|
||||
}
|
||||
|
||||
private static async ValueTask<Stream> ConnectToResolvedAddressAsync(
|
||||
SocketsHttpConnectionContext context,
|
||||
IReadOnlyList<IPAddress> addresses,
|
||||
CancellationToken token)
|
||||
{
|
||||
var requestUri = context.InitialRequestMessage.RequestUri ??
|
||||
throw new HttpRequestException("The HTTP request did not contain a target URL.");
|
||||
|
||||
if (addresses.Count == 0)
|
||||
throw new HttpRequestException($"The host '{requestUri.Host}' did not resolve to an IP address.");
|
||||
|
||||
List<SocketException> connectionErrors = [];
|
||||
foreach (var address in addresses.Distinct())
|
||||
{
|
||||
var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
|
||||
{
|
||||
NoDelay = true,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await socket.ConnectAsync(new IPEndPoint(address, context.DnsEndPoint.Port), token);
|
||||
return new NetworkStream(socket, ownsSocket: true);
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
connectionErrors.Add(exception);
|
||||
socket.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
socket.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
Exception innerException = connectionErrors.Count == 1
|
||||
? connectionErrors[0]
|
||||
: new AggregateException(connectionErrors);
|
||||
throw new HttpRequestException($"Could not connect to a validated address for '{requestUri.Host}'.", innerException);
|
||||
}
|
||||
|
||||
private static HttpRequestMessage CreateRequest(Uri url)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
request.Headers.TryAddWithoutValidation("User-Agent", USER_AGENT);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xhtml+xml"));
|
||||
request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("en-US"));
|
||||
request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("en", 0.9));
|
||||
request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
|
||||
request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
|
||||
request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("br"));
|
||||
request.Headers.TryAddWithoutValidation("Upgrade-Insecure-Requests", "1");
|
||||
request.Headers.TryAddWithoutValidation("Sec-Fetch-Site", "none");
|
||||
request.Headers.TryAddWithoutValidation("Sec-Fetch-Mode", "navigate");
|
||||
request.Headers.TryAddWithoutValidation("Sec-Fetch-Dest", "document");
|
||||
request.Headers.TryAddWithoutValidation("Sec-Fetch-User", "?1");
|
||||
return request;
|
||||
}
|
||||
|
||||
private static bool IsRedirect(HttpStatusCode statusCode) => (int)statusCode is >= 300 and <= 399;
|
||||
|
||||
private static async Task<string> ReadContentAsStringWithLimitAsync(HttpContent content, int maxResponseBytes, CancellationToken token)
|
||||
{
|
||||
if (content.Headers.ContentLength is long contentLength && contentLength > maxResponseBytes)
|
||||
throw new HttpRequestException($"The response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
|
||||
|
||||
await using var stream = await content.ReadAsStreamAsync(token);
|
||||
await using var buffer = new MemoryStream();
|
||||
var chunk = new byte[8192];
|
||||
while (true)
|
||||
{
|
||||
var read = await stream.ReadAsync(chunk, token);
|
||||
if (read == 0)
|
||||
break;
|
||||
|
||||
if (buffer.Length + read > maxResponseBytes)
|
||||
throw new HttpRequestException($"The response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
|
||||
|
||||
buffer.Write(chunk, 0, read);
|
||||
}
|
||||
|
||||
var encoding = TryGetContentEncoding(content) ?? Encoding.UTF8;
|
||||
return encoding.GetString(buffer.ToArray());
|
||||
}
|
||||
|
||||
private static Encoding? TryGetContentEncoding(HttpContent content)
|
||||
{
|
||||
var charset = content.Headers.ContentType?.CharSet?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(charset))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return Encoding.GetEncoding(charset.Trim('"'));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string ExtractTitle(HtmlDocument document)
|
||||
{
|
||||
var title = document.DocumentNode.SelectSingleNode("//title")?.InnerText?.Trim();
|
||||
return WebUtility.HtmlDecode(title ?? string.Empty).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts HTML content to the Markdown format.
|
||||
/// </summary>
|
||||
@ -54,4 +278,21 @@ public sealed class HTMLParser
|
||||
var markdownConverter = new Converter(MARKDOWN_PARSER_CONFIG);
|
||||
return markdownConverter.Convert(html);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class HTMLParserWebPage
|
||||
{
|
||||
public required Uri RequestedUrl { get; init; }
|
||||
|
||||
public required Uri FinalUrl { get; init; }
|
||||
|
||||
public required string ContentType { get; init; }
|
||||
|
||||
public required HtmlDocument Document { get; init; }
|
||||
}
|
||||
|
||||
public enum ExternalWebAuthenticationMode
|
||||
{
|
||||
NONE,
|
||||
OS_DEFAULT_CREDENTIALS
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
using System.Globalization;
|
||||
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Services;
|
||||
@ -154,6 +156,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
message = TB("The SETTINGS table does not exist or is not a valid table.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryValidateMinimumProviderConfidenceConfiguration(settingsTable, out message))
|
||||
return false;
|
||||
|
||||
// Config: check for updates, and if so, how often?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UpdateInterval, this.Id, settingsTable, dryRun);
|
||||
@ -194,6 +199,31 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
// Config: global voice recording shortcut
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShortcutVoiceRecording, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: global tool availability
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.EnableTools, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.DisabledToolIds, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: minimum provider confidence per tool
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.MinimumProviderConfidenceByToolId, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: web search tool settings
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchBaseUrl, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchDefaultLanguage, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchDefaultSafeSearch, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchDefaultCategories, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchDefaultEngines, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchMaxResults, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchTimeoutSeconds, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchMaxTotalContentCharacters, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchMinContentCharactersPerResult, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchPageTimeoutSeconds, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchRetrievalTimeoutSeconds, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: read web page tool settings
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.ReadWebPageTimeoutSeconds, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.ReadWebPageMaxContentCharacters, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.ReadWebPageAllowedPrivateHosts, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: timeout for external HTTP requests
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.HttpClientTimeoutSeconds, this.Id, settingsTable, dryRun);
|
||||
|
||||
@ -283,6 +313,37 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryValidateMinimumProviderConfidenceConfiguration(LuaTable settingsTable, out string message)
|
||||
{
|
||||
const string SETTING_NAME = "DataTools.MinimumProviderConfidenceByToolId";
|
||||
message = string.Empty;
|
||||
if (!settingsTable.TryGetValue(SETTING_NAME, out var configuredValue))
|
||||
return true;
|
||||
|
||||
if (configuredValue.Type is not LuaValueType.Table || !configuredValue.TryRead<LuaTable>(out var configuredTable))
|
||||
{
|
||||
message = $"The setting '{SETTING_NAME}' must be a table of tool IDs and confidence levels.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var previousKey = LuaValue.Nil;
|
||||
while (configuredTable.TryGetNext(previousKey, out var pair))
|
||||
{
|
||||
previousKey = pair.Key;
|
||||
if (!pair.Key.TryRead<string>(out var toolId) || string.IsNullOrWhiteSpace(toolId) ||
|
||||
!pair.Value.TryRead<string>(out var configuredLevel) ||
|
||||
!Enum.TryParse<ConfidenceLevel>(configuredLevel, true, out var confidenceLevel) ||
|
||||
!Enum.IsDefined(confidenceLevel) ||
|
||||
confidenceLevel is ConfidenceLevel.UNKNOWN)
|
||||
{
|
||||
message = $"The setting '{SETTING_NAME}' contains an invalid tool ID or confidence level. Allowed confidence levels are NONE, UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, and HIGH.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void TryProcessEnterpriseApprovedAssistantPlugins(LuaTable settingsTable, bool dryRun)
|
||||
{
|
||||
if (!ManagedConfiguration.TryGet(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, out ConfigMeta<DataAssistantPluginAudit, IList<DataAssistantPluginEnterpriseApproval>> configMeta))
|
||||
|
||||
@ -292,6 +292,61 @@ public static partial class PluginFactory
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShortcutVoiceRecording, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for global tool availability:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.EnableTools, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.DisabledToolIds, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for minimum provider confidence per tool:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.MinimumProviderConfidenceByToolId, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for web search tool settings:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchBaseUrl, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchDefaultLanguage, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchDefaultSafeSearch, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchDefaultCategories, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchDefaultEngines, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchMaxResults, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchTimeoutSeconds, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchMaxTotalContentCharacters, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchMinContentCharactersPerResult, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchPageTimeoutSeconds, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchRetrievalTimeoutSeconds, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for read web page tool settings:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.ReadWebPageTimeoutSeconds, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.ReadWebPageMaxContentCharacters, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.ReadWebPageAllowedPrivateHosts, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for the external HTTP client timeout:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.HttpClientTimeoutSeconds, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
@ -34,4 +34,9 @@ public enum SecretStoreType
|
||||
/// Data source secrets. Uses the "data-source::" prefix.
|
||||
/// </summary>
|
||||
DATA_SOURCE,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tool setting secrets. Uses the "tool::" prefix.
|
||||
/// </summary>
|
||||
TOOL_SETTINGS,
|
||||
}
|
||||
|
||||
@ -17,7 +17,8 @@ public static class SecretStoreTypeExtensions
|
||||
SecretStoreType.TRANSCRIPTION_PROVIDER => "transcription",
|
||||
SecretStoreType.IMAGE_PROVIDER => "image",
|
||||
SecretStoreType.DATA_SOURCE => "data-source",
|
||||
SecretStoreType.TOOL_SETTINGS => "tool",
|
||||
|
||||
_ => "provider",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
public interface IToolImplementation
|
||||
{
|
||||
public string ImplementationKey { get; }
|
||||
|
||||
public string Icon => Icons.Material.Filled.Build;
|
||||
|
||||
public IReadOnlySet<string> SensitiveTraceArgumentNames { get; }
|
||||
|
||||
public string GetDisplayName() => this.T("Tool");
|
||||
|
||||
public string GetDescription() => this.T("Tool description");
|
||||
|
||||
public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) =>
|
||||
this.T(fieldDefinition.Title);
|
||||
|
||||
public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) =>
|
||||
this.T(fieldDefinition.Description);
|
||||
|
||||
public string? GetSettingsFieldDefaultValue(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => null;
|
||||
|
||||
public Task<ToolConfigurationState?> ValidateConfigurationAsync(
|
||||
ToolDefinition definition,
|
||||
IReadOnlyDictionary<string, string> settingsValues,
|
||||
CancellationToken token = default) => Task.FromResult<ToolConfigurationState?>(null);
|
||||
|
||||
public Task<ToolExecutionResult> ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default);
|
||||
|
||||
public string FormatTraceResult(string rawResult) => rawResult;
|
||||
|
||||
private string T(string fallbackEN) => I18N.I.T(fallbackEN, this.GetType().Namespace, this.GetType().Name);
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
internal static class MarkdownTruncator
|
||||
{
|
||||
public static string Truncate(string markdown, int maxCharacters)
|
||||
{
|
||||
const string TRUNCATION_MARKER = "[Page content truncated]";
|
||||
if (maxCharacters <= TRUNCATION_MARKER.Length)
|
||||
return markdown[..maxCharacters];
|
||||
|
||||
var contentLimit = maxCharacters - TRUNCATION_MARKER.Length - 2;
|
||||
var breakPosition = markdown.LastIndexOf("\n\n", contentLimit, StringComparison.Ordinal);
|
||||
if (breakPosition < contentLimit / 2)
|
||||
breakPosition = markdown.LastIndexOf('\n', contentLimit);
|
||||
if (breakPosition < contentLimit / 2)
|
||||
breakPosition = contentLimit;
|
||||
|
||||
return $"{markdown[..breakPosition].TrimEnd()}\n\n{TRUNCATION_MARKER}";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
public readonly record struct ToolCallingAvailability(bool IsAvailable, string Message)
|
||||
{
|
||||
public static ToolCallingAvailability Available() => new(true, string.Empty);
|
||||
}
|
||||
|
||||
public static class ToolCallingAvailabilityExtensions
|
||||
{
|
||||
public static ToolCallingAvailability GetToolCallingAvailability(this AIStudio.Settings.Provider provider)
|
||||
{
|
||||
if (provider == AIStudio.Settings.Provider.NONE || provider.UsedLLMProvider is LLMProviders.NONE)
|
||||
return new(false, I18N.I.T("Please select an LLM provider.", typeof(ToolCallingAvailabilityExtensions).Namespace, nameof(ToolCallingAvailabilityExtensions)));
|
||||
|
||||
if (provider.UsedLLMProvider is LLMProviders.ANTHROPIC)
|
||||
return new(false, I18N.I.T("Tool calling for this provider is not implemented yet.", typeof(ToolCallingAvailabilityExtensions).Namespace, nameof(ToolCallingAvailabilityExtensions)));
|
||||
|
||||
var modelCapabilities = provider.GetModelCapabilities();
|
||||
var supportsRequiredApis =
|
||||
modelCapabilities.Contains(Capability.CHAT_COMPLETION_API) ||
|
||||
modelCapabilities.Contains(Capability.RESPONSES_API);
|
||||
|
||||
if (!supportsRequiredApis || !modelCapabilities.Contains(Capability.FUNCTION_CALLING))
|
||||
return new(false, I18N.I.T("The selected model does not support tool calling.", typeof(ToolCallingAvailabilityExtensions).Namespace, nameof(ToolCallingAvailabilityExtensions)));
|
||||
|
||||
return ToolCallingAvailability.Available();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,299 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Web;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations;
|
||||
|
||||
public sealed class ReadWebPageTool(WebPageRetrievalService webPageRetrievalService, ILogger<ReadWebPageTool> logger) : IToolImplementation
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool));
|
||||
|
||||
private const int DEFAULT_TIMEOUT_SECONDS = 30;
|
||||
private const int DEFAULT_MAX_CONTENT_CHARACTERS = 30000;
|
||||
private const int MAX_TIMEOUT_SECONDS = 60;
|
||||
private const int MAX_CONTENT_CHARACTERS = 50000;
|
||||
private const int MAX_TRACE_LENGTH = 12000;
|
||||
private const string ALLOWED_PRIVATE_HOSTS_SETTING = "allowedPrivateHosts";
|
||||
|
||||
public string ImplementationKey => ToolSelectionRules.READ_WEB_PAGE_TOOL_ID;
|
||||
|
||||
public string Icon => Icons.Material.Filled.Article;
|
||||
|
||||
public IReadOnlySet<string> SensitiveTraceArgumentNames => new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
public static string GetDisplayName() => TB("Read Web Page");
|
||||
|
||||
public string GetDescription() => TB("Load a web page and extract its readable content, links, and page details.");
|
||||
|
||||
public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch
|
||||
{
|
||||
"timeoutSeconds" => TB("Timeout Seconds"),
|
||||
"maxContentCharacters" => TB("Maximum Content Characters"),
|
||||
ALLOWED_PRIVATE_HOSTS_SETTING => TB("Allowed Private Hosts"),
|
||||
_ => TB(fieldDefinition.Title),
|
||||
};
|
||||
|
||||
public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch
|
||||
{
|
||||
"timeoutSeconds" => TB("(Optional) HTTP timeout for loading a web page in seconds."),
|
||||
"maxContentCharacters" => TB("(Optional) Global truncation limit for extracted characters returned to the model."),
|
||||
ALLOWED_PRIVATE_HOSTS_SETTING => TB("(Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication."),
|
||||
_ => TB(fieldDefinition.Description),
|
||||
};
|
||||
|
||||
public string? GetSettingsFieldDefaultValue(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch
|
||||
{
|
||||
"timeoutSeconds" => DEFAULT_TIMEOUT_SECONDS.ToString(),
|
||||
"maxContentCharacters" => DEFAULT_MAX_CONTENT_CHARACTERS.ToString(),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
public Task<ToolConfigurationState?> ValidateConfigurationAsync(
|
||||
ToolDefinition definition,
|
||||
IReadOnlyDictionary<string, string> settingsValues,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var positiveIntegerErrorFormat = TB("The setting '{0}' must be a positive integer.");
|
||||
if (!ToolSettingsValueParser.TryReadOptionalPositiveInt(settingsValues, "timeoutSeconds", positiveIntegerErrorFormat, out _, out var timeoutError))
|
||||
{
|
||||
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
Message = timeoutError,
|
||||
});
|
||||
}
|
||||
|
||||
if (!ToolSettingsValueParser.TryReadOptionalPositiveInt(settingsValues, "maxContentCharacters", positiveIntegerErrorFormat, out _, out var contentError))
|
||||
{
|
||||
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
Message = contentError,
|
||||
});
|
||||
}
|
||||
|
||||
if (!TryReadAllowedPrivateHostPatterns(settingsValues.GetValueOrDefault(ALLOWED_PRIVATE_HOSTS_SETTING), out _, out var allowlistError))
|
||||
{
|
||||
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
Message = allowlistError,
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult<ToolConfigurationState?>(null);
|
||||
}
|
||||
|
||||
public async Task<ToolExecutionResult> ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default)
|
||||
{
|
||||
var urlText = ReadRequiredString(arguments, "url");
|
||||
if (!Uri.TryCreate(urlText, UriKind.Absolute, out var url) || url is not { Scheme: "http" or "https" })
|
||||
throw new ArgumentException("Argument 'url' must be a valid HTTP or HTTPS URL.");
|
||||
|
||||
var timeoutSeconds = Math.Min(ToolSettingsValueParser.ReadOptionalPositiveInt(context.SettingsValues, "timeoutSeconds") ?? DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS);
|
||||
var maxContentCharacters = Math.Min(ToolSettingsValueParser.ReadOptionalPositiveInt(context.SettingsValues, "maxContentCharacters") ?? DEFAULT_MAX_CONTENT_CHARACTERS, MAX_CONTENT_CHARACTERS);
|
||||
if (!TryReadAllowedPrivateHostPatterns(context.SettingsValues.GetValueOrDefault(ALLOWED_PRIVATE_HOSTS_SETTING), out var allowedPrivateHosts, out var allowlistError))
|
||||
throw new InvalidOperationException(allowlistError);
|
||||
RetrievedWebPage retrievedPage;
|
||||
try
|
||||
{
|
||||
retrievedPage = await webPageRetrievalService.RetrieveAsync(
|
||||
url,
|
||||
new WebPageRetrievalOptions
|
||||
{
|
||||
TimeoutSeconds = timeoutSeconds,
|
||||
ProviderConfidence = context.ProviderConfidence,
|
||||
UseOsSso = true,
|
||||
IsPrivateHostAllowed = host => IsAllowedPrivateHost(host, allowedPrivateHosts),
|
||||
OnPrivateHostProviderBlockAsync = this.ReportPrivateHostProviderBlockAsync,
|
||||
},
|
||||
token);
|
||||
}
|
||||
catch (WebPageAccessBlockedException exception)
|
||||
{
|
||||
throw new ToolExecutionBlockedException(exception.Message);
|
||||
}
|
||||
var page = retrievedPage.Page;
|
||||
var extractedPage = retrievedPage.ExtractedPage;
|
||||
var markdown = extractedPage.Markdown;
|
||||
var originalContentCharacters = markdown.Length;
|
||||
List<string> warnings = [];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(markdown))
|
||||
warnings.Add("No readable static page content was extracted. The page may require JavaScript, authentication, or browser cookies.");
|
||||
else if (markdown.Length < 500)
|
||||
warnings.Add("Only a small amount of readable page content was extracted; the result may be incomplete.");
|
||||
|
||||
var contentTruncated = false;
|
||||
if (markdown.Length > maxContentCharacters)
|
||||
{
|
||||
markdown = MarkdownTruncator.Truncate(markdown, maxContentCharacters);
|
||||
contentTruncated = true;
|
||||
warnings.Add($"The extracted page content was truncated from {originalContentCharacters} to {markdown.Length} characters.");
|
||||
}
|
||||
|
||||
return new ToolExecutionResult
|
||||
{
|
||||
JsonContent = BuildModelContent(page, extractedPage, retrievedPage.RetrievedAtUtc, markdown, originalContentCharacters, contentTruncated, warnings),
|
||||
RequiredProviderConfidence = retrievedPage.RequiredProviderConfidence,
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonNode? BuildModelContent(
|
||||
HTMLParserWebPage page,
|
||||
ExtractedWebPage extractedPage,
|
||||
DateTimeOffset retrievedAtUtc,
|
||||
string websiteContentAsMarkdown,
|
||||
int originalContentCharacters,
|
||||
bool contentTruncated,
|
||||
IReadOnlyList<string> warnings)
|
||||
{
|
||||
var metadata = new JsonObject
|
||||
{
|
||||
};
|
||||
|
||||
var status = string.IsNullOrWhiteSpace(websiteContentAsMarkdown)
|
||||
? "empty response"
|
||||
: contentTruncated || originalContentCharacters < 500
|
||||
? "partial"
|
||||
: "complete";
|
||||
var warningArray = new JsonArray();
|
||||
foreach (var warning in warnings)
|
||||
warningArray.Add(warning);
|
||||
|
||||
AddIfNotEmpty(metadata, "language", extractedPage.Language);
|
||||
AddIfNotEmpty(metadata, "published_time", extractedPage.PublishedTime);
|
||||
AddIfNotEmpty(metadata, "modified_time", extractedPage.ModifiedTime);
|
||||
AddIfNotEmpty(metadata, "media_type", page.ContentType);
|
||||
metadata["warnings"] = warningArray;
|
||||
if (contentTruncated)
|
||||
{
|
||||
metadata["original_content_characters"] = originalContentCharacters;
|
||||
metadata["returned_content_characters"] = websiteContentAsMarkdown.Length;
|
||||
}
|
||||
|
||||
var content = new JsonObject
|
||||
{
|
||||
["text_content"] = websiteContentAsMarkdown,
|
||||
};
|
||||
|
||||
AddIfNotEmpty(content, "title", extractedPage.Title);
|
||||
AddIfNotEmpty(content, "description", extractedPage.Description);
|
||||
AddStringArrayIfNotEmpty(content, "authors", extractedPage.Authors);
|
||||
|
||||
|
||||
var result = new JsonObject
|
||||
{
|
||||
["url"] = page.RequestedUrl.ToString(),
|
||||
["status"] = status,
|
||||
["retrieved_at_utc"] = retrievedAtUtc.ToString("O"),
|
||||
["content"] = content,
|
||||
["metadata"] = metadata,
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AddIfNotEmpty(JsonObject target, string propertyName, string? value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
target[propertyName] = value;
|
||||
}
|
||||
|
||||
private static void AddStringArrayIfNotEmpty(JsonObject target, string propertyName, IReadOnlyList<string> values)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
return;
|
||||
|
||||
var array = new JsonArray();
|
||||
foreach (var value in values)
|
||||
array.Add(value);
|
||||
target[propertyName] = array;
|
||||
}
|
||||
|
||||
public string FormatTraceResult(string rawResult)
|
||||
{
|
||||
if (rawResult.Length <= MAX_TRACE_LENGTH)
|
||||
return rawResult;
|
||||
|
||||
return $"{rawResult[..MAX_TRACE_LENGTH]}...";
|
||||
}
|
||||
|
||||
private async Task ReportPrivateHostProviderBlockAsync(Uri url, ConfidenceLevel providerConfidence)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Blocked read_web_page access to allowed private host '{Host}' because provider confidence '{ProviderConfidence}' is below HIGH.",
|
||||
url.Host,
|
||||
providerConfidence);
|
||||
|
||||
await MessageBus.INSTANCE.SendError(new DataErrorMessage(
|
||||
Icons.Material.Filled.Security,
|
||||
TB("The web page was not loaded because private or VPN web pages require a High-confidence provider.")));
|
||||
}
|
||||
|
||||
private static bool IsAllowedPrivateHost(string host, IReadOnlyList<AllowedPrivateHostPattern> allowedPrivateHosts)
|
||||
{
|
||||
var normalizedHost = WebHostHelper.Normalize(host);
|
||||
return allowedPrivateHosts.Any(pattern => pattern.IsMatch(normalizedHost));
|
||||
}
|
||||
|
||||
private static bool TryReadAllowedPrivateHostPatterns(
|
||||
string? rawValue,
|
||||
out List<AllowedPrivateHostPattern> patterns,
|
||||
out string error)
|
||||
{
|
||||
patterns = [];
|
||||
error = string.Empty;
|
||||
|
||||
foreach (var rawPattern in SplitAllowedPrivateHostPatterns(rawValue))
|
||||
{
|
||||
var pattern = WebHostHelper.Normalize(rawPattern);
|
||||
if (pattern.Contains("://", StringComparison.Ordinal) || pattern.Contains('/'))
|
||||
{
|
||||
error = TB("Allowed private hosts must be host names only, without scheme or path.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var isWildcard = pattern.StartsWith("*.", StringComparison.Ordinal);
|
||||
var host = isWildcard ? pattern[2..] : pattern;
|
||||
if (string.IsNullOrWhiteSpace(host) || Uri.CheckHostName(host) is UriHostNameType.Unknown)
|
||||
{
|
||||
error = string.Format(TB("Allowed private host '{0}' is not valid."), rawPattern);
|
||||
return false;
|
||||
}
|
||||
|
||||
patterns.Add(new AllowedPrivateHostPattern(host, isWildcard));
|
||||
}
|
||||
|
||||
patterns = patterns
|
||||
.Distinct()
|
||||
.ToList();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> SplitAllowedPrivateHostPatterns(string? rawValue) => rawValue?
|
||||
.Split(['\r', '\n', ',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x)) ?? [];
|
||||
|
||||
private static string ReadRequiredString(JsonElement arguments, string propertyName)
|
||||
{
|
||||
if (!arguments.TryGetProperty(propertyName, out var value) || value.ValueKind is not JsonValueKind.String)
|
||||
throw new ArgumentException($"Missing required argument '{propertyName}'.");
|
||||
|
||||
var text = value.GetString()?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
throw new ArgumentException($"Missing required argument '{propertyName}'.");
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private readonly record struct AllowedPrivateHostPattern(string Host, bool IsWildcard)
|
||||
{
|
||||
public bool IsMatch(string normalizedHost) =>
|
||||
this.IsWildcard
|
||||
? normalizedHost.EndsWith($".{this.Host}", StringComparison.Ordinal) && normalizedHost.Length > this.Host.Length + 1
|
||||
: normalizedHost.Equals(this.Host, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,156 @@
|
||||
using AIStudio.Tools.Web;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations;
|
||||
|
||||
internal sealed class SearXNGPageRetrievalService(WebPageRetrievalService webPageRetrievalService)
|
||||
{
|
||||
private const int MAX_PARALLEL_RETRIEVALS = 4;
|
||||
|
||||
public async Task<WebSearchPageRetrievalResult> RetrieveAsync(
|
||||
IReadOnlyList<SearchCandidate> candidates,
|
||||
int pageTimeoutSeconds,
|
||||
int retrievalTimeoutSeconds,
|
||||
int maxTotalContentCharacters,
|
||||
int minContentCharactersPerResult,
|
||||
CancellationToken token)
|
||||
{
|
||||
var attemptedCount = 0;
|
||||
var blockedCount = 0;
|
||||
var pageTimedOutCount = 0;
|
||||
var failedCount = 0;
|
||||
var emptyContentCount = 0;
|
||||
var retrievalTimedOut = 0;
|
||||
using var retrievalTimeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
retrievalTimeoutCts.CancelAfter(TimeSpan.FromSeconds(retrievalTimeoutSeconds));
|
||||
using var retrievalSemaphore = new SemaphoreSlim(MAX_PARALLEL_RETRIEVALS);
|
||||
|
||||
async Task<RetrievedSearchPage?> RetrieveCandidateAsync(SearchCandidate candidate)
|
||||
{
|
||||
var enteredSemaphore = false;
|
||||
try
|
||||
{
|
||||
await retrievalSemaphore.WaitAsync(retrievalTimeoutCts.Token);
|
||||
enteredSemaphore = true;
|
||||
Interlocked.Increment(ref attemptedCount);
|
||||
var retrievedPage = await webPageRetrievalService.RetrieveAsync(
|
||||
candidate.RetrievalUrl,
|
||||
new WebPageRetrievalOptions
|
||||
{
|
||||
TimeoutSeconds = pageTimeoutSeconds,
|
||||
PublicTargetsOnly = true,
|
||||
},
|
||||
retrievalTimeoutCts.Token);
|
||||
if (string.IsNullOrWhiteSpace(retrievedPage.ExtractedPage.Markdown))
|
||||
{
|
||||
Interlocked.Increment(ref emptyContentCount);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RetrievedSearchPage(candidate, retrievedPage);
|
||||
}
|
||||
catch (OperationCanceledException) when (!token.IsCancellationRequested)
|
||||
{
|
||||
Interlocked.Exchange(ref retrievalTimedOut, 1);
|
||||
return null;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (WebPageAccessBlockedException)
|
||||
{
|
||||
Interlocked.Increment(ref blockedCount);
|
||||
return null;
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
Interlocked.Increment(ref pageTimedOutCount);
|
||||
return null;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
Interlocked.Increment(ref failedCount);
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (enteredSemaphore)
|
||||
retrievalSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
var retrievedPages = await Task.WhenAll(candidates.Select(RetrieveCandidateAsync));
|
||||
token.ThrowIfCancellationRequested();
|
||||
var mergedResults = MergeFinalUrlDuplicates(retrievedPages.OfType<RetrievedSearchPage>());
|
||||
ApplyContentBudget(mergedResults, maxTotalContentCharacters, minContentCharactersPerResult);
|
||||
var statistics = new WebSearchPageRetrievalStatistics(
|
||||
attemptedCount,
|
||||
blockedCount,
|
||||
pageTimedOutCount,
|
||||
failedCount,
|
||||
emptyContentCount);
|
||||
return new WebSearchPageRetrievalResult(mergedResults, retrievalTimedOut == 1, statistics);
|
||||
}
|
||||
|
||||
private static List<WebSearchPageResult> MergeFinalUrlDuplicates(IEnumerable<RetrievedSearchPage> retrievedPages) => retrievedPages
|
||||
.GroupBy(result => SearXNGSearchClient.NormalizeUrl(result.RetrievedPage.Page.FinalUrl), StringComparer.Ordinal)
|
||||
.Select(group =>
|
||||
{
|
||||
var rankedGroup = group.OrderBy(result => result.Candidate.Rank).ToList();
|
||||
var metadata = rankedGroup[0].Candidate.Clone();
|
||||
foreach (var duplicate in rankedGroup.Skip(1))
|
||||
metadata.Merge(duplicate.Candidate);
|
||||
|
||||
return new WebSearchPageResult(metadata, rankedGroup[0].RetrievedPage);
|
||||
})
|
||||
.OrderBy(result => result.Candidate.Rank)
|
||||
.ToList();
|
||||
|
||||
private static void ApplyContentBudget(List<WebSearchPageResult> results, int maxTotalContentCharacters, int minContentCharactersPerResult)
|
||||
{
|
||||
var remainingBudget = maxTotalContentCharacters;
|
||||
for (var index = 0; index < results.Count; index++)
|
||||
{
|
||||
var result = results[index];
|
||||
var originalMarkdown = result.RetrievedPage.ExtractedPage.Markdown;
|
||||
var remainingResults = results.Count - index - 1;
|
||||
var currentBudget = remainingBudget - minContentCharactersPerResult * remainingResults;
|
||||
if (originalMarkdown.Length > currentBudget)
|
||||
{
|
||||
result.ReturnedMarkdown = MarkdownTruncator.Truncate(originalMarkdown, currentBudget);
|
||||
result.ContentTruncated = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ReturnedMarkdown = originalMarkdown;
|
||||
}
|
||||
|
||||
remainingBudget -= result.ReturnedMarkdown.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record RetrievedSearchPage(SearchCandidate Candidate, RetrievedWebPage RetrievedPage);
|
||||
}
|
||||
|
||||
internal sealed record WebSearchPageRetrievalResult(
|
||||
IReadOnlyList<WebSearchPageResult> Results,
|
||||
bool RetrievalTimedOut,
|
||||
WebSearchPageRetrievalStatistics ErrorStatistics);
|
||||
|
||||
internal sealed record WebSearchPageRetrievalStatistics(
|
||||
int AttemptedCount,
|
||||
int BlockedCount,
|
||||
int PageTimedOutCount,
|
||||
int FailedCount,
|
||||
int EmptyContentCount);
|
||||
|
||||
internal sealed class WebSearchPageResult(SearchCandidate candidate, RetrievedWebPage retrievedPage)
|
||||
{
|
||||
public SearchCandidate Candidate { get; } = candidate;
|
||||
|
||||
public RetrievedWebPage RetrievedPage { get; } = retrievedPage;
|
||||
|
||||
public string ReturnedMarkdown { get; set; } = string.Empty;
|
||||
|
||||
public bool ContentTruncated { get; set; }
|
||||
}
|
||||
@ -0,0 +1,366 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using AIStudio.Tools;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations;
|
||||
|
||||
internal sealed class SearXNGSearchClient
|
||||
{
|
||||
private const int MAX_RESPONSE_BYTES = 1024 * 1024;
|
||||
|
||||
public async Task<SearXNGSearchResponse> SearchAsync(SearXNGSearchRequest searchRequest, CancellationToken token)
|
||||
{
|
||||
var queryParameters = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new("q", searchRequest.Query),
|
||||
new("format", "json"),
|
||||
};
|
||||
|
||||
if (searchRequest.Categories.Count > 0)
|
||||
queryParameters.Add(new KeyValuePair<string, string>("categories", string.Join(",", searchRequest.Categories)));
|
||||
|
||||
if (searchRequest.Engines.Count > 0)
|
||||
queryParameters.Add(new KeyValuePair<string, string>("engines", string.Join(",", searchRequest.Engines)));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchRequest.Language))
|
||||
queryParameters.Add(new KeyValuePair<string, string>("language", searchRequest.Language));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchRequest.TimeRange))
|
||||
queryParameters.Add(new KeyValuePair<string, string>("time_range", searchRequest.TimeRange));
|
||||
|
||||
if (searchRequest.Page is not null)
|
||||
queryParameters.Add(new KeyValuePair<string, string>("pageno", searchRequest.Page.Value.ToString()));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchRequest.SafeSearch))
|
||||
queryParameters.Add(new KeyValuePair<string, string>("safesearch", searchRequest.SafeSearch));
|
||||
|
||||
using var httpClient = ExternalHttpClientTimeout.CreateHttpClient(searchRequest.SearchUri, ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED);
|
||||
httpClient.Timeout = Timeout.InfiniteTimeSpan;
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, BuildRequestUri(searchRequest.SearchUri, queryParameters));
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(searchRequest.TimeoutSeconds));
|
||||
|
||||
using var response = await SendAsync(httpClient, request, timeoutCts.Token, searchRequest.TimeoutSeconds, token);
|
||||
var responseBody = await ReadContentAsStringWithLimitAsync(response.Content, MAX_RESPONSE_BYTES, timeoutCts.Token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var responseDetails = string.IsNullOrWhiteSpace(responseBody) ? string.Empty : $" Response body: {responseBody[..Math.Min(responseBody.Length, 400)]}";
|
||||
throw new InvalidOperationException($"The SearXNG request failed with status code {(int)response.StatusCode} ({response.StatusCode}).{responseDetails}");
|
||||
}
|
||||
|
||||
JsonNode? responseJson;
|
||||
try
|
||||
{
|
||||
responseJson = JsonNode.Parse(responseBody);
|
||||
}
|
||||
catch (JsonException exception)
|
||||
{
|
||||
throw new InvalidOperationException($"The SearXNG response was not valid JSON: {exception.Message}", exception);
|
||||
}
|
||||
|
||||
if (responseJson is not JsonObject responseObject)
|
||||
throw new InvalidOperationException("The SearXNG response JSON must be an object.");
|
||||
|
||||
var candidates = BuildCandidates(responseObject["results"] as JsonArray, searchRequest.EffectiveLimit, out var candidateCount);
|
||||
return new SearXNGSearchResponse(candidates, candidateCount);
|
||||
}
|
||||
|
||||
public static bool TryNormalizeSearchUri(
|
||||
string rawUrl,
|
||||
string requiredUrlError,
|
||||
string invalidAbsoluteUrlError,
|
||||
string unsupportedSchemeError,
|
||||
out Uri searchUri,
|
||||
out string error)
|
||||
{
|
||||
searchUri = null!;
|
||||
error = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rawUrl))
|
||||
{
|
||||
error = requiredUrlError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(rawUrl.Trim(), UriKind.Absolute, out var parsedUri))
|
||||
{
|
||||
error = invalidAbsoluteUrlError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parsedUri.Scheme is not ("http" or "https"))
|
||||
{
|
||||
error = unsupportedSchemeError;
|
||||
return false;
|
||||
}
|
||||
|
||||
var basePath = parsedUri.AbsolutePath.TrimEnd('/');
|
||||
if (basePath.EndsWith("/search", StringComparison.OrdinalIgnoreCase))
|
||||
basePath = basePath[..^"/search".Length];
|
||||
|
||||
var builder = new UriBuilder(parsedUri)
|
||||
{
|
||||
Path = $"{basePath}/search",
|
||||
Query = string.Empty,
|
||||
Fragment = string.Empty,
|
||||
};
|
||||
searchUri = builder.Uri;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<SearchCandidate> BuildCandidates(JsonArray? resultArray, int effectiveLimit, out int candidateCount)
|
||||
{
|
||||
var resultObjects = resultArray?.OfType<JsonObject>().ToList() ?? [];
|
||||
var hasSortableScores = resultObjects.Any(result => TryGetScore(result, out _));
|
||||
IEnumerable<JsonObject> orderedResults = hasSortableScores
|
||||
? resultObjects
|
||||
.OrderByDescending(result => TryGetScore(result, out var score) ? score : double.MinValue)
|
||||
.ThenBy(result => result["title"]?.ToString(), StringComparer.OrdinalIgnoreCase)
|
||||
: resultObjects;
|
||||
var rankedResults = orderedResults
|
||||
.Take(effectiveLimit)
|
||||
.ToList();
|
||||
candidateCount = rankedResults.Count;
|
||||
|
||||
var candidatesByUrl = new Dictionary<string, SearchCandidate>(StringComparer.Ordinal);
|
||||
for (var index = 0; index < rankedResults.Count; index++)
|
||||
{
|
||||
var result = rankedResults[index];
|
||||
var originalUrl = ReadNodeString(result["url"]);
|
||||
if (!Uri.TryCreate(originalUrl, UriKind.Absolute, out var url) || url is not { Scheme: "http" or "https" })
|
||||
continue;
|
||||
|
||||
var retrievalUrl = RemoveFragment(url);
|
||||
var candidate = new SearchCandidate
|
||||
{
|
||||
Rank = index + 1,
|
||||
RetrievalUrl = retrievalUrl,
|
||||
OriginalUrls = [originalUrl],
|
||||
Title = ReadNodeString(result["title"]),
|
||||
Snippet = ReadNodeString(result["content"]),
|
||||
Engines = ReadStringValues(result, "engine", "engines"),
|
||||
Categories = ReadStringValues(result, "category", "categories"),
|
||||
PublishedDate = FirstNonEmpty(ReadNodeString(result["publishedDate"]), ReadNodeString(result["published_date"])),
|
||||
};
|
||||
var normalizedUrl = NormalizeUrl(retrievalUrl);
|
||||
if (candidatesByUrl.TryGetValue(normalizedUrl, out var existingCandidate))
|
||||
existingCandidate.Merge(candidate);
|
||||
else
|
||||
candidatesByUrl[normalizedUrl] = candidate;
|
||||
}
|
||||
|
||||
return candidatesByUrl.Values
|
||||
.OrderBy(candidate => candidate.Rank)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<string> ReadStringValues(JsonObject source, string singularPropertyName, string pluralPropertyName)
|
||||
{
|
||||
var values = new List<string>();
|
||||
AddNodeStringValues(source[singularPropertyName], values);
|
||||
AddNodeStringValues(source[pluralPropertyName], values);
|
||||
return values
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static void AddNodeStringValues(JsonNode? node, List<string> values)
|
||||
{
|
||||
if (node is JsonArray array)
|
||||
{
|
||||
foreach (var item in array)
|
||||
AddNodeStringValues(item, values);
|
||||
return;
|
||||
}
|
||||
|
||||
var value = ReadNodeString(node);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
values.Add(value);
|
||||
}
|
||||
|
||||
private static string ReadNodeString(JsonNode? node) => node is null ? string.Empty : node.ToString().Trim();
|
||||
|
||||
private static bool TryGetScore(JsonObject result, out double score)
|
||||
{
|
||||
score = double.MinValue;
|
||||
if (!result.TryGetPropertyValue("score", out var scoreNode) || scoreNode is null)
|
||||
return false;
|
||||
|
||||
return scoreNode switch
|
||||
{
|
||||
JsonValue value when value.TryGetValue<double>(out var doubleScore) => ReturnScore(doubleScore, out score),
|
||||
JsonValue value when value.TryGetValue<decimal>(out var decimalScore) => ReturnScore((double)decimalScore, out score),
|
||||
JsonValue value when value.TryGetValue<int>(out var intScore) => ReturnScore(intScore, out score),
|
||||
_ => double.TryParse(scoreNode.ToString(), out var parsedScore) && ReturnScore(parsedScore, out score),
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ReturnScore(double input, out double score)
|
||||
{
|
||||
score = input;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Uri BuildRequestUri(Uri searchUri, IEnumerable<KeyValuePair<string, string>> queryParameters)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
foreach (var parameter in queryParameters)
|
||||
{
|
||||
if (builder.Length > 0)
|
||||
builder.Append('&');
|
||||
|
||||
builder.Append(WebUtility.UrlEncode(parameter.Key));
|
||||
builder.Append('=');
|
||||
builder.Append(WebUtility.UrlEncode(parameter.Value));
|
||||
}
|
||||
|
||||
var uriBuilder = new UriBuilder(searchUri)
|
||||
{
|
||||
Query = builder.ToString(),
|
||||
};
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
private static async Task<string> ReadContentAsStringWithLimitAsync(HttpContent content, int maxResponseBytes, CancellationToken token)
|
||||
{
|
||||
if (content.Headers.ContentLength is long contentLength && contentLength > maxResponseBytes)
|
||||
throw new InvalidOperationException($"The SearXNG response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
|
||||
|
||||
await using var stream = await content.ReadAsStreamAsync(token);
|
||||
await using var buffer = new MemoryStream();
|
||||
var chunk = new byte[8192];
|
||||
while (true)
|
||||
{
|
||||
var read = await stream.ReadAsync(chunk, token);
|
||||
if (read == 0)
|
||||
break;
|
||||
|
||||
if (buffer.Length + read > maxResponseBytes)
|
||||
throw new InvalidOperationException($"The SearXNG response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
|
||||
|
||||
buffer.Write(chunk, 0, read);
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(buffer.ToArray());
|
||||
}
|
||||
|
||||
private static async Task<HttpResponseMessage> SendAsync(
|
||||
HttpClient httpClient,
|
||||
HttpRequestMessage request,
|
||||
CancellationToken requestToken,
|
||||
int timeoutSeconds,
|
||||
CancellationToken callerToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await httpClient.SendAsync(request, requestToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (!callerToken.IsCancellationRequested)
|
||||
{
|
||||
throw new TimeoutException($"The SearXNG request timed out after {timeoutSeconds} seconds.");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
throw new InvalidOperationException($"The SearXNG request failed: {exception.Message}", exception);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string NormalizeUrl(Uri url)
|
||||
{
|
||||
var scheme = url.Scheme.ToLowerInvariant();
|
||||
var host = url.IdnHost.TrimEnd('.').ToLowerInvariant();
|
||||
var port = url.IsDefaultPort ? string.Empty : $":{url.Port}";
|
||||
var userInfo = string.IsNullOrEmpty(url.UserInfo) ? string.Empty : $"{url.UserInfo}@";
|
||||
return $"{scheme}://{userInfo}{host}{port}{url.AbsolutePath}{url.Query}";
|
||||
}
|
||||
|
||||
internal static string FirstNonEmpty(params string[] values) => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;
|
||||
|
||||
private static Uri RemoveFragment(Uri url) => new UriBuilder(url)
|
||||
{
|
||||
Fragment = string.Empty,
|
||||
}.Uri;
|
||||
}
|
||||
|
||||
internal sealed record SearXNGSearchRequest(
|
||||
Uri SearchUri,
|
||||
string Query,
|
||||
IReadOnlyList<string> Categories,
|
||||
IReadOnlyList<string> Engines,
|
||||
string? Language,
|
||||
string? TimeRange,
|
||||
int? Page,
|
||||
string? SafeSearch,
|
||||
int EffectiveLimit,
|
||||
int TimeoutSeconds);
|
||||
|
||||
internal sealed record SearXNGSearchResponse(IReadOnlyList<SearchCandidate> Candidates, int CandidateCount);
|
||||
|
||||
internal sealed class SearchCandidate
|
||||
{
|
||||
public required int Rank { get; set; }
|
||||
|
||||
public required Uri RetrievalUrl { get; set; }
|
||||
|
||||
public required List<string> OriginalUrls { get; init; }
|
||||
|
||||
public required string Title { get; set; }
|
||||
|
||||
public required string Snippet { get; set; }
|
||||
|
||||
public required List<string> Engines { get; init; }
|
||||
|
||||
public required List<string> Categories { get; init; }
|
||||
|
||||
public required string PublishedDate { get; set; }
|
||||
|
||||
public SearchCandidate Clone() => new()
|
||||
{
|
||||
Rank = this.Rank,
|
||||
RetrievalUrl = this.RetrievalUrl,
|
||||
OriginalUrls = [..this.OriginalUrls],
|
||||
Title = this.Title,
|
||||
Snippet = this.Snippet,
|
||||
Engines = [..this.Engines],
|
||||
Categories = [..this.Categories],
|
||||
PublishedDate = this.PublishedDate,
|
||||
};
|
||||
|
||||
public void Merge(SearchCandidate candidate)
|
||||
{
|
||||
if (candidate.Rank < this.Rank)
|
||||
{
|
||||
this.Rank = candidate.Rank;
|
||||
this.RetrievalUrl = candidate.RetrievalUrl;
|
||||
this.Title = candidate.Title;
|
||||
this.Snippet = candidate.Snippet;
|
||||
this.PublishedDate = candidate.PublishedDate;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Title = SearXNGSearchClient.FirstNonEmpty(this.Title, candidate.Title);
|
||||
this.Snippet = SearXNGSearchClient.FirstNonEmpty(this.Snippet, candidate.Snippet);
|
||||
this.PublishedDate = SearXNGSearchClient.FirstNonEmpty(this.PublishedDate, candidate.PublishedDate);
|
||||
}
|
||||
|
||||
AddDistinct(this.OriginalUrls, candidate.OriginalUrls, StringComparer.Ordinal);
|
||||
AddDistinct(this.Engines, candidate.Engines, StringComparer.OrdinalIgnoreCase);
|
||||
AddDistinct(this.Categories, candidate.Categories, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static void AddDistinct(List<string> target, IEnumerable<string> values, StringComparer comparer)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (!target.Contains(value, comparer))
|
||||
target.Add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,393 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Web;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations;
|
||||
|
||||
public sealed class SearXNGWebSearchTool : IToolImplementation
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool));
|
||||
|
||||
private readonly SearXNGSearchClient searchClient = new();
|
||||
private readonly SearXNGPageRetrievalService pageRetrievalService;
|
||||
|
||||
private const int DEFAULT_MAX_RESULTS = 5;
|
||||
private const int DEFAULT_TIMEOUT_SECONDS = 20;
|
||||
private const int MAX_RESULTS = 20;
|
||||
private const int MAX_PAGE = 20;
|
||||
private const int MAX_TIMEOUT_SECONDS = 60;
|
||||
private const int MAX_TRACE_LENGTH = 4000;
|
||||
private const int DEFAULT_MAX_TOTAL_CONTENT_CHARACTERS = 100000;
|
||||
private const int DEFAULT_MIN_CONTENT_CHARACTERS_PER_RESULT = 3000;
|
||||
private const int DEFAULT_PAGE_TIMEOUT_SECONDS = 30;
|
||||
private const int DEFAULT_RETRIEVAL_TIMEOUT_SECONDS = 90;
|
||||
private const int MAX_TOTAL_CONTENT_CHARACTERS = 100000;
|
||||
private const int MAX_MIN_CONTENT_CHARACTERS_PER_RESULT = 3000;
|
||||
private const int MAX_PAGE_TIMEOUT_SECONDS = 30;
|
||||
private const int MAX_RETRIEVAL_TIMEOUT_SECONDS = 90;
|
||||
|
||||
public SearXNGWebSearchTool(WebPageRetrievalService webPageRetrievalService)
|
||||
{
|
||||
this.pageRetrievalService = new SearXNGPageRetrievalService(webPageRetrievalService);
|
||||
}
|
||||
|
||||
public string ImplementationKey => ToolSelectionRules.WEB_SEARCH_TOOL_ID;
|
||||
|
||||
public string Icon => Icons.Material.Filled.Language;
|
||||
|
||||
public IReadOnlySet<string> SensitiveTraceArgumentNames => new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
public string GetDisplayName() => TB("Web Search");
|
||||
|
||||
public string GetDescription() => TB("Search the web with a configured SearXNG instance and retrieve the readable content of the best matching pages.");
|
||||
|
||||
public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch
|
||||
{
|
||||
"baseUrl" => TB("SearXNG URL"),
|
||||
"defaultLanguage" => TB("Default Language"),
|
||||
"defaultSafeSearch" => TB("Default Safe Search"),
|
||||
"defaultCategories" => TB("Default Categories"),
|
||||
"defaultEngines" => TB("Default Engines"),
|
||||
"maxResults" => TB("Maximum Results"),
|
||||
"timeoutSeconds" => TB("Timeout Seconds"),
|
||||
"maxTotalContentCharacters" => TB("Maximum Total Content Characters"),
|
||||
"minContentCharactersPerResult" => TB("Minimum Content Characters Per Result"),
|
||||
"pageTimeoutSeconds" => TB("Page Timeout Seconds"),
|
||||
"retrievalTimeoutSeconds" => TB("Retrieval Timeout Seconds"),
|
||||
_ => TB(fieldDefinition.Title),
|
||||
};
|
||||
|
||||
public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch
|
||||
{
|
||||
"baseUrl" => TB("Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint."),
|
||||
"defaultLanguage" => TB("Optional fallback language code when the model does not provide a language."),
|
||||
"defaultSafeSearch" => TB("Optional safe search policy sent to SearXNG when configured."),
|
||||
"defaultCategories" => TB("Optional comma-separated default categories. Do not set this together with default engines."),
|
||||
"defaultEngines" => TB("Optional comma-separated default engines. Do not set this together with default categories."),
|
||||
"maxResults" => TB("Optional default maximum number of results returned to the model when the model does not provide a limit."),
|
||||
"timeoutSeconds" => TB("Optional HTTP timeout for the search request in seconds."),
|
||||
"maxTotalContentCharacters" => TB("Optional total character budget shared by all retrieved pages."),
|
||||
"minContentCharactersPerResult" => TB("Optional minimum character budget reserved for each successfully retrieved page."),
|
||||
"pageTimeoutSeconds" => TB("Optional timeout for loading each individual result page in seconds."),
|
||||
"retrievalTimeoutSeconds" => TB("Optional overall timeout for retrieving all result pages in seconds."),
|
||||
_ => TB(fieldDefinition.Description),
|
||||
};
|
||||
|
||||
public string? GetSettingsFieldDefaultValue(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch
|
||||
{
|
||||
"maxResults" => DEFAULT_MAX_RESULTS.ToString(),
|
||||
"timeoutSeconds" => DEFAULT_TIMEOUT_SECONDS.ToString(),
|
||||
"maxTotalContentCharacters" => DEFAULT_MAX_TOTAL_CONTENT_CHARACTERS.ToString(),
|
||||
"minContentCharactersPerResult" => DEFAULT_MIN_CONTENT_CHARACTERS_PER_RESULT.ToString(),
|
||||
"pageTimeoutSeconds" => DEFAULT_PAGE_TIMEOUT_SECONDS.ToString(),
|
||||
"retrievalTimeoutSeconds" => DEFAULT_RETRIEVAL_TIMEOUT_SECONDS.ToString(),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
public Task<ToolConfigurationState?> ValidateConfigurationAsync(
|
||||
ToolDefinition definition,
|
||||
IReadOnlyDictionary<string, string> settingsValues,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var positiveIntegerErrorFormat = TB("The setting '{0}' must be a positive integer.");
|
||||
var maximumErrorFormat = TB("The setting '{0}' must be less than or equal to {1}.");
|
||||
settingsValues.TryGetValue("baseUrl", out var baseUrl);
|
||||
if (!TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError))
|
||||
{
|
||||
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
Message = uriError,
|
||||
});
|
||||
}
|
||||
|
||||
var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories"));
|
||||
var hasDefaultEngines = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultEngines"));
|
||||
if (hasDefaultCategories && hasDefaultEngines)
|
||||
{
|
||||
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
Message = TB("Default categories and default engines cannot both be set for the web search tool."),
|
||||
});
|
||||
}
|
||||
|
||||
var defaultSafeSearch = settingsValues.GetValueOrDefault("defaultSafeSearch");
|
||||
if (!string.IsNullOrWhiteSpace(defaultSafeSearch) && defaultSafeSearch is not ("0" or "1" or "2"))
|
||||
{
|
||||
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
Message = TB("The default safe search setting must be 0, 1, or 2."),
|
||||
});
|
||||
}
|
||||
|
||||
if (!ToolSettingsValueParser.TryReadOptionalPositiveInt(settingsValues, "maxResults", positiveIntegerErrorFormat, out _, out var maxResultsError))
|
||||
{
|
||||
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
Message = maxResultsError,
|
||||
});
|
||||
}
|
||||
|
||||
if (!ToolSettingsValueParser.TryReadOptionalPositiveInt(settingsValues, "timeoutSeconds", positiveIntegerErrorFormat, out _, out var timeoutError))
|
||||
{
|
||||
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
Message = timeoutError,
|
||||
});
|
||||
}
|
||||
|
||||
if (!ToolSettingsValueParser.TryReadBoundedOptionalPositiveInt(settingsValues, "maxTotalContentCharacters", MAX_TOTAL_CONTENT_CHARACTERS, positiveIntegerErrorFormat, maximumErrorFormat, out var maxTotalContentCharacters, out var maxTotalContentError))
|
||||
{
|
||||
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
Message = maxTotalContentError,
|
||||
});
|
||||
}
|
||||
|
||||
if (!ToolSettingsValueParser.TryReadBoundedOptionalPositiveInt(settingsValues, "minContentCharactersPerResult", MAX_MIN_CONTENT_CHARACTERS_PER_RESULT, positiveIntegerErrorFormat, maximumErrorFormat, out var minContentCharactersPerResult, out var minContentError))
|
||||
{
|
||||
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
Message = minContentError,
|
||||
});
|
||||
}
|
||||
|
||||
if (!ToolSettingsValueParser.TryReadBoundedOptionalPositiveInt(settingsValues, "pageTimeoutSeconds", MAX_PAGE_TIMEOUT_SECONDS, positiveIntegerErrorFormat, maximumErrorFormat, out _, out var pageTimeoutError))
|
||||
{
|
||||
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
Message = pageTimeoutError,
|
||||
});
|
||||
}
|
||||
|
||||
if (!ToolSettingsValueParser.TryReadBoundedOptionalPositiveInt(settingsValues, "retrievalTimeoutSeconds", MAX_RETRIEVAL_TIMEOUT_SECONDS, positiveIntegerErrorFormat, maximumErrorFormat, out _, out var retrievalTimeoutError))
|
||||
{
|
||||
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
Message = retrievalTimeoutError,
|
||||
});
|
||||
}
|
||||
|
||||
var effectiveMaxTotalContentCharacters = maxTotalContentCharacters ?? DEFAULT_MAX_TOTAL_CONTENT_CHARACTERS;
|
||||
var effectiveMinContentCharactersPerResult = minContentCharactersPerResult ?? DEFAULT_MIN_CONTENT_CHARACTERS_PER_RESULT;
|
||||
if (effectiveMaxTotalContentCharacters < effectiveMinContentCharactersPerResult * MAX_RESULTS)
|
||||
{
|
||||
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
Message = string.Format(TB("The total content budget must reserve at least {0} characters for each of up to {1} results."), effectiveMinContentCharactersPerResult, MAX_RESULTS),
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult<ToolConfigurationState?>(null);
|
||||
}
|
||||
|
||||
public async Task<ToolExecutionResult> ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default)
|
||||
{
|
||||
context.SettingsValues.TryGetValue("baseUrl", out var baseUrl);
|
||||
if (!TryNormalizeSearchUri(baseUrl ?? string.Empty, out var searchUri, out var uriError))
|
||||
throw new InvalidOperationException(uriError);
|
||||
|
||||
var query = ReadRequiredString(arguments, "query");
|
||||
var categories = ReadOptionalStringArray(arguments, "categories");
|
||||
var engines = ReadOptionalStringArray(arguments, "engines");
|
||||
var language = ReadOptionalString(arguments, "language");
|
||||
var timeRange = ReadOptionalString(arguments, "time_range");
|
||||
var page = ReadOptionalPositiveInt(arguments, "page");
|
||||
var requestedLimit = ReadOptionalPositiveInt(arguments, "limit");
|
||||
|
||||
if (timeRange is not null && timeRange is not ("day" or "month" or "year"))
|
||||
throw new ArgumentException($"Invalid time_range '{timeRange}'.");
|
||||
|
||||
language = string.IsNullOrWhiteSpace(language) ? context.SettingsValues.GetValueOrDefault("defaultLanguage") : language;
|
||||
var safeSearch = context.SettingsValues.GetValueOrDefault("defaultSafeSearch");
|
||||
|
||||
if (categories.Count == 0)
|
||||
categories = SplitCommaSeparatedValues(context.SettingsValues.GetValueOrDefault("defaultCategories"));
|
||||
|
||||
if (engines.Count == 0)
|
||||
engines = SplitCommaSeparatedValues(context.SettingsValues.GetValueOrDefault("defaultEngines"));
|
||||
|
||||
if (categories.Count > 0 && engines.Count > 0 && !string.IsNullOrWhiteSpace(context.SettingsValues.GetValueOrDefault("defaultCategories")) && !string.IsNullOrWhiteSpace(context.SettingsValues.GetValueOrDefault("defaultEngines")))
|
||||
throw new InvalidOperationException(TB("Default categories and default engines cannot both be set for the web search tool."));
|
||||
|
||||
var defaultLimit = ToolSettingsValueParser.ReadOptionalPositiveInt(context.SettingsValues, "maxResults") ?? DEFAULT_MAX_RESULTS;
|
||||
var effectiveLimit = Math.Min(requestedLimit ?? defaultLimit, MAX_RESULTS);
|
||||
var timeoutSeconds = Math.Min(ToolSettingsValueParser.ReadOptionalPositiveInt(context.SettingsValues, "timeoutSeconds") ?? DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS);
|
||||
var maxTotalContentCharacters = Math.Min(ToolSettingsValueParser.ReadOptionalPositiveInt(context.SettingsValues, "maxTotalContentCharacters") ?? DEFAULT_MAX_TOTAL_CONTENT_CHARACTERS, MAX_TOTAL_CONTENT_CHARACTERS);
|
||||
var minContentCharactersPerResult = Math.Min(ToolSettingsValueParser.ReadOptionalPositiveInt(context.SettingsValues, "minContentCharactersPerResult") ?? DEFAULT_MIN_CONTENT_CHARACTERS_PER_RESULT, MAX_MIN_CONTENT_CHARACTERS_PER_RESULT);
|
||||
var pageTimeoutSeconds = Math.Min(ToolSettingsValueParser.ReadOptionalPositiveInt(context.SettingsValues, "pageTimeoutSeconds") ?? DEFAULT_PAGE_TIMEOUT_SECONDS, MAX_PAGE_TIMEOUT_SECONDS);
|
||||
var retrievalTimeoutSeconds = Math.Min(ToolSettingsValueParser.ReadOptionalPositiveInt(context.SettingsValues, "retrievalTimeoutSeconds") ?? DEFAULT_RETRIEVAL_TIMEOUT_SECONDS, MAX_RETRIEVAL_TIMEOUT_SECONDS);
|
||||
if (maxTotalContentCharacters < minContentCharactersPerResult * MAX_RESULTS)
|
||||
throw new InvalidOperationException(TB("The configured web search content budget is not valid."));
|
||||
if (page is > MAX_PAGE)
|
||||
throw new ArgumentException($"Argument 'page' must be less than or equal to {MAX_PAGE}.");
|
||||
|
||||
var searchResponse = await this.searchClient.SearchAsync(
|
||||
new SearXNGSearchRequest(
|
||||
searchUri,
|
||||
query,
|
||||
categories,
|
||||
engines,
|
||||
language,
|
||||
timeRange,
|
||||
page,
|
||||
safeSearch,
|
||||
effectiveLimit,
|
||||
timeoutSeconds),
|
||||
token);
|
||||
var retrievalResult = await this.pageRetrievalService.RetrieveAsync(
|
||||
searchResponse.Candidates,
|
||||
pageTimeoutSeconds,
|
||||
retrievalTimeoutSeconds,
|
||||
maxTotalContentCharacters,
|
||||
minContentCharactersPerResult,
|
||||
token);
|
||||
|
||||
var resultArray = new JsonArray();
|
||||
foreach (var result in retrievalResult.Results)
|
||||
resultArray.Add(BuildResultJson(result));
|
||||
|
||||
var resultObject = new JsonObject
|
||||
{
|
||||
["candidate_count"] = searchResponse.CandidateCount,
|
||||
["result_count"] = retrievalResult.Results.Count,
|
||||
["retrieval_timed_out"] = retrievalResult.RetrievalTimedOut,
|
||||
["results"] = resultArray,
|
||||
};
|
||||
if (retrievalResult.Results.Count == 0)
|
||||
resultObject["diagnostic"] = "No result page could be retrieved as readable public HTML. Pages may have failed, timed out, been blocked by network safety checks, used an unsupported content type, or contained no readable static content.";
|
||||
|
||||
return new ToolExecutionResult
|
||||
{
|
||||
JsonContent = resultObject
|
||||
};
|
||||
}
|
||||
|
||||
public string FormatTraceResult(string rawResult)
|
||||
{
|
||||
if (rawResult.Length <= MAX_TRACE_LENGTH)
|
||||
return rawResult;
|
||||
|
||||
return $"{rawResult[..MAX_TRACE_LENGTH]}...";
|
||||
}
|
||||
|
||||
private static JsonObject BuildResultJson(WebSearchPageResult result)
|
||||
{
|
||||
var extractedPage = result.RetrievedPage.ExtractedPage;
|
||||
var page = result.RetrievedPage.Page;
|
||||
var originalContentCharacters = extractedPage.Markdown.Length;
|
||||
var searchMetadata = new JsonObject
|
||||
{
|
||||
["rank"] = result.Candidate.Rank,
|
||||
["requested_url"] = page.RequestedUrl.ToString(),
|
||||
["final_url"] = page.FinalUrl.ToString(),
|
||||
["engines"] = BuildJsonArray(result.Candidate.Engines),
|
||||
["published_date"] = result.Candidate.PublishedDate,
|
||||
};
|
||||
var pageContent = new JsonObject
|
||||
{
|
||||
["status"] = result.ContentTruncated || originalContentCharacters < 500 ? "partial or truncated" : "complete",
|
||||
["title"] = extractedPage.Title,
|
||||
["description"] = extractedPage.Description,
|
||||
["authors"] = BuildJsonArray(extractedPage.Authors),
|
||||
["content"] = result.ReturnedMarkdown,
|
||||
};
|
||||
|
||||
return new JsonObject
|
||||
{
|
||||
["search_metadata"] = searchMetadata,
|
||||
["page"] = pageContent,
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonArray BuildJsonArray(IEnumerable<string> values)
|
||||
{
|
||||
var result = new JsonArray();
|
||||
foreach (var value in values)
|
||||
result.Add(value);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string ReadRequiredString(JsonElement arguments, string propertyName)
|
||||
{
|
||||
var value = ReadOptionalString(arguments, propertyName);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new ArgumentException($"Missing required argument '{propertyName}'.");
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string? ReadOptionalString(JsonElement arguments, string propertyName)
|
||||
{
|
||||
if (!arguments.TryGetProperty(propertyName, out var value))
|
||||
return null;
|
||||
|
||||
return value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Null => null,
|
||||
JsonValueKind.String => value.GetString()?.Trim(),
|
||||
_ => throw new ArgumentException($"Argument '{propertyName}' must be a string."),
|
||||
};
|
||||
}
|
||||
|
||||
private static int? ReadOptionalPositiveInt(JsonElement arguments, string propertyName)
|
||||
{
|
||||
if (!arguments.TryGetProperty(propertyName, out var value))
|
||||
return null;
|
||||
|
||||
if (value.ValueKind is JsonValueKind.Null)
|
||||
return null;
|
||||
|
||||
if (value.ValueKind is not JsonValueKind.Number || !value.TryGetInt32(out var intValue) || intValue <= 0)
|
||||
throw new ArgumentException($"Argument '{propertyName}' must be a positive integer.");
|
||||
|
||||
return intValue;
|
||||
}
|
||||
|
||||
private static List<string> ReadOptionalStringArray(JsonElement arguments, string propertyName)
|
||||
{
|
||||
if (!arguments.TryGetProperty(propertyName, out var value) || value.ValueKind is JsonValueKind.Null)
|
||||
return [];
|
||||
|
||||
if (value.ValueKind is not JsonValueKind.Array)
|
||||
throw new ArgumentException($"Argument '{propertyName}' must be an array of strings.");
|
||||
|
||||
var values = new List<string>();
|
||||
foreach (var element in value.EnumerateArray())
|
||||
{
|
||||
if (element.ValueKind is not JsonValueKind.String)
|
||||
throw new ArgumentException($"Argument '{propertyName}' must be an array of strings.");
|
||||
|
||||
var item = element.GetString()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(item))
|
||||
values.Add(item);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static List<string> SplitCommaSeparatedValues(string? value) => value?
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList() ?? [];
|
||||
|
||||
private static bool TryNormalizeSearchUri(string rawUrl, out Uri searchUri, out string error) =>
|
||||
SearXNGSearchClient.TryNormalizeSearchUri(
|
||||
rawUrl,
|
||||
TB("A SearXNG URL is required."),
|
||||
TB("The configured SearXNG URL is not a valid absolute URL."),
|
||||
TB("The configured SearXNG URL must start with http:// or https://."),
|
||||
out searchUri,
|
||||
out error);
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
public sealed class ToolDefinition
|
||||
{
|
||||
public int SchemaVersion { get; init; } = 1;
|
||||
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
|
||||
public string Icon { get; init; } = Icons.Material.Filled.Build;
|
||||
|
||||
public string ImplementationKey { get; init; } = string.Empty;
|
||||
|
||||
public ToolVisibilityDefinition VisibleIn { get; init; } = new();
|
||||
|
||||
public ToolSettingsSchema SettingsSchema { get; init; } = new();
|
||||
|
||||
public string SystemPromptInstructions { get; init; } = string.Empty;
|
||||
|
||||
public ToolFunctionDefinition Function { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class ToolVisibilityDefinition
|
||||
{
|
||||
public bool Chat { get; init; } = true;
|
||||
|
||||
public bool Assistants { get; init; } = true;
|
||||
}
|
||||
|
||||
public sealed class ToolFunctionDefinition
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
public string DescriptionForLLM { get; init; } = string.Empty;
|
||||
|
||||
public bool Strict { get; init; } = true;
|
||||
|
||||
public JsonElement Parameters { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ToolSettingsSchema
|
||||
{
|
||||
public string Type { get; init; } = "object";
|
||||
|
||||
public Dictionary<string, ToolSettingsFieldDefinition> Properties { get; init; } = [];
|
||||
|
||||
public HashSet<string> Required { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class ToolSettingsFieldDefinition
|
||||
{
|
||||
public string Type { get; init; } = "string";
|
||||
|
||||
public string Title { get; init; } = string.Empty;
|
||||
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("enum")]
|
||||
public List<string> EnumValues { get; init; } = [];
|
||||
|
||||
public bool Secret { get; init; }
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
public sealed class ToolExecutionContext
|
||||
{
|
||||
public required ToolDefinition Definition { get; init; }
|
||||
|
||||
public required SettingsManager SettingsManager { get; init; }
|
||||
|
||||
public required IReadOnlyDictionary<string, string> SettingsValues { get; init; }
|
||||
|
||||
public ConfidenceLevel ProviderConfidence { get; init; } = ConfidenceLevel.UNKNOWN;
|
||||
}
|
||||
|
||||
public sealed class ToolExecutionResult
|
||||
{
|
||||
public string? TextContent { get; init; }
|
||||
|
||||
public JsonNode? JsonContent { get; init; }
|
||||
|
||||
public ConfidenceLevel RequiredProviderConfidence { get; init; } = ConfidenceLevel.NONE;
|
||||
|
||||
public string ToModelContent()
|
||||
{
|
||||
if (this.JsonContent is not null)
|
||||
return this.JsonContent.ToJsonString();
|
||||
|
||||
return this.TextContent ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ToolExecutionBlockedException(string message) : Exception(message);
|
||||
|
||||
public enum ToolInvocationTraceStatus
|
||||
{
|
||||
NONE = 0,
|
||||
SUCCESS,
|
||||
ERROR,
|
||||
BLOCKED,
|
||||
}
|
||||
|
||||
public sealed class ToolInvocationTrace
|
||||
{
|
||||
public int Order { get; set; }
|
||||
|
||||
public string ToolId { get; set; } = string.Empty;
|
||||
|
||||
public string ToolName { get; set; } = string.Empty;
|
||||
|
||||
public string ToolIcon { get; set; } = Icons.Material.Filled.Build;
|
||||
|
||||
public string ToolCallId { get; set; } = string.Empty;
|
||||
|
||||
public ToolInvocationTraceStatus Status { get; set; } = ToolInvocationTraceStatus.NONE;
|
||||
|
||||
public bool WasExecuted { get; set; }
|
||||
|
||||
public string StatusMessage { get; set; } = string.Empty;
|
||||
|
||||
public Dictionary<string, string> Arguments { get; set; } = [];
|
||||
|
||||
[JsonIgnore]
|
||||
public string Result { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ToolRuntimeStatus
|
||||
{
|
||||
public bool IsRunning { get; set; }
|
||||
|
||||
public List<string> ToolNames { get; set; } = [];
|
||||
|
||||
public string Message => this.ToolNames.Count switch
|
||||
{
|
||||
0 => string.Empty,
|
||||
1 => $"Using tool: {this.ToolNames[0]}",
|
||||
_ => $"Using tools: {string.Join(", ", this.ToolNames)}",
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class ToolConfigurationState
|
||||
{
|
||||
public bool IsConfigured { get; init; }
|
||||
|
||||
public List<string> MissingRequiredFields { get; init; } = [];
|
||||
|
||||
public string Message { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ToolCatalogItem
|
||||
{
|
||||
public required ToolDefinition Definition { get; init; }
|
||||
|
||||
public required IToolImplementation Implementation { get; init; }
|
||||
|
||||
public required ToolConfigurationState ConfigurationState { get; init; }
|
||||
|
||||
public bool IsActive { get; init; }
|
||||
|
||||
public ConfidenceLevel MinimumProviderConfidence { get; init; } = ConfidenceLevel.NONE;
|
||||
}
|
||||
|
||||
public sealed class ToolSelectionState
|
||||
{
|
||||
public HashSet<string> SelectedToolIds { get; init; } = [];
|
||||
}
|
||||
159
app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutor.cs
Normal file
159
app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutor.cs
Normal file
@ -0,0 +1,159 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Provider;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogger<ToolExecutor> logger)
|
||||
{
|
||||
public async Task<(string Content, ToolInvocationTrace Trace, ConfidenceLevel RequiredProviderConfidence)> ExecuteAsync(
|
||||
string toolCallId,
|
||||
string toolName,
|
||||
string argumentsJson,
|
||||
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools,
|
||||
ConfidenceLevel providerConfidence,
|
||||
int order,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var runnableTool = runnableTools.FirstOrDefault(x => x.Definition.Function.Name.Equals(toolName, StringComparison.Ordinal));
|
||||
Dictionary<string, string> formattedArguments = [];
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson);
|
||||
formattedArguments = FormatArguments(document.RootElement, runnableTool.Implementation?.SensitiveTraceArgumentNames ?? EmptySensitiveTraceArgumentNames.INSTANCE);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"Starting tool execution. ToolName={ToolName}, ToolCallId={ToolCallId}, ArgumentNames={ArgumentNames}",
|
||||
toolName,
|
||||
toolCallId,
|
||||
formattedArguments.Keys.OrderBy(x => x, StringComparer.Ordinal).ToList());
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
if (runnableTool.Definition is null || runnableTool.Implementation is null)
|
||||
{
|
||||
var error = this.CreateError(toolName);
|
||||
logger.LogWarning("Completed tool execution. ToolName={ToolName}, ToolCallId={ToolCallId}, DurationMs={DurationMs}, Status={Status}", toolName, toolCallId, stopwatch.ElapsedMilliseconds, ToolInvocationTraceStatus.BLOCKED);
|
||||
return (error, new ToolInvocationTrace
|
||||
{
|
||||
Order = order,
|
||||
ToolId = toolName,
|
||||
ToolName = toolName,
|
||||
ToolCallId = toolCallId,
|
||||
Status = ToolInvocationTraceStatus.BLOCKED,
|
||||
StatusMessage = "Tool is not available in the current context.",
|
||||
Arguments = formattedArguments,
|
||||
Result = error,
|
||||
}, ConfidenceLevel.NONE);
|
||||
}
|
||||
|
||||
var definition = runnableTool.Definition;
|
||||
var implementation = runnableTool.Implementation;
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson);
|
||||
var settingsValues = await toolSettingsService.GetSettingsAsync(definition);
|
||||
var result = await implementation.ExecuteAsync(document.RootElement, new ToolExecutionContext
|
||||
{
|
||||
Definition = definition,
|
||||
SettingsManager = Program.SERVICE_PROVIDER.GetRequiredService<Settings.SettingsManager>(),
|
||||
SettingsValues = settingsValues,
|
||||
ProviderConfidence = providerConfidence,
|
||||
}, token);
|
||||
logger.LogInformation("Completed tool execution. ToolName={ToolName}, ToolCallId={ToolCallId}, DurationMs={DurationMs}, Status={Status}", toolName, toolCallId, stopwatch.ElapsedMilliseconds, ToolInvocationTraceStatus.SUCCESS);
|
||||
|
||||
var resultModelContent = result.ToModelContent();
|
||||
var toolInvocationTrace = new ToolInvocationTrace
|
||||
{
|
||||
Order = order,
|
||||
ToolId = definition.Id,
|
||||
ToolName = implementation.GetDisplayName(),
|
||||
ToolIcon = implementation.Icon,
|
||||
ToolCallId = toolCallId,
|
||||
Status = ToolInvocationTraceStatus.SUCCESS,
|
||||
WasExecuted = true,
|
||||
Arguments = FormatArguments(document.RootElement,
|
||||
implementation.SensitiveTraceArgumentNames),
|
||||
Result =
|
||||
implementation.FormatTraceResult(result.ToModelContent()),
|
||||
};
|
||||
|
||||
return (resultModelContent, toolInvocationTrace, result.RequiredProviderConfidence);
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (ToolExecutionBlockedException exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Tool execution was blocked. ToolName={ToolName}, ToolCallId={ToolCallId}, DurationMs={DurationMs}, Status={Status}, ErrorMessage={ErrorMessage}", toolName, toolCallId, stopwatch.ElapsedMilliseconds, ToolInvocationTraceStatus.BLOCKED, exception.Message);
|
||||
|
||||
var toolInvocationTrace = new ToolInvocationTrace
|
||||
{
|
||||
Order = order,
|
||||
ToolId = definition.Id,
|
||||
ToolName = implementation.GetDisplayName(),
|
||||
ToolIcon = implementation.Icon,
|
||||
ToolCallId = toolCallId,
|
||||
Status = ToolInvocationTraceStatus.BLOCKED,
|
||||
StatusMessage = exception.Message,
|
||||
Arguments = formattedArguments,
|
||||
Result = exception.Message,
|
||||
};
|
||||
|
||||
return (exception.Message, toolInvocationTrace, ConfidenceLevel.NONE);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
var error = $"Tool execution failed: {exception.Message}";
|
||||
logger.LogError(exception, "Tool execution failed. ToolName={ToolName}, ToolCallId={ToolCallId}, DurationMs={DurationMs}, Status={Status}, ErrorMessage={ErrorMessage}", toolName, toolCallId, stopwatch.ElapsedMilliseconds, ToolInvocationTraceStatus.ERROR, exception.Message);
|
||||
|
||||
var toolInvocationTrace = new ToolInvocationTrace
|
||||
{
|
||||
Order = order,
|
||||
ToolId = definition.Id,
|
||||
ToolName = implementation.GetDisplayName(),
|
||||
ToolIcon = implementation.Icon,
|
||||
ToolCallId = toolCallId,
|
||||
Status = ToolInvocationTraceStatus.ERROR,
|
||||
StatusMessage = error,
|
||||
Arguments = formattedArguments,
|
||||
Result = error,
|
||||
};
|
||||
|
||||
return (error, toolInvocationTrace, ConfidenceLevel.NONE);
|
||||
}
|
||||
}
|
||||
|
||||
private static class EmptySensitiveTraceArgumentNames
|
||||
{
|
||||
public static readonly IReadOnlySet<string> INSTANCE = new HashSet<string>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private string CreateError(string toolName) => $"Tool '{toolName}' is not available.";
|
||||
|
||||
private static Dictionary<string, string> FormatArguments(JsonElement rootElement, IReadOnlySet<string> sensitiveNames)
|
||||
{
|
||||
if (rootElement.ValueKind is not JsonValueKind.Object)
|
||||
return [];
|
||||
|
||||
var arguments = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
foreach (var property in rootElement.EnumerateObject())
|
||||
{
|
||||
arguments[property.Name] = sensitiveNames.Contains(property.Name)
|
||||
? "*****"
|
||||
: property.Value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => property.Value.GetString() ?? string.Empty,
|
||||
_ => property.Value.ToString(),
|
||||
};
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
}
|
||||
297
app/MindWork AI Studio/Tools/ToolCallingSystem/ToolRegistry.cs
Normal file
297
app/MindWork AI Studio/Tools/ToolCallingSystem/ToolRegistry.cs
Normal file
@ -0,0 +1,297 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
public sealed class ToolRegistry
|
||||
{
|
||||
private readonly ILogger<ToolRegistry> logger;
|
||||
private readonly SettingsManager settingsManager;
|
||||
private readonly ToolSettingsService toolSettingsService;
|
||||
private readonly Dictionary<string, ToolDefinition> definitionsById = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, IToolImplementation> implementationsByKey = new(StringComparer.Ordinal);
|
||||
|
||||
public ToolRegistry(
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
IEnumerable<IToolImplementation> implementations,
|
||||
SettingsManager settingsManager,
|
||||
ToolSettingsService toolSettingsService,
|
||||
ILogger<ToolRegistry> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.settingsManager = settingsManager;
|
||||
this.toolSettingsService = toolSettingsService;
|
||||
|
||||
foreach (var implementation in implementations)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(implementation.ImplementationKey))
|
||||
{
|
||||
this.logger.LogWarning("Skipping a tool implementation with an empty implementation key.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.implementationsByKey.TryAdd(implementation.ImplementationKey, implementation))
|
||||
this.logger.LogWarning("Skipping duplicate tool implementation key '{ImplementationKey}'.", implementation.ImplementationKey);
|
||||
}
|
||||
|
||||
var definitionsDirectory = webHostEnvironment.WebRootFileProvider.GetDirectoryContents("tool_definitions");
|
||||
if (!definitionsDirectory.Exists)
|
||||
{
|
||||
this.logger.LogWarning("The tool definitions directory was not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var serializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
var functionNames = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var file in definitionsDirectory.Where(x => !x.IsDirectory && x.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = file.CreateReadStream();
|
||||
var definition = JsonSerializer.Deserialize<ToolDefinition>(stream, serializerOptions);
|
||||
if (definition is null)
|
||||
{
|
||||
this.logger.LogWarning("Skipping tool definition '{ToolFile}' because it could not be deserialized.", file.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryValidateDefinition(definition, out var validationIssue))
|
||||
{
|
||||
this.logger.LogWarning("Skipping tool definition '{ToolFile}': {ValidationIssue}", file.Name, validationIssue);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.implementationsByKey.ContainsKey(definition.ImplementationKey))
|
||||
{
|
||||
this.logger.LogWarning("Skipping tool definition '{ToolId}' because implementation key '{ImplementationKey}' is not registered.", definition.Id, definition.ImplementationKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.definitionsById.ContainsKey(definition.Id))
|
||||
{
|
||||
this.logger.LogWarning("Skipping duplicate tool definition ID '{ToolId}' from '{ToolFile}'.", definition.Id, file.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!functionNames.Add(definition.Function.Name))
|
||||
{
|
||||
this.logger.LogWarning("Skipping tool definition '{ToolId}' because function name '{FunctionName}' is already registered.", definition.Id, definition.Function.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.definitionsById.Add(definition.Id, definition);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
this.logger.LogWarning(exception, "Skipping invalid tool definition file '{ToolFile}'.", file.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryValidateDefinition(ToolDefinition definition, out string issue)
|
||||
{
|
||||
issue = string.Empty;
|
||||
if (definition.SchemaVersion != 1)
|
||||
{
|
||||
issue = $"unsupported schema version '{definition.SchemaVersion}'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(definition.Id))
|
||||
{
|
||||
issue = "the definition ID is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(definition.ImplementationKey))
|
||||
{
|
||||
issue = "the implementation key is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (definition.Function is null || !IsValidFunctionName(definition.Function.Name))
|
||||
{
|
||||
issue = "the function name must contain 1-64 ASCII letters, digits, underscores, or hyphens";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (definition.Function.Parameters.ValueKind is not JsonValueKind.Object)
|
||||
{
|
||||
issue = "the function parameters schema must be a JSON object";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (definition.SettingsSchema is null ||
|
||||
!string.Equals(definition.SettingsSchema.Type, "object", StringComparison.OrdinalIgnoreCase) ||
|
||||
definition.SettingsSchema.Properties is null ||
|
||||
definition.SettingsSchema.Required is null)
|
||||
{
|
||||
issue = "the settings schema must have type 'object'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (definition.SettingsSchema.Properties.Any(x =>
|
||||
string.IsNullOrWhiteSpace(x.Key) ||
|
||||
x.Value is null ||
|
||||
!string.Equals(x.Value.Type, "string", StringComparison.OrdinalIgnoreCase) ||
|
||||
x.Value.EnumValues is null))
|
||||
{
|
||||
issue = "settings properties must be named string fields with valid enum lists";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (definition.SettingsSchema.Required.Any(string.IsNullOrWhiteSpace))
|
||||
{
|
||||
issue = "required setting names cannot be empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
var missingRequiredProperties = definition.SettingsSchema.Required
|
||||
.Where(x => !definition.SettingsSchema.Properties.ContainsKey(x))
|
||||
.ToList();
|
||||
if (missingRequiredProperties.Count > 0)
|
||||
{
|
||||
issue = $"required settings are missing definitions: {string.Join(", ", missingRequiredProperties)}";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsValidFunctionName(string? functionName) =>
|
||||
!string.IsNullOrWhiteSpace(functionName) &&
|
||||
functionName.Length <= 64 &&
|
||||
functionName.All(character => char.IsAsciiLetterOrDigit(character) || character is '_' or '-');
|
||||
|
||||
public IReadOnlyList<ToolDefinition> GetDefinitionsForComponent(AIStudio.Tools.Components component)
|
||||
{
|
||||
var isChat = component is AIStudio.Tools.Components.CHAT;
|
||||
return this.definitionsById.Values
|
||||
.Where(x => isChat ? x.VisibleIn.Chat : x.VisibleIn.Assistants)
|
||||
.OrderBy(x => this.implementationsByKey.GetValueOrDefault(x.ImplementationKey)?.GetDisplayName(), StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public IReadOnlyList<ToolDefinition> GetAllDefinitions() => this.definitionsById.Values
|
||||
.OrderBy(x => this.implementationsByKey.GetValueOrDefault(x.ImplementationKey)?.GetDisplayName(), StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
public ToolDefinition? GetDefinition(string toolId) => this.definitionsById.GetValueOrDefault(toolId);
|
||||
|
||||
public IToolImplementation? GetImplementation(string implementationKey) => this.implementationsByKey.GetValueOrDefault(implementationKey);
|
||||
|
||||
public async Task<IReadOnlyList<ToolCatalogItem>> GetCatalogAsync(AIStudio.Tools.Components component)
|
||||
{
|
||||
var definitions = this.GetDefinitionsForComponent(component);
|
||||
return await this.GetCatalogAsync(definitions);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ToolCatalogItem>> GetCatalogAsync(IEnumerable<ToolDefinition> definitions)
|
||||
{
|
||||
var definitionList = definitions.ToList();
|
||||
var items = new List<ToolCatalogItem>(definitionList.Count);
|
||||
foreach (var definition in definitionList)
|
||||
{
|
||||
if (!this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation))
|
||||
continue;
|
||||
|
||||
items.Add(new ToolCatalogItem
|
||||
{
|
||||
Definition = definition,
|
||||
Implementation = implementation,
|
||||
ConfigurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition, implementation),
|
||||
IsActive = this.settingsManager.IsToolActive(definition.Id),
|
||||
MinimumProviderConfidence = this.settingsManager.GetMinimumProviderConfidenceForTool(definition.Id),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)>> GetRunnableToolsAsync(
|
||||
AIStudio.Settings.Provider provider,
|
||||
AIStudio.Tools.Components component,
|
||||
IEnumerable<string> selectedToolIds,
|
||||
IReadOnlyCollection<Capability> modelCapabilities,
|
||||
ConfidenceLevel providerConfidence,
|
||||
bool isToolSelectionVisible)
|
||||
{
|
||||
if (!this.settingsManager.AreToolsEnabled())
|
||||
{
|
||||
this.logger.LogInformation("Tool calling is skipped because tools are disabled by managed configuration.");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!isToolSelectionVisible)
|
||||
{
|
||||
this.logger.LogInformation("Tool calling is skipped for component '{Component}' because tool selection is not visible.", component);
|
||||
return [];
|
||||
}
|
||||
|
||||
var toolCallingAvailability = provider.GetToolCallingAvailability();
|
||||
if (!toolCallingAvailability.IsAvailable)
|
||||
{
|
||||
this.logger.LogInformation("Tool calling is unavailable for provider '{Provider}' with model '{ModelId}': {Reason}", provider.InstanceName, provider.Model.Id, toolCallingAvailability.Message);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!modelCapabilities.Contains(Capability.FUNCTION_CALLING) ||
|
||||
(!modelCapabilities.Contains(Capability.CHAT_COMPLETION_API) && !modelCapabilities.Contains(Capability.RESPONSES_API)))
|
||||
{
|
||||
this.logger.LogInformation("Tool calling is unavailable for provider '{Provider}' with model '{ModelId}' because the model lacks the required API or function-calling capability.", provider.InstanceName, provider.Model.Id);
|
||||
return [];
|
||||
}
|
||||
|
||||
var selectedToolIdSet = ToolSelectionRules.NormalizeSelection(selectedToolIds);
|
||||
this.logger.LogInformation("Resolving runnable tools for provider '{Provider}' with model '{ModelId}'. Selected tool IDs: [{ToolIds}].", provider.InstanceName, provider.Model.Id, string.Join(", ", selectedToolIdSet.OrderBy(x => x, StringComparer.Ordinal)));
|
||||
|
||||
var definitions = this.GetDefinitionsForComponent(component).Where(x => selectedToolIdSet.Contains(x.Id)).ToList();
|
||||
var result = new List<(ToolDefinition, IToolImplementation)>(definitions.Count);
|
||||
foreach (var definition in definitions)
|
||||
{
|
||||
if (!this.settingsManager.IsToolActive(definition.Id))
|
||||
{
|
||||
this.logger.LogInformation("Skipping tool '{ToolId}' because it is disabled by managed configuration.", definition.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation))
|
||||
{
|
||||
this.logger.LogInformation("Skipping tool '{ToolId}' because no implementation is registered.", definition.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
var configurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition, implementation);
|
||||
if (!configurationState.IsConfigured)
|
||||
{
|
||||
this.logger.LogInformation("Skipping tool '{ToolId}' because it is not configured.", definition.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
var resolution = this.settingsManager.GetMinimumProviderConfidenceResolutionForTool(definition.Id);
|
||||
var minimumToolConfidence = resolution.ConfidenceLevel;
|
||||
this.logger.LogInformation("Tool '{ToolId}' uses minimum provider confidence '{ConfidenceLevel}' from {Source}.", definition.Id, minimumToolConfidence, resolution.Source);
|
||||
|
||||
if (!ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumToolConfidence))
|
||||
{
|
||||
this.logger.LogInformation("Skipping tool '{ToolId}' because provider confidence '{ProviderConfidence}' is below the required minimum '{MinimumConfidence}'.", definition.Id, providerConfidence, minimumToolConfidence);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add((definition, implementation));
|
||||
}
|
||||
|
||||
foreach (var selectedToolId in selectedToolIdSet.Where(selectedToolId => definitions.All(definition => !definition.Id.Equals(selectedToolId, StringComparison.Ordinal))))
|
||||
this.logger.LogInformation("Skipping tool '{ToolId}' because it is not selected in this component or not available in this context.", selectedToolId);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using AIStudio.Provider;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
public static class ToolSelectionRules
|
||||
{
|
||||
public const int MAX_TOOL_CALLS = 15;
|
||||
public const string WEB_SEARCH_TOOL_ID = "web_search";
|
||||
public const string READ_WEB_PAGE_TOOL_ID = "read_web_page";
|
||||
|
||||
public static HashSet<string> NormalizeSelection(IEnumerable<string> selectedToolIds)
|
||||
=> selectedToolIds.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
public static ConfidenceLevel GetDefaultMinimumProviderConfidence(string toolId) => toolId switch
|
||||
{
|
||||
WEB_SEARCH_TOOL_ID => ConfidenceLevel.MEDIUM,
|
||||
READ_WEB_PAGE_TOOL_ID => ConfidenceLevel.MEDIUM,
|
||||
_ => ConfidenceLevel.NONE,
|
||||
};
|
||||
|
||||
public static string GetMaxToolCallsFinalResponseInstruction() => $"The maximum of {MAX_TOOL_CALLS} tool calls has been reached. No more tools are available. Provide the best possible final answer to the user based on the tool results already available.";
|
||||
|
||||
public static string BuildToolPolicyPrompt(IEnumerable<ToolDefinition> definitions)
|
||||
{
|
||||
var policySections = definitions
|
||||
.Select(x => (ToolName: x.Function.Name, PolicyLines: x.SystemPromptInstructions?.Trim()))
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.PolicyLines))
|
||||
.Select(x => $"## Tool `{x.ToolName}`{Environment.NewLine}{x.PolicyLines}")
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
if (policySections.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
var toolPolicyPrompt = $"""
|
||||
# Tool usage instructions:
|
||||
You have multiple tools available. Each tool has a different purpose and usage policy. Choose wisely and if you are not sure, always ask the user for clarification. You must follow the usage policy of each tool to ensure accurate and reliable results. Here are the usage policies for each tool:
|
||||
|
||||
{string.Join(Environment.NewLine+Environment.NewLine, policySections)}
|
||||
""";
|
||||
|
||||
return toolPolicyPrompt;
|
||||
}
|
||||
|
||||
public static bool IsProviderConfidenceAllowed(ConfidenceLevel providerConfidence, ConfidenceLevel minimumToolConfidence) =>
|
||||
minimumToolConfidence is ConfidenceLevel.NONE || providerConfidence >= minimumToolConfidence;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
using AIStudio.Tools;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
internal sealed record ToolSettingsSecretId(string ToolId, string FieldName) : ISecretId
|
||||
{
|
||||
public string SecretId => this.ToolId;
|
||||
|
||||
public string SecretName => this.FieldName;
|
||||
}
|
||||
@ -0,0 +1,178 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
public sealed class ToolSettingsService(SettingsManager settingsManager, RustService rustService)
|
||||
{
|
||||
private static readonly Dictionary<(string ToolId, string FieldName), ManagedToolSetting> MANAGED_SETTINGS = new()
|
||||
{
|
||||
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "baseUrl")] = CreateManagedToolSetting(x => x.WebSearchBaseUrl, (tools, value) => tools.WebSearchBaseUrl = value),
|
||||
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "defaultLanguage")] = CreateManagedToolSetting(x => x.WebSearchDefaultLanguage),
|
||||
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "defaultSafeSearch")] = CreateManagedToolSetting(x => x.WebSearchDefaultSafeSearch),
|
||||
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "defaultCategories")] = CreateManagedToolSetting(x => x.WebSearchDefaultCategories),
|
||||
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "defaultEngines")] = CreateManagedToolSetting(x => x.WebSearchDefaultEngines),
|
||||
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "maxResults")] = CreateManagedToolSetting(x => x.WebSearchMaxResults),
|
||||
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "timeoutSeconds")] = CreateManagedToolSetting(x => x.WebSearchTimeoutSeconds),
|
||||
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "maxTotalContentCharacters")] = CreateManagedToolSetting(x => x.WebSearchMaxTotalContentCharacters),
|
||||
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "minContentCharactersPerResult")] = CreateManagedToolSetting(x => x.WebSearchMinContentCharactersPerResult),
|
||||
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "pageTimeoutSeconds")] = CreateManagedToolSetting(x => x.WebSearchPageTimeoutSeconds),
|
||||
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "retrievalTimeoutSeconds")] = CreateManagedToolSetting(x => x.WebSearchRetrievalTimeoutSeconds),
|
||||
[(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, "timeoutSeconds")] = CreateManagedToolSetting(x => x.ReadWebPageTimeoutSeconds),
|
||||
[(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, "maxContentCharacters")] = CreateManagedToolSetting(x => x.ReadWebPageMaxContentCharacters),
|
||||
[(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, "allowedPrivateHosts")] = CreateManagedToolSetting(x => x.ReadWebPageAllowedPrivateHosts, (tools, value) => tools.ReadWebPageAllowedPrivateHosts = value),
|
||||
};
|
||||
|
||||
public async Task<Dictionary<string, string>> GetSettingsAsync(ToolDefinition definition)
|
||||
{
|
||||
var values = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var storedValues = settingsManager.ConfigurationData.Tools.Settings.GetValueOrDefault(definition.Id);
|
||||
foreach (var property in definition.SettingsSchema.Properties)
|
||||
{
|
||||
var fieldName = property.Key;
|
||||
var fieldDefinition = property.Value;
|
||||
if (TryGetManagedSetting(definition, fieldName, out var managedSetting))
|
||||
{
|
||||
var meta = managedSetting.GetMeta();
|
||||
if (meta?.IsLocked is true || managedSetting.SetLegacyLocalValue is not null)
|
||||
values[fieldName] = managedSetting.GetValue(settingsManager.ConfigurationData.Tools);
|
||||
else if (storedValues?.TryGetValue(fieldName, out var managedStoredValue) is true)
|
||||
values[fieldName] = managedStoredValue;
|
||||
else if (meta?.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT)
|
||||
values[fieldName] = managedSetting.GetValue(settingsManager.ConfigurationData.Tools);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fieldDefinition.Secret)
|
||||
{
|
||||
var response = await rustService.GetSecret(new ToolSettingsSecretId(definition.Id, fieldName), SecretStoreType.TOOL_SETTINGS, isTrying: true);
|
||||
if (response.Success)
|
||||
values[fieldName] = await response.Secret.Decrypt(Program.ENCRYPTION);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (storedValues?.TryGetValue(fieldName, out var storedValue) is true)
|
||||
values[fieldName] = storedValue;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
public async Task<ToolConfigurationState> GetConfigurationStateAsync(
|
||||
ToolDefinition definition,
|
||||
IToolImplementation? implementation = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var values = await this.GetSettingsAsync(definition);
|
||||
return await this.ValidateSettingsAsync(definition, values, implementation, token);
|
||||
}
|
||||
|
||||
public async Task<ToolConfigurationState> ValidateSettingsAsync(
|
||||
ToolDefinition definition,
|
||||
IReadOnlyDictionary<string, string> values,
|
||||
IToolImplementation? implementation = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var missing = new List<string>();
|
||||
foreach (var requiredField in definition.SettingsSchema.Required)
|
||||
{
|
||||
if (!values.TryGetValue(requiredField, out var value) || string.IsNullOrWhiteSpace(value))
|
||||
missing.Add(requiredField);
|
||||
}
|
||||
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
return new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = false,
|
||||
MissingRequiredFields = missing,
|
||||
};
|
||||
}
|
||||
|
||||
if (implementation is not null)
|
||||
{
|
||||
var validationState = await implementation.ValidateConfigurationAsync(definition, values, token);
|
||||
if (validationState is not null && !validationState.IsConfigured)
|
||||
return validationState;
|
||||
}
|
||||
|
||||
return new ToolConfigurationState
|
||||
{
|
||||
IsConfigured = true,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task SaveSettingsAsync(ToolDefinition definition, IReadOnlyDictionary<string, string> values)
|
||||
{
|
||||
if (!settingsManager.ConfigurationData.Tools.Settings.TryGetValue(definition.Id, out var storedValues))
|
||||
{
|
||||
storedValues = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
settingsManager.ConfigurationData.Tools.Settings[definition.Id] = storedValues;
|
||||
}
|
||||
|
||||
foreach (var property in definition.SettingsSchema.Properties)
|
||||
{
|
||||
var fieldName = property.Key;
|
||||
var fieldDefinition = property.Value;
|
||||
values.TryGetValue(fieldName, out var value);
|
||||
value ??= string.Empty;
|
||||
|
||||
if (TryGetManagedSetting(definition, fieldName, out var managedSetting))
|
||||
{
|
||||
if (managedSetting.GetMeta()?.IsLocked is true)
|
||||
continue;
|
||||
|
||||
if (managedSetting.SetLegacyLocalValue is not null)
|
||||
managedSetting.SetLegacyLocalValue(settingsManager.ConfigurationData.Tools, value);
|
||||
else
|
||||
storedValues[fieldName] = value;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fieldDefinition.Secret)
|
||||
{
|
||||
var secretId = new ToolSettingsSecretId(definition.Id, fieldName);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
await rustService.DeleteSecret(secretId, SecretStoreType.TOOL_SETTINGS);
|
||||
else
|
||||
await rustService.SetSecret(secretId, value, SecretStoreType.TOOL_SETTINGS);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
storedValues[fieldName] = value;
|
||||
}
|
||||
|
||||
await settingsManager.StoreSettings();
|
||||
await MessageBus.INSTANCE.SendMessage<object?>(null, Event.CONFIGURATION_CHANGED, null);
|
||||
}
|
||||
|
||||
public bool IsFieldLocked(ToolDefinition definition, string fieldName) =>
|
||||
TryGetManagedSetting(definition, fieldName, out var managedSetting) &&
|
||||
managedSetting.GetMeta()?.IsLocked is true;
|
||||
|
||||
private static bool TryGetManagedSetting(ToolDefinition definition, string fieldName, out ManagedToolSetting managedSetting) =>
|
||||
MANAGED_SETTINGS.TryGetValue((definition.Id, fieldName), out managedSetting!);
|
||||
|
||||
private static ManagedToolSetting CreateManagedToolSetting(
|
||||
Expression<Func<DataTools, string>> propertyExpression,
|
||||
Action<DataTools, string>? setLegacyLocalValue = null)
|
||||
{
|
||||
var getValue = propertyExpression.Compile();
|
||||
return new ManagedToolSetting(
|
||||
getValue,
|
||||
() => ManagedConfiguration.TryGet(x => x.Tools, propertyExpression, out var meta) ? meta : null,
|
||||
setLegacyLocalValue);
|
||||
}
|
||||
|
||||
private sealed record ManagedToolSetting(
|
||||
Func<DataTools, string> GetValue,
|
||||
Func<ConfigMeta<DataTools, string>?> GetMeta,
|
||||
Action<DataTools, string>? SetLegacyLocalValue);
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
internal static class ToolSettingsValueParser
|
||||
{
|
||||
public static int? ReadOptionalPositiveInt(IReadOnlyDictionary<string, string> settingsValues, string key)
|
||||
{
|
||||
if (!settingsValues.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))
|
||||
return null;
|
||||
|
||||
return int.TryParse(value, out var parsedValue) && parsedValue > 0 ? parsedValue : null;
|
||||
}
|
||||
|
||||
public static bool TryReadOptionalPositiveInt(
|
||||
IReadOnlyDictionary<string, string> settingsValues,
|
||||
string key,
|
||||
string invalidValueErrorFormat,
|
||||
out int? value,
|
||||
out string error)
|
||||
{
|
||||
value = null;
|
||||
error = string.Empty;
|
||||
|
||||
if (!settingsValues.TryGetValue(key, out var rawValue) || string.IsNullOrWhiteSpace(rawValue))
|
||||
return true;
|
||||
|
||||
if (int.TryParse(rawValue, out var parsedValue) && parsedValue > 0)
|
||||
{
|
||||
value = parsedValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
error = string.Format(invalidValueErrorFormat, key);
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryReadBoundedOptionalPositiveInt(
|
||||
IReadOnlyDictionary<string, string> settingsValues,
|
||||
string key,
|
||||
int maximum,
|
||||
string invalidValueErrorFormat,
|
||||
string maximumErrorFormat,
|
||||
out int? value,
|
||||
out string error)
|
||||
{
|
||||
if (!TryReadOptionalPositiveInt(settingsValues, key, invalidValueErrorFormat, out value, out error))
|
||||
return false;
|
||||
|
||||
if (value is null || value <= maximum)
|
||||
return true;
|
||||
|
||||
error = string.Format(maximumErrorFormat, key, maximum);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
6
app/MindWork AI Studio/Tools/Web/WebHostHelper.cs
Normal file
6
app/MindWork AI Studio/Tools/Web/WebHostHelper.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace AIStudio.Tools.Web;
|
||||
|
||||
internal static class WebHostHelper
|
||||
{
|
||||
public static string Normalize(string host) => host.Trim().TrimEnd('.').ToLowerInvariant();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user