diff --git a/AGENTS.md b/AGENTS.md
index 48a25021..d6e31347 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -119,6 +119,19 @@ When adding configuration options, update:
- `app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs` for parsing logic of complex configuration objects.
- `app/MindWork AI Studio/Plugins/configuration/plugin.lua` to document the new configuration option.
+## 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:
@@ -216,4 +229,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.
\ No newline at end of file
+The entire changelog is sorted by these categories in the order shown above. The language used for the changelog is US English.
diff --git a/README.md b/README.md
index 73cc6c8b..8e81390d 100644
--- a/README.md
+++ b/README.md
@@ -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).
+
@@ -212,4 +214,4 @@ MindWork AI Studio is licensed under the `FSL-1.1-MIT` license (functional sourc
For more details, refer to the [LICENSE](LICENSE.md) file. This license structure ensures you have plenty of freedom to use and enjoy the software while protecting our work.
-
\ No newline at end of file
+
diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor
index 59c9f7a2..9b11152c 100644
--- a/app/MindWork AI Studio/Assistants/AssistantBase.razor
+++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor
@@ -163,6 +163,11 @@
}
+ @if (this.SettingsManager.IsToolSelectionVisible(this.Component))
+ {
+
+ }
+
diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs
index d9cf2afe..baed2afc 100644
--- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs
+++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs
@@ -3,6 +3,7 @@ using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Dialogs.Settings;
using AIStudio.Tools.Services;
+using AIStudio.Tools.ToolCallingSystem;
using Microsoft.AspNetCore.Components;
@@ -119,6 +120,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher
protected ChatThread? ChatThread;
protected IContent? LastUserPrompt;
protected CancellationTokenSource? CancellationTokenSource;
+ protected HashSet selectedToolIds = [];
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
@@ -150,6 +152,7 @@ public abstract partial class AssistantBase : 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);
}
protected override async Task OnParametersSetAsync()
@@ -249,6 +252,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher
ChatId = Guid.NewGuid(),
Name = string.Format(this.TB("Assistant - {0}"), this.Title),
Blocks = [],
+ RuntimeComponent = this.Component,
};
}
@@ -265,6 +269,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher
ChatId = chatId,
Name = name,
Blocks = [],
+ RuntimeComponent = this.Component,
};
return chatId;
@@ -276,6 +281,12 @@ public abstract partial class AssistantBase : AssistantLowerBase wher
this.CurrentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
this.CurrentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(this.Component);
}
+
+ protected Task SelectedToolIdsChanged(HashSet updatedToolIds)
+ {
+ this.selectedToolIds = ToolSelectionRules.NormalizeSelection(updatedToolIds);
+ return Task.CompletedTask;
+ }
protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false, params List attachments)
{
@@ -323,6 +334,10 @@ public abstract partial class AssistantBase : 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)
+ ? ToolSelectionRules.NormalizeSelection(this.selectedToolIds)
+ : [];
}
this.isProcessing = true;
diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
index 6d2a30e1..54a62d93 100644
--- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
+++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
@@ -1912,21 +1912,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."
@@ -1936,6 +1957,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"
@@ -1945,9 +1969,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?"
@@ -1957,9 +1987,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."
@@ -2176,15 +2212,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"
@@ -2995,6 +3022,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"
+
+-- Configure global settings for each tool. Tool defaults for chat and assistants are configured in the corresponding feature settings.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T176751696"] = "Configure global settings for each tool. Tool defaults for chat and assistants are configured in the corresponding feature settings."
+
+-- 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"
+
+-- Tool Settings
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3730473128"] = "Tool Settings"
+
+-- State
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T502047894"] = "State"
+
-- No transcription provider configured yet.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1079350363"] = "No transcription provider configured yet."
@@ -3064,6 +3124,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."
+
+-- 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."
+
+-- The selected provider or model does not support tool calling.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3364063757"] = "The selected provider or model does not support tool calling."
+
+-- 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."
@@ -5683,6 +5803,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"
@@ -6631,6 +6766,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T3948127789"] = "Suggestion"
-- Your stage directions
UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T779923726"] = "Your stage directions"
+-- The tool calling request failed with status code {0}. See the logs for details.
+UI_TEXT_CONTENT["AISTUDIO::PROVIDER::ANTHROPIC::PROVIDERANTHROPIC::T3117779001"] = "The tool calling request failed with status code {0}. See the logs for details."
+
-- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'"
@@ -6664,6 +6802,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}'"
@@ -6754,6 +6895,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."
@@ -7894,6 +8038,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."
+
+-- Optional global truncation limit for extracted Markdown returned to the model.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2066580916"] = "Optional global truncation limit for extracted Markdown returned to the model."
+
+-- 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 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.com. 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::T2866833707"] = "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.com. 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."
+
+-- 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"
+
+-- 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."
+
+-- 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"
diff --git a/app/MindWork AI Studio/Chat/ChatThread.cs b/app/MindWork AI Studio/Chat/ChatThread.cs
index e8277cb5..3fe4f3da 100644
--- a/app/MindWork AI Studio/Chat/ChatThread.cs
+++ b/app/MindWork AI Studio/Chat/ChatThread.cs
@@ -1,8 +1,11 @@
using System.Globalization;
+using System.Text.Json.Serialization;
using AIStudio.Components;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
+using AIStudio.Tools;
+using AIStudio.Tools.ToolCallingSystem;
using AIStudio.Tools.ERIClient.DataModel;
namespace AIStudio.Chat;
@@ -79,6 +82,12 @@ public sealed record ChatThread
/// The content blocks of the chat thread.
///
public List Blocks { get; init; } = [];
+
+ [JsonIgnore]
+ public AIStudio.Tools.Components RuntimeComponent { get; set; } = AIStudio.Tools.Components.CHAT;
+
+ [JsonIgnore]
+ public HashSet RuntimeSelectedToolIds { get; set; } = [];
private bool allowProfile = true;
@@ -92,7 +101,7 @@ public sealed record ChatThread
///
/// The settings manager instance to use.
/// The prepared system prompt.
- public string PrepareSystemPrompt(SettingsManager settingsManager)
+ public string PrepareSystemPrompt(SettingsManager settingsManager, IEnumerable? runnableToolDefinitions = null)
{
//
// Use the information from the chat template, if provided. Otherwise, use the default system prompt
@@ -185,6 +194,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;
@@ -287,4 +307,4 @@ public sealed record ChatThread
return new Tools.ERIClient.DataModel.ChatThread { ContentBlocks = contentBlocks };
}
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor
index 8d0689da..4e0402bc 100644
--- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor
+++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor
@@ -11,9 +11,27 @@
-
- @this.Role.ToName() (@this.Time.LocalDateTime)
-
+
+
+ @this.Role.ToName() (@this.Time.LocalDateTime)
+
+ @if (this.HasToolTrace)
+ {
+
+
+
+
+
+
+
+
+ }
+
@if (this.Content.FileAttachments.Count > 0)
@@ -96,6 +114,67 @@
}
else
{
+ @if (this.HasToolTrace && this.showToolTrace)
+ {
+
+
+ @string.Format(T("Tool Calls ({0})"), textContent.ToolInvocations.Count)
+
+ @foreach (var invocation in textContent.ToolInvocations.OrderBy(x => x.Order))
+ {
+
+
+
+
+
+ @($"{invocation.Order}. {invocation.ToolName}")
+
+ @this.GetTraceStatusText(invocation)
+
+
+
+
+
+
+ @if (this.IsToolInvocationExpanded(invocation.Order))
+ {
+ @if (!string.IsNullOrWhiteSpace(invocation.StatusMessage))
+ {
+ @invocation.StatusMessage
+ }
+
+ @T("Result")
+
+ @this.GetToolInvocationResult(invocation)
+
+
+ @T("Arguments")
+ @if (invocation.Arguments.Count == 0)
+ {
+ @T("No arguments")
+ }
+ else
+ {
+
+ @foreach (var argument in invocation.Arguments)
+ {
+
+ @argument.Key: @argument.Value
+
+ }
+
+ }
+ }
+
+ }
+
+ }
+
var renderPlan = this.GetMarkdownRenderPlan(textContent.Text);
@foreach (var segment in renderPlan.Segments)
@@ -115,6 +194,13 @@
}
+
+ @if (this.Role is ChatRole.AI && !string.IsNullOrWhiteSpace(textContent.ToolRuntimeStatus.Message))
+ {
+
+ @textContent.ToolRuntimeStatus.Message
+
+ }
}
}
}
diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs
index 0dcb910c..32ee1cd9 100644
--- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs
+++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs
@@ -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 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 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 },
diff --git a/app/MindWork AI Studio/Chat/ContentText.cs b/app/MindWork AI Studio/Chat/ContentText.cs
index 4c8be646..eafea313 100644
--- a/app/MindWork AI Studio/Chat/ContentText.cs
+++ b/app/MindWork AI Studio/Chat/ContentText.cs
@@ -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
///
public List FileAttachments { get; set; } = [];
+ public List ToolInvocations { get; set; } = [];
+
+ [JsonIgnore]
+ public ToolRuntimeStatus ToolRuntimeStatus { get; set; } = new();
+
///
public async Task 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(x.Arguments, StringComparer.Ordinal),
+ Result = x.Result,
+ })],
};
#endregion
diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor
index e431e719..9fa82897 100644
--- a/app/MindWork AI Studio/Components/ChatComponent.razor
+++ b/app/MindWork AI Studio/Components/ChatComponent.razor
@@ -123,6 +123,8 @@
+
+
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
{
diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs
index 62caf008..5f210bbc 100644
--- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs
+++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs
@@ -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 Microsoft.AspNetCore.Components;
@@ -69,6 +70,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private bool mustLoadChat;
private LoadChat loadChat;
private bool autoSaveEnabled;
+ private HashSet selectedToolIds = [];
private string currentWorkspaceName = string.Empty;
private Guid currentWorkspaceId = Guid.Empty;
private Guid currentChatThreadId = Guid.Empty;
@@ -76,6 +78,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private Guid loadedParameterWorkspaceId = Guid.Empty;
private Guid foregroundChatId = Guid.Empty;
private int workspaceHeaderSyncVersion;
+ private CancellationTokenSource? cancellationTokenSource;
// Unfortunately, we need the input field reference to blur the focus away. Without
// this, we cannot clear the input field.
@@ -114,6 +117,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));
var deferredInput = MessageBus.INSTANCE.CheckDeferredMessages(Event.SEND_TO_CHAT_INPUT).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(deferredInput))
@@ -714,14 +718,33 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
}
this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'.");
- await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
+ // TODO: await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
+ //{
+ // ChatThread = this.ChatThread!,
+ // AIText = aiText,
+ // LastUserPrompt = lastUserPrompt,
+ // ProviderSettings = this.Provider,
+ // IsForeground = true,
+ //});
+ using (this.cancellationTokenSource = new CancellationTokenSource())
{
- ChatThread = this.ChatThread!,
- AIText = aiText,
- LastUserPrompt = lastUserPrompt,
- ProviderSettings = this.Provider,
- IsForeground = true,
- });
+ this.StateHasChanged();
+ this.ChatThread!.RuntimeComponent = Tools.Components.CHAT;
+ this.ChatThread.RuntimeSelectedToolIds = ToolSelectionRules.NormalizeSelection(this.selectedToolIds);
+
+ // Use the selected provider to get the AI response.
+ // By awaiting this line, we wait for the entire
+ // content to be streamed.
+ this.ChatThread = await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(), this.Provider.Model, lastUserPrompt, this.ChatThread, this.cancellationTokenSource.Token);
+ }
+
+ this.cancellationTokenSource = null;
+
+ // Save the chat:
+ if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
+ {
+ await this.SaveThread();
+ }
await this.SyncForegroundChatAsync();
this.StateHasChanged();
@@ -732,6 +755,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if (this.ChatThread is not null)
await this.AIJobService.CancelChatGenerationAsync(this.ChatThread.ChatId);
}
+
+ private Task SelectedToolIdsChanged(HashSet updatedToolIds)
+ {
+ this.selectedToolIds = ToolSelectionRules.NormalizeSelection(updatedToolIds);
+ return Task.CompletedTask;
+ }
private async Task SaveThread()
{
@@ -795,6 +824,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
//
this.hasUnsavedChanges = false;
this.ComposerState.Clear();
+ this.selectedToolIds = this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT);
//
// Reset the LLM provider considering the user's settings:
@@ -1087,6 +1117,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.StateHasChanged();
}
break;
+
+ case Event.CONFIGURATION_CHANGED:
+ await this.InvokeAsync(this.StateHasChanged);
+ break;
}
}
@@ -1124,4 +1158,4 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
}
#endregion
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs b/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs
index e924b4fd..8aebdc20 100644
--- a/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs
+++ b/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs
@@ -33,6 +33,15 @@ public partial class ConfigurationMultiSelect : ConfigurationBaseCore
///
[Parameter]
public Func 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 : ConfigurationBaseCore
private string GetMultiSelectionText(List? 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 : ConfigurationBaseCore
"This feature is managed by your organization and has therefore been disabled.",
typeof(ConfigurationBase).Namespace,
nameof(ConfigurationBase));
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor
new file mode 100644
index 00000000..238b0133
--- /dev/null
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor
@@ -0,0 +1,56 @@
+@using AIStudio.Provider
+@using AIStudio.Tools.ToolCallingSystem
+@inherits SettingsPanelBase
+
+
+
+ @T("Configure global settings for each tool. Tool defaults for chat and assistants are configured in the corresponding feature settings.")
+
+
+
+
+ @T("Icon")
+ @T("Name")
+ @T("Description")
+ @T("Minimum provider confidence")
+ @T("State")
+ @T("Settings")
+
+
+
+
+
+
+ @context.Implementation.GetDisplayName()
+
+
+ @context.Implementation.GetDescription()
+
+
+
+ @foreach (var confidenceLevel in this.GetSelectableConfidenceLevels())
+ {
+
+ @this.GetConfidenceLevelName(confidenceLevel)
+
+ }
+
+
+
+ @if (context.ConfigurationState.IsConfigured)
+ {
+
+ }
+ else
+ {
+
+
+
+ }
+
+
+
+
+
+
+
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor.cs
new file mode 100644
index 00000000..36b0e7a8
--- /dev/null
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor.cs
@@ -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 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
+ {
+ { x => x.ToolId, toolId },
+ };
+
+ var dialog = await this.DialogService.ShowAsync(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 GetSelectableConfidenceLevels() =>
+ Enum.GetValues().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(this, Event.CONFIGURATION_CHANGED);
+ }
+
+ protected override async Task ProcessIncomingMessage(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;
+ }
+ }
+}
diff --git a/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor b/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor
new file mode 100644
index 00000000..be71c551
--- /dev/null
+++ b/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor
@@ -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)
+ {
+
+ }
+
+}
diff --git a/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor.cs b/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor.cs
new file mode 100644
index 00000000..a63f8734
--- /dev/null
+++ b/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor.cs
@@ -0,0 +1,43 @@
+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> 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);
+
+ protected override async Task OnInitializedAsync()
+ {
+ this.availableTools = (await this.ToolRegistry.GetCatalogAsync(this.Component))
+ .Select(x => new ConfigurationSelectData(x.Implementation.GetDisplayName(), x.Definition.Id))
+ .ToList();
+ await base.OnInitializedAsync();
+ }
+
+ private HashSet GetSelectedValues() => this.SettingsManager.GetDefaultToolIds(this.Component);
+
+ private void UpdateSelection(HashSet values) => this.SettingsManager.ConfigurationData.Tools.DefaultToolIdsByComponent[this.Component.ToString()] = [..ToolSelectionRules.NormalizeSelection(values)];
+}
diff --git a/app/MindWork AI Studio/Components/ToolSelection.razor b/app/MindWork AI Studio/Components/ToolSelection.razor
new file mode 100644
index 00000000..23613c26
--- /dev/null
+++ b/app/MindWork AI Studio/Components/ToolSelection.razor
@@ -0,0 +1,78 @@
+@using AIStudio.Settings
+@using AIStudio.Tools.ToolCallingSystem
+@inherits MSGComponentBase
+
+