diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index 8b6e6e95..bc1a7387 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; @@ -257,7 +258,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher protected Task SelectedToolIdsChanged(HashSet updatedToolIds) { - this.selectedToolIds = updatedToolIds; + this.selectedToolIds = ToolSelectionRules.NormalizeSelection(updatedToolIds); return Task.CompletedTask; } @@ -309,7 +310,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher this.chatThread.SelectedProvider = this.providerSettings.Id; this.chatThread.RuntimeComponent = this.Component; this.chatThread.RuntimeSelectedToolIds = this.SettingsManager.IsToolSelectionVisible(this.Component) - ? [..this.selectedToolIds] + ? ToolSelectionRules.NormalizeSelection(this.selectedToolIds) : []; } diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 9b98da8f..1e331bd5 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -2632,15 +2632,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T176751696" -- This tool still needs to be configured. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T1958939818"] = "This tool still needs to be configured." --- Tools -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T2499909372"] = "Tools" - -- 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" +-- Tool Settings +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3730473128"] = "Tool Settings" + -- State UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T502047894"] = "State" @@ -2743,9 +2743,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T907403808"] = -- 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." @@ -6841,17 +6847,17 @@ 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" --- Current Weather -UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::GETCURRENTWEATHERTOOL::T1597702905"] = "Current Weather" +-- Current Weather", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)); public string GetDescription() => I18N.I.T("Use this demo tool to retrieve the current weather for a given city and state. It is primarily meant to demonstrate tool calling and tool settings in AI Studio.", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)); public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "demoLabel" => I18N.I.T("Demo Label", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "demoLabel" => I18N.I.T("Required demo setting for validating tool settings in tests. It does not affect the weather result.", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var city = arguments.TryGetProperty("city", out var cityValue) ? cityValue.GetString() ?? string.Empty : string.Empty; var state = arguments.TryGetProperty("state", out var stateValue) ? stateValue.GetString() ?? string.Empty : string.Empty; var unit = arguments.TryGetProperty("unit", out var unitValue) ? unitValue.GetString() ?? string.Empty : string.Empty; if (unit is not ("celsius" or "fahrenheit +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::GETCURRENTWEATHERTOOL::T1597702905"] = "Current Weather\", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)); public string GetDescription() => I18N.I.T(\"Use this demo tool to retrieve the current weather for a given city and state. It is primarily meant to demonstrate tool calling and tool settings in AI Studio.\", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)); public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"demoLabel\" => I18N.I.T(\"Demo Label\", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"demoLabel\" => I18N.I.T(\"Required demo setting for validating tool settings in tests. It does not affect the weather result.\", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var city = arguments.TryGetProperty(\"city\", out var cityValue) ? cityValue.GetString() ?? string.Empty : string.Empty; var state = arguments.TryGetProperty(\"state\", out var stateValue) ? stateValue.GetString() ?? string.Empty : string.Empty; var unit = arguments.TryGetProperty(\"unit\", out var unitValue) ? unitValue.GetString() ?? string.Empty : string.Empty; if (unit is not (\"celsius\" or \"fahrenheit" --- Use this demo tool to retrieve the current weather for a given city and state. It is primarily meant to demonstrate tool calling and tool settings in AI Studio. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::GETCURRENTWEATHERTOOL::T2152408159"] = "Use this demo tool to retrieve the current weather for a given city and state. It is primarily meant to demonstrate tool calling and tool settings in AI Studio." +-- Use this demo tool to retrieve the current weather for a given city and state. It is primarily meant to demonstrate tool calling and tool settings in AI Studio.", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)); public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "demoLabel" => I18N.I.T("Demo Label", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "demoLabel" => I18N.I.T("Required demo setting for validating tool settings in tests. It does not affect the weather result.", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var city = arguments.TryGetProperty("city", out var cityValue) ? cityValue.GetString() ?? string.Empty : string.Empty; var state = arguments.TryGetProperty("state", out var stateValue) ? stateValue.GetString() ?? string.Empty : string.Empty; var unit = arguments.TryGetProperty("unit", out var unitValue) ? unitValue.GetString() ?? string.Empty : string.Empty; if (unit is not ("celsius" or "fahrenheit +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::GETCURRENTWEATHERTOOL::T2152408159"] = "Use this demo tool to retrieve the current weather for a given city and state. It is primarily meant to demonstrate tool calling and tool settings in AI Studio.\", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)); public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"demoLabel\" => I18N.I.T(\"Demo Label\", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"demoLabel\" => I18N.I.T(\"Required demo setting for validating tool settings in tests. It does not affect the weather result.\", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var city = arguments.TryGetProperty(\"city\", out var cityValue) ? cityValue.GetString() ?? string.Empty : string.Empty; var state = arguments.TryGetProperty(\"state\", out var stateValue) ? stateValue.GetString() ?? string.Empty : string.Empty; var unit = arguments.TryGetProperty(\"unit\", out var unitValue) ? unitValue.GetString() ?? string.Empty : string.Empty; if (unit is not (\"celsius\" or \"fahrenheit" --- Required demo setting for validating tool settings in tests. It does not affect the weather result. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::GETCURRENTWEATHERTOOL::T25380508"] = "Required demo setting for validating tool settings in tests. It does not affect the weather result." +-- Required demo setting for validating tool settings in tests. It does not affect the weather result.", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var city = arguments.TryGetProperty("city", out var cityValue) ? cityValue.GetString() ?? string.Empty : string.Empty; var state = arguments.TryGetProperty("state", out var stateValue) ? stateValue.GetString() ?? string.Empty : string.Empty; var unit = arguments.TryGetProperty("unit", out var unitValue) ? unitValue.GetString() ?? string.Empty : string.Empty; if (unit is not ("celsius" or "fahrenheit +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::GETCURRENTWEATHERTOOL::T25380508"] = "Required demo setting for validating tool settings in tests. It does not affect the weather result.\", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var city = arguments.TryGetProperty(\"city\", out var cityValue) ? cityValue.GetString() ?? string.Empty : string.Empty; var state = arguments.TryGetProperty(\"state\", out var stateValue) ? stateValue.GetString() ?? string.Empty : string.Empty; var unit = arguments.TryGetProperty(\"unit\", out var unitValue) ? unitValue.GetString() ?? string.Empty : string.Empty; if (unit is not (\"celsius\" or \"fahrenheit" --- Demo Label -UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::GETCURRENTWEATHERTOOL::T3346467484"] = "Demo Label" +-- Demo Label", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "demoLabel" => I18N.I.T("Required demo setting for validating tool settings in tests. It does not affect the weather result.", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var city = arguments.TryGetProperty("city", out var cityValue) ? cityValue.GetString() ?? string.Empty : string.Empty; var state = arguments.TryGetProperty("state", out var stateValue) ? stateValue.GetString() ?? string.Empty : string.Empty; var unit = arguments.TryGetProperty("unit", out var unitValue) ? unitValue.GetString() ?? string.Empty : string.Empty; if (unit is not ("celsius" or "fahrenheit +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::GETCURRENTWEATHERTOOL::T3346467484"] = "Demo Label\", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"demoLabel\" => I18N.I.T(\"Required demo setting for validating tool settings in tests. It does not affect the weather result.\", typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(GetCurrentWeatherTool).Namespace, nameof(GetCurrentWeatherTool)), }; public Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var city = arguments.TryGetProperty(\"city\", out var cityValue) ? cityValue.GetString() ?? string.Empty : string.Empty; var state = arguments.TryGetProperty(\"state\", out var stateValue) ? stateValue.GetString() ?? string.Empty : string.Empty; var unit = arguments.TryGetProperty(\"unit\", out var unitValue) ? unitValue.GetString() ?? string.Empty : string.Empty; if (unit is not (\"celsius\" or \"fahrenheit" -- Tool UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T3517012711"] = "Tool" @@ -6859,6 +6865,87 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T35170 -- Tool description UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T4056470505"] = "Tool description" +-- Optional HTTP timeout for loading a web page in seconds.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), "maxContentCharacters" => I18N.I.T("Optional global truncation limit for extracted Markdown returned to the model.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { if (!TryReadOptionalPositiveInt(settingsValues, "timeoutSeconds", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } if (!TryReadOptionalPositiveInt(settingsValues, "maxContentCharacters", out _, out var contentError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = contentError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var urlText = ReadRequiredString(arguments, "url +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::READWEBPAGETOOL::T1169117578"] = "Optional HTTP timeout for loading a web page in seconds.\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), \"maxContentCharacters\" => I18N.I.T(\"Optional global truncation limit for extracted Markdown returned to the model.\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { if (!TryReadOptionalPositiveInt(settingsValues, \"timeoutSeconds\", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } if (!TryReadOptionalPositiveInt(settingsValues, \"maxContentCharacters\", out _, out var contentError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = contentError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var urlText = ReadRequiredString(arguments, \"url" + +-- Load a single web page, extract its main HTML content, and return Markdown plus metadata for the model.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)); public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "timeoutSeconds" => I18N.I.T("Timeout Seconds", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), "maxContentCharacters" => I18N.I.T("Maximum Content Characters", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for loading a web page in seconds.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), "maxContentCharacters" => I18N.I.T("Optional global truncation limit for extracted Markdown returned to the model.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { if (!TryReadOptionalPositiveInt(settingsValues, "timeoutSeconds", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } if (!TryReadOptionalPositiveInt(settingsValues, "maxContentCharacters", out _, out var contentError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = contentError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var urlText = ReadRequiredString(arguments, "url +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::READWEBPAGETOOL::T2215866777"] = "Load a single web page, extract its main HTML content, and return Markdown plus metadata for the model.\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)); public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"timeoutSeconds\" => I18N.I.T(\"Timeout Seconds\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), \"maxContentCharacters\" => I18N.I.T(\"Maximum Content Characters\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for loading a web page in seconds.\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), \"maxContentCharacters\" => I18N.I.T(\"Optional global truncation limit for extracted Markdown returned to the model.\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { if (!TryReadOptionalPositiveInt(settingsValues, \"timeoutSeconds\", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } if (!TryReadOptionalPositiveInt(settingsValues, \"maxContentCharacters\", out _, out var contentError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = contentError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var urlText = ReadRequiredString(arguments, \"url" + +-- Optional global truncation limit for extracted Markdown returned to the model.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { if (!TryReadOptionalPositiveInt(settingsValues, "timeoutSeconds", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } if (!TryReadOptionalPositiveInt(settingsValues, "maxContentCharacters", out _, out var contentError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = contentError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var urlText = ReadRequiredString(arguments, "url +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::READWEBPAGETOOL::T2765372972"] = "Optional global truncation limit for extracted Markdown returned to the model.\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { if (!TryReadOptionalPositiveInt(settingsValues, \"timeoutSeconds\", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } if (!TryReadOptionalPositiveInt(settingsValues, \"maxContentCharacters\", out _, out var contentError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = contentError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var urlText = ReadRequiredString(arguments, \"url" + +-- Maximum Content Characters", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for loading a web page in seconds.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), "maxContentCharacters" => I18N.I.T("Optional global truncation limit for extracted Markdown returned to the model.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { if (!TryReadOptionalPositiveInt(settingsValues, "timeoutSeconds", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } if (!TryReadOptionalPositiveInt(settingsValues, "maxContentCharacters", out _, out var contentError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = contentError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var urlText = ReadRequiredString(arguments, "url +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::READWEBPAGETOOL::T2860394705"] = "Maximum Content Characters\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for loading a web page in seconds.\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), \"maxContentCharacters\" => I18N.I.T(\"Optional global truncation limit for extracted Markdown returned to the model.\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { if (!TryReadOptionalPositiveInt(settingsValues, \"timeoutSeconds\", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } if (!TryReadOptionalPositiveInt(settingsValues, \"maxContentCharacters\", out _, out var contentError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = contentError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var urlText = ReadRequiredString(arguments, \"url" + +-- Timeout Seconds", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), "maxContentCharacters" => I18N.I.T("Maximum Content Characters", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for loading a web page in seconds.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), "maxContentCharacters" => I18N.I.T("Optional global truncation limit for extracted Markdown returned to the model.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { if (!TryReadOptionalPositiveInt(settingsValues, "timeoutSeconds", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } if (!TryReadOptionalPositiveInt(settingsValues, "maxContentCharacters", out _, out var contentError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = contentError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var urlText = ReadRequiredString(arguments, "url +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::READWEBPAGETOOL::T3510104271"] = "Timeout Seconds\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), \"maxContentCharacters\" => I18N.I.T(\"Maximum Content Characters\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for loading a web page in seconds.\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), \"maxContentCharacters\" => I18N.I.T(\"Optional global truncation limit for extracted Markdown returned to the model.\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { if (!TryReadOptionalPositiveInt(settingsValues, \"timeoutSeconds\", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } if (!TryReadOptionalPositiveInt(settingsValues, \"maxContentCharacters\", out _, out var contentError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = contentError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var urlText = ReadRequiredString(arguments, \"url" + +-- Read Web Page", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)); public string GetDescription() => I18N.I.T("Load a single web page, extract its main HTML content, and return Markdown plus metadata for the model.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)); public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "timeoutSeconds" => I18N.I.T("Timeout Seconds", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), "maxContentCharacters" => I18N.I.T("Maximum Content Characters", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for loading a web page in seconds.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), "maxContentCharacters" => I18N.I.T("Optional global truncation limit for extracted Markdown returned to the model.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { if (!TryReadOptionalPositiveInt(settingsValues, "timeoutSeconds", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } if (!TryReadOptionalPositiveInt(settingsValues, "maxContentCharacters", out _, out var contentError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = contentError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var urlText = ReadRequiredString(arguments, "url +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::READWEBPAGETOOL::T3517638643"] = "Read Web Page\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)); public string GetDescription() => I18N.I.T(\"Load a single web page, extract its main HTML content, and return Markdown plus metadata for the model.\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)); public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"timeoutSeconds\" => I18N.I.T(\"Timeout Seconds\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), \"maxContentCharacters\" => I18N.I.T(\"Maximum Content Characters\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for loading a web page in seconds.\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), \"maxContentCharacters\" => I18N.I.T(\"Optional global truncation limit for extracted Markdown returned to the model.\", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { if (!TryReadOptionalPositiveInt(settingsValues, \"timeoutSeconds\", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } if (!TryReadOptionalPositiveInt(settingsValues, \"maxContentCharacters\", out _, out var contentError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = contentError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { var urlText = ReadRequiredString(arguments, \"url" + +-- Default Categories", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Default Engines", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Maximum Results", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Timeout Seconds", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "baseUrl" => I18N.I.T("Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultLanguage" => I18N.I.T("Optional fallback language code when the model does not provide a language.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Optional safe search policy sent to SearXNG when configured.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T1254458306"] = "Default Categories\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Default Engines\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Maximum Results\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Timeout Seconds\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"baseUrl\" => I18N.I.T(\"Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultLanguage\" => I18N.I.T(\"Optional fallback language code when the model does not provide a language.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Optional safe search policy sent to SearXNG when configured.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Optional comma-separated default categories. Do not set this together with default engines.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- Optional safe search policy sent to SearXNG when configured.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T1327402904"] = "Optional safe search policy sent to SearXNG when configured.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Optional comma-separated default categories. Do not set this together with default engines.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T1401266403"] = "Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T1539252250"] = "Optional comma-separated default categories. Do not set this together with default engines.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- Search the web with a configured SearXNG instance and return structured JSON results for the model. When deeper content is needed, use Read Web Page on relevant result URLs.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "baseUrl" => I18N.I.T("SearXNG URL", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultLanguage" => I18N.I.T("Default Language", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Default Safe Search", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Default Categories", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Default Engines", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Maximum Results", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Timeout Seconds", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "baseUrl" => I18N.I.T("Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultLanguage" => I18N.I.T("Optional fallback language code when the model does not provide a language.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Optional safe search policy sent to SearXNG when configured.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T1563842161"] = "Search the web with a configured SearXNG instance and return structured JSON results for the model. When deeper content is needed, use Read Web Page on relevant result URLs.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"baseUrl\" => I18N.I.T(\"SearXNG URL\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultLanguage\" => I18N.I.T(\"Default Language\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Default Safe Search\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Default Categories\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Default Engines\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Maximum Results\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Timeout Seconds\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"baseUrl\" => I18N.I.T(\"Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultLanguage\" => I18N.I.T(\"Optional fallback language code when the model does not provide a language.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Optional safe search policy sent to SearXNG when configured.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Optional comma-separated default categories. Do not set this together with default engines.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- Timeout Seconds", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "baseUrl" => I18N.I.T("Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultLanguage" => I18N.I.T("Optional fallback language code when the model does not provide a language.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Optional safe search policy sent to SearXNG when configured.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T186659624"] = "Timeout Seconds\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"baseUrl\" => I18N.I.T(\"Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultLanguage\" => I18N.I.T(\"Optional fallback language code when the model does not provide a language.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Optional safe search policy sent to SearXNG when configured.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Optional comma-separated default categories. Do not set this together with default engines.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- Default categories and default engines cannot both be set for the web search tool.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool))); var defaultLimit = ReadOptionalPositiveIntSetting(context.SettingsValues, "maxResults +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T2087438861"] = "Default categories and default engines cannot both be set for the web search tool.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool))); var defaultLimit = ReadOptionalPositiveIntSetting(context.SettingsValues, \"maxResults" + +-- The configured SearXNG URL must start with http:// or https://.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); return false; } var basePath = parsedUri.AbsolutePath.TrimEnd('/'); if (basePath.EndsWith("/search", StringComparison.OrdinalIgnoreCase)) basePath = basePath[..^"/search".Length]; var normalizedPath = $"{basePath}/search"; var builder = new UriBuilder(parsedUri) { Path = normalizedPath, Query = string.Empty, Fragment = string.Empty, }; searchUri = builder.Uri; return true; } private static Uri BuildRequestUri(Uri searchUri, IEnumerable> 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 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. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T212620884"] = "The configured SearXNG URL must start with http:// or https://.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); return false; } var basePath = parsedUri.AbsolutePath.TrimEnd('/'); if (basePath.EndsWith(\"/search\", StringComparison.OrdinalIgnoreCase)) basePath = basePath[..^\"/search\".Length]; var normalizedPath = $\"{basePath}/search\"; var builder = new UriBuilder(parsedUri) { Path = normalizedPath, Query = string.Empty, Fragment = string.Empty, }; searchUri = builder.Uri; return true; } private static Uri BuildRequestUri(Uri searchUri, IEnumerable> 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 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." + +-- Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T2170342710"] = "Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- A SearXNG URL is required.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); return false; } if (!Uri.TryCreate(rawUrl.Trim(), UriKind.Absolute, out var parsedUri)) { error = I18N.I.T("The configured SearXNG URL is not a valid absolute URL.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); return false; } if (parsedUri.Scheme is not ("http" or "https +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T2420801571"] = "A SearXNG URL is required.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); return false; } if (!Uri.TryCreate(rawUrl.Trim(), UriKind.Absolute, out var parsedUri)) { error = I18N.I.T(\"The configured SearXNG URL is not a valid absolute URL.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); return false; } if (parsedUri.Scheme is not (\"http\" or \"https" + +-- Default Engines", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Maximum Results", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Timeout Seconds", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "baseUrl" => I18N.I.T("Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultLanguage" => I18N.I.T("Optional fallback language code when the model does not provide a language.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Optional safe search policy sent to SearXNG when configured.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T2435794648"] = "Default Engines\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Maximum Results\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Timeout Seconds\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"baseUrl\" => I18N.I.T(\"Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultLanguage\" => I18N.I.T(\"Optional fallback language code when the model does not provide a language.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Optional safe search policy sent to SearXNG when configured.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Optional comma-separated default categories. Do not set this together with default engines.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- Default categories and default engines cannot both be set for the web search tool.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }); } if (!TryReadOptionalPositiveInt(settingsValues, "maxResults", out _, out var maxResultsError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = maxResultsError, }); } if (!TryReadOptionalPositiveInt(settingsValues, "timeoutSeconds", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { context.SettingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out var searchUri, out var uriError); if (!isValidBaseUrl) throw new InvalidOperationException(uriError); var query = ReadRequiredString(arguments, "query +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T2471183191"] = "Default categories and default engines cannot both be set for the web search tool.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }); } if (!TryReadOptionalPositiveInt(settingsValues, \"maxResults\", out _, out var maxResultsError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = maxResultsError, }); } if (!TryReadOptionalPositiveInt(settingsValues, \"timeoutSeconds\", out _, out var timeoutError)) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = timeoutError, }); } return Task.FromResult(null); } public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) { context.SettingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out var searchUri, out var uriError); if (!isValidBaseUrl) throw new InvalidOperationException(uriError); var query = ReadRequiredString(arguments, \"query" + +-- Web Search", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); public string GetDescription() => I18N.I.T("Search the web with a configured SearXNG instance and return structured JSON results for the model. When deeper content is needed, use Read Web Page on relevant result URLs.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "baseUrl" => I18N.I.T("SearXNG URL", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultLanguage" => I18N.I.T("Default Language", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Default Safe Search", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Default Categories", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Default Engines", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Maximum Results", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Timeout Seconds", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "baseUrl" => I18N.I.T("Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultLanguage" => I18N.I.T("Optional fallback language code when the model does not provide a language.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Optional safe search policy sent to SearXNG when configured.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T2590432104"] = "Web Search\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); public string GetDescription() => I18N.I.T(\"Search the web with a configured SearXNG instance and return structured JSON results for the model. When deeper content is needed, use Read Web Page on relevant result URLs.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"baseUrl\" => I18N.I.T(\"SearXNG URL\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultLanguage\" => I18N.I.T(\"Default Language\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Default Safe Search\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Default Categories\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Default Engines\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Maximum Results\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Timeout Seconds\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"baseUrl\" => I18N.I.T(\"Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultLanguage\" => I18N.I.T(\"Optional fallback language code when the model does not provide a language.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Optional safe search policy sent to SearXNG when configured.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Optional comma-separated default categories. Do not set this together with default engines.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- SearXNG URL", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultLanguage" => I18N.I.T("Default Language", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Default Safe Search", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Default Categories", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Default Engines", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Maximum Results", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Timeout Seconds", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "baseUrl" => I18N.I.T("Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultLanguage" => I18N.I.T("Optional fallback language code when the model does not provide a language.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Optional safe search policy sent to SearXNG when configured.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T2707478507"] = "SearXNG URL\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultLanguage\" => I18N.I.T(\"Default Language\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Default Safe Search\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Default Categories\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Default Engines\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Maximum Results\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Timeout Seconds\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"baseUrl\" => I18N.I.T(\"Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultLanguage\" => I18N.I.T(\"Optional fallback language code when the model does not provide a language.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Optional safe search policy sent to SearXNG when configured.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Optional comma-separated default categories. Do not set this together with default engines.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- Optional fallback language code when the model does not provide a language.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Optional safe search policy sent to SearXNG when configured.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T2911071656"] = "Optional fallback language code when the model does not provide a language.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Optional safe search policy sent to SearXNG when configured.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Optional comma-separated default categories. Do not set this together with default engines.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T2953585467"] = "Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- Maximum Results", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Timeout Seconds", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "baseUrl" => I18N.I.T("Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultLanguage" => I18N.I.T("Optional fallback language code when the model does not provide a language.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Optional safe search policy sent to SearXNG when configured.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T3332435511"] = "Maximum Results\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Timeout Seconds\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"baseUrl\" => I18N.I.T(\"Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultLanguage\" => I18N.I.T(\"Optional fallback language code when the model does not provide a language.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Optional safe search policy sent to SearXNG when configured.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Optional comma-separated default categories. Do not set this together with default engines.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- The configured SearXNG URL is not a valid absolute URL.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); return false; } if (parsedUri.Scheme is not ("http" or "https +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T371406570"] = "The configured SearXNG URL is not a valid absolute URL.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); return false; } if (parsedUri.Scheme is not (\"http\" or \"https" + +-- Default Safe Search", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Default Categories", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Default Engines", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Maximum Results", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Timeout Seconds", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "baseUrl" => I18N.I.T("Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultLanguage" => I18N.I.T("Optional fallback language code when the model does not provide a language.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Optional safe search policy sent to SearXNG when configured.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T3780386928"] = "Default Safe Search\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Default Categories\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Default Engines\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Maximum Results\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Timeout Seconds\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"baseUrl\" => I18N.I.T(\"Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultLanguage\" => I18N.I.T(\"Optional fallback language code when the model does not provide a language.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Optional safe search policy sent to SearXNG when configured.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Optional comma-separated default categories. Do not set this together with default engines.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- Default Language", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Default Safe Search", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Default Categories", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Default Engines", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Maximum Results", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Timeout Seconds", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { "baseUrl" => I18N.I.T("Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultLanguage" => I18N.I.T("Optional fallback language code when the model does not provide a language.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Optional safe search policy sent to SearXNG when configured.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T54221234"] = "Default Language\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Default Safe Search\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Default Categories\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Default Engines\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Maximum Results\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Timeout Seconds\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch { \"baseUrl\" => I18N.I.T(\"Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultLanguage\" => I18N.I.T(\"Optional fallback language code when the model does not provide a language.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Optional safe search policy sent to SearXNG when configured.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Optional comma-separated default categories. Do not set this together with default engines.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + +-- Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultLanguage" => I18N.I.T("Optional fallback language code when the model does not provide a language.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultSafeSearch" => I18N.I.T("Optional safe search policy sent to SearXNG when configured.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultCategories" => I18N.I.T("Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue("baseUrl", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault("defaultCategories +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::SEARXNGWEBSEARCHTOOL::T54269506"] = "Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultLanguage\" => I18N.I.T(\"Optional fallback language code when the model does not provide a language.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultSafeSearch\" => I18N.I.T(\"Optional safe search policy sent to SearXNG when configured.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultCategories\" => I18N.I.T(\"Optional comma-separated default categories. Do not set this together with default engines.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"defaultEngines\" => I18N.I.T(\"Optional comma-separated default engines. Do not set this together with default categories.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"maxResults\" => I18N.I.T(\"Optional default maximum number of results returned to the model when the model does not provide a limit.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), \"timeoutSeconds\" => I18N.I.T(\"Optional HTTP timeout for the search request in seconds.\", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), }; public Task ValidateConfigurationAsync( ToolDefinition definition, IReadOnlyDictionary settingsValues, CancellationToken token = default) { settingsValues.TryGetValue(\"baseUrl\", out var baseUrl); var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); if (!isValidBaseUrl) { return Task.FromResult(new ToolConfigurationState { IsConfigured = false, Message = uriError, }); } var hasDefaultCategories = !string.IsNullOrWhiteSpace(settingsValues.GetValueOrDefault(\"defaultCategories" + -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::TOOLS::USERFILE::T185447014"] = "Pandoc Installation" diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index 85b4588a..88e1ec20 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 Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; @@ -92,7 +93,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable // Get the preselected chat template: this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT); this.userInput = this.currentChatTemplate.PredefinedUserPrompt; - this.selectedToolIds = this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT); + this.selectedToolIds = ToolSelectionRules.NormalizeSelection(this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT)); // Apply template's file attachments, if any: foreach (var attachment in this.currentChatTemplate.FileAttachments) @@ -610,7 +611,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable { this.StateHasChanged(); this.ChatThread!.RuntimeComponent = Tools.Components.CHAT; - this.ChatThread.RuntimeSelectedToolIds = [..this.selectedToolIds]; + 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 @@ -643,7 +644,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable private Task SelectedToolIdsChanged(HashSet updatedToolIds) { - this.selectedToolIds = updatedToolIds; + this.selectedToolIds = ToolSelectionRules.NormalizeSelection(updatedToolIds); return Task.CompletedTask; } diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor index e47f9e79..c10c1983 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor @@ -1,7 +1,7 @@ @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.") diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor.cs index 56f75474..c978d16c 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor.cs @@ -34,6 +34,7 @@ public partial class SettingsPanelTools : SettingsPanelBase 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)))) }; diff --git a/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor.cs b/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor.cs index 0fed200d..a63f8734 100644 --- a/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor.cs +++ b/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor.cs @@ -39,5 +39,5 @@ public partial class ToolDefaultsConfiguration : MSGComponentBase private HashSet GetSelectedValues() => this.SettingsManager.GetDefaultToolIds(this.Component); - private void UpdateSelection(HashSet values) => this.SettingsManager.ConfigurationData.Tools.DefaultToolIdsByComponent[this.Component.ToString()] = [..values]; + 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 index 5e4b22f5..a98f02e2 100644 --- a/app/MindWork AI Studio/Components/ToolSelection.razor +++ b/app/MindWork AI Studio/Components/ToolSelection.razor @@ -39,10 +39,11 @@ { var isSelected = this.SelectedToolIds.Contains(item.Definition.Id); var isConfigured = item.ConfigurationState.IsConfigured; + var dependencyHint = this.GetDependencyHint(item.Definition.Id); - + @item.Implementation.GetDisplayName() @@ -52,7 +53,11 @@ @if (!isConfigured) { - @T("Required settings are missing. Configure this tool before enabling it.") + @(string.IsNullOrWhiteSpace(item.ConfigurationState.Message) ? T("Required settings are missing. Configure this tool before enabling it.") : item.ConfigurationState.Message) + } + @if (!string.IsNullOrWhiteSpace(dependencyHint)) + { + @dependencyHint } } diff --git a/app/MindWork AI Studio/Components/ToolSelection.razor.cs b/app/MindWork AI Studio/Components/ToolSelection.razor.cs index dca52ef4..7f96fb06 100644 --- a/app/MindWork AI Studio/Components/ToolSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ToolSelection.razor.cs @@ -37,6 +37,12 @@ public partial class ToolSelection : MSGComponentBase private bool showSelection; private IReadOnlyList catalog = []; + protected override void OnParametersSet() + { + this.SelectedToolIds = ToolSelectionRules.NormalizeSelection(this.SelectedToolIds); + base.OnParametersSet(); + } + private bool SupportsTools => this.LLMProvider != AIStudio.Settings.Provider.NONE && this.LLMProvider.GetModelCapabilities().Contains(Capability.CHAT_COMPLETION_API) && @@ -59,10 +65,24 @@ public partial class ToolSelection : MSGComponentBase else updated.Remove(toolId); + updated = ToolSelectionRules.NormalizeSelection(updated); this.SelectedToolIds = updated; await this.SelectedToolIdsChanged.InvokeAsync(updated); } + private bool IsSelectionLockedByDependency(string toolId) => ToolSelectionRules.IsRequiredBySelectedTools(toolId, this.SelectedToolIds); + + private string? GetDependencyHint(string toolId) + { + if (toolId == ToolSelectionRules.WEB_SEARCH_TOOL_ID) + return this.T("Enabling this tool also enables Read Web Page."); + + if (this.IsSelectionLockedByDependency(toolId)) + return this.T("This tool is currently required because Web Search is enabled."); + + return null; + } + private async Task OpenSettings(string toolId) { var parameters = new DialogParameters diff --git a/app/MindWork AI Studio/Pages/Settings.razor b/app/MindWork AI Studio/Pages/Settings.razor index 16542dfb..56ec6e99 100644 --- a/app/MindWork AI Studio/Pages/Settings.razor +++ b/app/MindWork AI Studio/Pages/Settings.razor @@ -21,6 +21,7 @@ } + @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 3d180bde..e83fda0b 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -171,6 +171,8 @@ internal sealed class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 43b10fcf..25678efe 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -5,6 +5,7 @@ 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; @@ -349,7 +350,7 @@ public sealed class SettingsManager { var key = component.ToString(); if (this.ConfigurationData.Tools.DefaultToolIdsByComponent.TryGetValue(key, out var toolIds)) - return [..toolIds]; + return ToolSelectionRules.NormalizeSelection(toolIds); return []; } diff --git a/app/MindWork AI Studio/Tools/HTMLParser.cs b/app/MindWork AI Studio/Tools/HTMLParser.cs index 4f9dca2a..e627c535 100644 --- a/app/MindWork AI Studio/Tools/HTMLParser.cs +++ b/app/MindWork AI Studio/Tools/HTMLParser.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Text; using HtmlAgilityPack; @@ -23,10 +24,8 @@ public sealed class HTMLParser /// The web content as text. public async Task 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; } /// @@ -36,14 +35,42 @@ public sealed class HTMLParser /// The web content as an HTML string. public async Task 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 LoadWebPageAsync(Uri url, CancellationToken token = default, int timeoutSeconds = 30) + { + using var httpClient = new HttpClient + { + Timeout = Timeout.InfiniteTimeSpan, + }; + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token); + timeoutCts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds)); + using var response = await httpClient.GetAsync(url, timeoutCts.Token); + response.EnsureSuccessStatusCode(); + + var html = await response.Content.ReadAsStringAsync(token); + var document = new HtmlDocument(); + document.LoadHtml(html); + + return new HTMLParserWebPage + { + RequestedUrl = url, + FinalUrl = response.RequestMessage?.RequestUri ?? url, + ContentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty, + Document = document, + }; + } + + public string ExtractTitle(HtmlDocument document) + { + var title = document.DocumentNode.SelectSingleNode("//title")?.InnerText?.Trim(); + return WebUtility.HtmlDecode(title ?? string.Empty).Trim(); + } + /// /// Converts HTML content to the Markdown format. /// @@ -54,4 +81,15 @@ public sealed class HTMLParser var markdownConverter = new Converter(MARKDOWN_PARSER_CONFIG); return markdownConverter.Convert(html); } -} \ No newline at end of file +} + +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; } +} diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/IToolImplementation.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/IToolImplementation.cs index a425a6f5..9d0d8669 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/IToolImplementation.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/IToolImplementation.cs @@ -22,6 +22,11 @@ public interface IToolImplementation public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => this.T(fieldDefinition.Description); + public Task ValidateConfigurationAsync( + ToolDefinition definition, + IReadOnlyDictionary settingsValues, + CancellationToken token = default) => Task.FromResult(null); + public Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default); public string FormatTraceResult(string rawResult) => rawResult; diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ReadWebPageTool.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ReadWebPageTool.cs new file mode 100644 index 00000000..ef5c8d0b --- /dev/null +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ReadWebPageTool.cs @@ -0,0 +1,215 @@ +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Nodes; + +using AIStudio.Tools; +using AIStudio.Tools.PluginSystem; + +using HtmlAgilityPack; + +namespace AIStudio.Tools.ToolCallingSystem; + +public sealed class ReadWebPageTool(HTMLParser htmlParser) : IToolImplementation +{ + private const int DEFAULT_TIMEOUT_SECONDS = 30; + private const int DEFAULT_MAX_CONTENT_CHARACTERS = 12000; + private const int MAX_TRACE_LENGTH = 4000; + + private static readonly string[] REMOVED_NODE_XPATHS = + [ + "//script", + "//style", + "//noscript", + "//nav", + "//footer", + "//aside", + "//form", + "//iframe", + "//*[@role='navigation']", + "//*[@role='contentinfo']", + "//*[@role='complementary']" + ]; + + public string ImplementationKey => ToolSelectionRules.READ_WEB_PAGE_TOOL_ID; + + public string Icon => Icons.Material.Filled.Article; + + public IReadOnlySet SensitiveTraceArgumentNames => new HashSet(StringComparer.Ordinal); + + public string GetDisplayName() => I18N.I.T("Read Web Page", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)); + + public string GetDescription() => I18N.I.T("Load a single web page, extract its main HTML content, and return Markdown plus metadata for the model.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)); + + public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch + { + "timeoutSeconds" => I18N.I.T("Timeout Seconds", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), + "maxContentCharacters" => I18N.I.T("Maximum Content Characters", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), + _ => I18N.I.T(fieldDefinition.Title, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), + }; + + public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch + { + "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for loading a web page in seconds.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), + "maxContentCharacters" => I18N.I.T("Optional global truncation limit for extracted Markdown returned to the model.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), + _ => I18N.I.T(fieldDefinition.Description, typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)), + }; + + public Task ValidateConfigurationAsync( + ToolDefinition definition, + IReadOnlyDictionary settingsValues, + CancellationToken token = default) + { + if (!TryReadOptionalPositiveInt(settingsValues, "timeoutSeconds", out _, out var timeoutError)) + { + return Task.FromResult(new ToolConfigurationState + { + IsConfigured = false, + Message = timeoutError, + }); + } + + if (!TryReadOptionalPositiveInt(settingsValues, "maxContentCharacters", out _, out var contentError)) + { + return Task.FromResult(new ToolConfigurationState + { + IsConfigured = false, + Message = contentError, + }); + } + + return Task.FromResult(null); + } + + public async Task 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 = ReadOptionalPositiveIntSetting(context.SettingsValues, "timeoutSeconds") ?? DEFAULT_TIMEOUT_SECONDS; + var maxContentCharacters = ReadOptionalPositiveIntSetting(context.SettingsValues, "maxContentCharacters") ?? DEFAULT_MAX_CONTENT_CHARACTERS; + + HTMLParserWebPage page; + try + { + page = await htmlParser.LoadWebPageAsync(url, token, timeoutSeconds); + } + catch (OperationCanceledException) when (!token.IsCancellationRequested) + { + throw new TimeoutException($"Loading the web page timed out after {timeoutSeconds} seconds."); + } + catch (HttpRequestException exception) + { + throw new InvalidOperationException($"Loading the web page failed: {exception.Message}", exception); + } + + if (!IsSupportedHtmlContentType(page.ContentType)) + throw new InvalidOperationException($"Unsupported content type '{page.ContentType}'. Only HTML pages are supported."); + + var document = page.Document; + var title = htmlParser.ExtractTitle(document); + var contentRoot = document.DocumentNode.SelectSingleNode("//main") ?? + document.DocumentNode.SelectSingleNode("//article") ?? + document.DocumentNode.SelectSingleNode("//body") ?? + document.DocumentNode; + + RemoveNoiseNodes(contentRoot); + + var markdown = htmlParser.ParseToMarkdown(contentRoot.InnerHtml).Trim(); + var warnings = new JsonArray(); + if (string.IsNullOrWhiteSpace(title)) + warnings.Add("No title could be extracted from the page."); + + if (string.IsNullOrWhiteSpace(markdown)) + warnings.Add("The extracted page content is empty."); + else if (markdown.Length < 200) + warnings.Add("The extracted page content is very short and may be incomplete."); + + if (markdown.Length > maxContentCharacters) + { + markdown = markdown[..maxContentCharacters].TrimEnd(); + warnings.Add($"The extracted page content was truncated to {maxContentCharacters} characters."); + } + + return new ToolExecutionResult + { + JsonContent = new JsonObject + { + ["url"] = page.RequestedUrl.ToString(), + ["final_url"] = page.FinalUrl.ToString(), + ["title"] = title, + ["markdown"] = markdown, + ["warnings"] = warnings, + }, + }; + } + + public string FormatTraceResult(string rawResult) + { + if (rawResult.Length <= MAX_TRACE_LENGTH) + return rawResult; + + return $"{rawResult[..MAX_TRACE_LENGTH]}..."; + } + + private static void RemoveNoiseNodes(HtmlNode rootNode) + { + foreach (var xpath in REMOVED_NODE_XPATHS) + { + var nodes = rootNode.SelectNodes(xpath); + if (nodes is null) + continue; + + foreach (var node in nodes.ToList()) + node.Remove(); + } + } + + private static bool IsSupportedHtmlContentType(string? contentType) => + string.IsNullOrWhiteSpace(contentType) || + contentType.StartsWith("text/html", StringComparison.OrdinalIgnoreCase) || + contentType.StartsWith("application/xhtml+xml", StringComparison.OrdinalIgnoreCase); + + 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 static int? ReadOptionalPositiveIntSetting(IReadOnlyDictionary 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; + } + + private static bool TryReadOptionalPositiveInt( + IReadOnlyDictionary settingsValues, + string key, + 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 = I18N.I.T($"The setting '{key}' must be a positive integer.", typeof(ReadWebPageTool).Namespace, nameof(ReadWebPageTool)); + return false; + } +} diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/SearXNGWebSearchTool.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/SearXNGWebSearchTool.cs new file mode 100644 index 00000000..354cc4bd --- /dev/null +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/SearXNGWebSearchTool.cs @@ -0,0 +1,419 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; + +using AIStudio.Tools.PluginSystem; + +namespace AIStudio.Tools.ToolCallingSystem; + +public sealed class SearXNGWebSearchTool : IToolImplementation +{ + private const int DEFAULT_MAX_RESULTS = 5; + private const int DEFAULT_TIMEOUT_SECONDS = 20; + private const int MAX_TRACE_LENGTH = 4000; + + public string ImplementationKey => "web_search"; + + public string Icon => Icons.Material.Filled.Language; + + public IReadOnlySet SensitiveTraceArgumentNames => new HashSet(StringComparer.Ordinal); + + public string GetDisplayName() => I18N.I.T("Web Search", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); + + public string GetDescription() => I18N.I.T("Search the web with a configured SearXNG instance and return structured JSON results for the model. When deeper content is needed, use Read Web Page on relevant result URLs.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); + + public string GetSettingsFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch + { + "baseUrl" => I18N.I.T("SearXNG URL", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + "defaultLanguage" => I18N.I.T("Default Language", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + "defaultSafeSearch" => I18N.I.T("Default Safe Search", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + "defaultCategories" => I18N.I.T("Default Categories", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + "defaultEngines" => I18N.I.T("Default Engines", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + "maxResults" => I18N.I.T("Maximum Results", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + "timeoutSeconds" => I18N.I.T("Timeout Seconds", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + _ => I18N.I.T(fieldDefinition.Title, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + }; + + public string GetSettingsFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => fieldName switch + { + "baseUrl" => I18N.I.T("Base URL of the SearXNG instance. You can enter either the instance root URL or the /search endpoint.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + "defaultLanguage" => I18N.I.T("Optional fallback language code when the model does not provide a language.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + "defaultSafeSearch" => I18N.I.T("Optional safe search policy sent to SearXNG when configured.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + "defaultCategories" => I18N.I.T("Optional comma-separated default categories. Do not set this together with default engines.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + "defaultEngines" => I18N.I.T("Optional comma-separated default engines. Do not set this together with default categories.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + "maxResults" => I18N.I.T("Optional default maximum number of results returned to the model when the model does not provide a limit.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + "timeoutSeconds" => I18N.I.T("Optional HTTP timeout for the search request in seconds.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + _ => I18N.I.T(fieldDefinition.Description, typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + }; + + public Task ValidateConfigurationAsync( + ToolDefinition definition, + IReadOnlyDictionary settingsValues, + CancellationToken token = default) + { + settingsValues.TryGetValue("baseUrl", out var baseUrl); + var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out _, out var uriError); + if (!isValidBaseUrl) + { + return Task.FromResult(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(new ToolConfigurationState + { + IsConfigured = false, + Message = I18N.I.T("Default categories and default engines cannot both be set for the web search tool.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)), + }); + } + + if (!TryReadOptionalPositiveInt(settingsValues, "maxResults", out _, out var maxResultsError)) + { + return Task.FromResult(new ToolConfigurationState + { + IsConfigured = false, + Message = maxResultsError, + }); + } + + if (!TryReadOptionalPositiveInt(settingsValues, "timeoutSeconds", out _, out var timeoutError)) + { + return Task.FromResult(new ToolConfigurationState + { + IsConfigured = false, + Message = timeoutError, + }); + } + + return Task.FromResult(null); + } + + public async Task ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default) + { + context.SettingsValues.TryGetValue("baseUrl", out var baseUrl); + var isValidBaseUrl = TryNormalizeSearchUri(baseUrl ?? string.Empty, out var searchUri, out var uriError); + if (!isValidBaseUrl) + 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(I18N.I.T("Default categories and default engines cannot both be set for the web search tool.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool))); + + var defaultLimit = ReadOptionalPositiveIntSetting(context.SettingsValues, "maxResults") ?? DEFAULT_MAX_RESULTS; + var effectiveLimit = requestedLimit ?? defaultLimit; + var timeoutSeconds = ReadOptionalPositiveIntSetting(context.SettingsValues, "timeoutSeconds") ?? DEFAULT_TIMEOUT_SECONDS; + + var queryParameters = new List> + { + new("q", query), + new("format", "json"), + }; + + if (categories.Count > 0) + queryParameters.Add(new KeyValuePair("categories", string.Join(",", categories))); + + if (engines.Count > 0) + queryParameters.Add(new KeyValuePair("engines", string.Join(",", engines))); + + if (!string.IsNullOrWhiteSpace(language)) + queryParameters.Add(new KeyValuePair("language", language)); + + if (!string.IsNullOrWhiteSpace(timeRange)) + queryParameters.Add(new KeyValuePair("time_range", timeRange)); + + if (page is not null) + queryParameters.Add(new KeyValuePair("pageno", page.Value.ToString())); + + if (!string.IsNullOrWhiteSpace(safeSearch)) + queryParameters.Add(new KeyValuePair("safesearch", safeSearch)); + + using var httpClient = new HttpClient + { + Timeout = Timeout.InfiniteTimeSpan, + }; + using var request = new HttpRequestMessage(HttpMethod.Get, BuildRequestUri(searchUri, queryParameters)); + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token); + timeoutCts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds)); + + using var response = await SendAsync(httpClient, request, timeoutCts.Token, timeoutSeconds, token); + var responseBody = await response.Content.ReadAsStringAsync(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 resultArray = responseObject["results"] as JsonArray; + if (resultArray is not null && resultArray.Count > effectiveLimit) + { + var truncatedResults = new JsonArray(); + foreach (var result in resultArray.Take(effectiveLimit)) + truncatedResults.Add(result?.DeepClone()); + + responseObject["results"] = truncatedResults; + } + + var requestJson = new JsonObject + { + ["query"] = query, + ["format"] = "json", + ["limit"] = effectiveLimit, + }; + + if (categories.Count > 0) + requestJson["categories"] = BuildJsonArray(categories); + + if (engines.Count > 0) + requestJson["engines"] = BuildJsonArray(engines); + + if (!string.IsNullOrWhiteSpace(language)) + requestJson["language"] = language; + + if (!string.IsNullOrWhiteSpace(timeRange)) + requestJson["time_range"] = timeRange; + + if (page is not null) + requestJson["page"] = page.Value; + + if (!string.IsNullOrWhiteSpace(safeSearch)) + requestJson["safesearch"] = safeSearch; + + return new ToolExecutionResult + { + JsonContent = new JsonObject + { + ["request"] = requestJson, + ["response"] = responseObject, + }, + }; + } + + public string FormatTraceResult(string rawResult) + { + if (rawResult.Length <= MAX_TRACE_LENGTH) + return rawResult; + + return $"{rawResult[..MAX_TRACE_LENGTH]}..."; + } + + 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 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(); + 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 JsonArray BuildJsonArray(IEnumerable values) + { + var array = new JsonArray(); + foreach (var value in values) + array.Add(value); + + return array; + } + + private static List SplitCommaSeparatedValues(string? value) => value? + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct(StringComparer.Ordinal) + .ToList() ?? []; + + private static int? ReadOptionalPositiveIntSetting(IReadOnlyDictionary 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; + } + + private static bool TryReadOptionalPositiveInt( + IReadOnlyDictionary settingsValues, + string key, + 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 = I18N.I.T($"The setting '{key}' must be a positive integer.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); + return false; + } + + private static bool TryNormalizeSearchUri(string rawUrl, out Uri searchUri, out string error) + { + searchUri = null!; + error = string.Empty; + + if (string.IsNullOrWhiteSpace(rawUrl)) + { + error = I18N.I.T("A SearXNG URL is required.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); + return false; + } + + if (!Uri.TryCreate(rawUrl.Trim(), UriKind.Absolute, out var parsedUri)) + { + error = I18N.I.T("The configured SearXNG URL is not a valid absolute URL.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); + return false; + } + + if (parsedUri.Scheme is not ("http" or "https")) + { + error = I18N.I.T("The configured SearXNG URL must start with http:// or https://.", typeof(SearXNGWebSearchTool).Namespace, nameof(SearXNGWebSearchTool)); + return false; + } + + var basePath = parsedUri.AbsolutePath.TrimEnd('/'); + if (basePath.EndsWith("/search", StringComparison.OrdinalIgnoreCase)) + basePath = basePath[..^"/search".Length]; + + var normalizedPath = $"{basePath}/search"; + var builder = new UriBuilder(parsedUri) + { + Path = normalizedPath, + Query = string.Empty, + Fragment = string.Empty, + }; + searchUri = builder.Uri; + return true; + } + + private static Uri BuildRequestUri(Uri searchUri, IEnumerable> 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 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 (Exception exception) + { + throw new InvalidOperationException($"The SearXNG request failed: {exception.Message}", exception); + } + } +} diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutionModels.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutionModels.cs index ebd61b1f..f90cf538 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutionModels.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutionModels.cs @@ -79,6 +79,8 @@ public sealed class ToolConfigurationState public bool IsConfigured { get; init; } public List MissingRequiredFields { get; init; } = []; + + public string Message { get; init; } = string.Empty; } public sealed class ToolCatalogItem diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolRegistry.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolRegistry.cs index 1b99ea66..ea4732d2 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolRegistry.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolRegistry.cs @@ -100,7 +100,7 @@ public sealed class ToolRegistry { Definition = definition, Implementation = implementation, - ConfigurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition), + ConfigurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition, implementation), }); } @@ -119,7 +119,7 @@ public sealed class ToolRegistry if (!modelCapabilities.Contains(Capability.CHAT_COMPLETION_API) || !modelCapabilities.Contains(Capability.FUNCTION_CALLING)) return []; - var selectedToolIdSet = selectedToolIds.ToHashSet(StringComparer.Ordinal); + var selectedToolIdSet = ToolSelectionRules.NormalizeSelection(selectedToolIds); 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) @@ -127,7 +127,7 @@ public sealed class ToolRegistry if (!this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation)) continue; - var configurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition); + var configurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition, implementation); if (!configurationState.IsConfigured) continue; diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs new file mode 100644 index 00000000..fc5b9d39 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; + +namespace AIStudio.Tools.ToolCallingSystem; + +public static class ToolSelectionRules +{ + public const string WEB_SEARCH_TOOL_ID = "web_search"; + public const string READ_WEB_PAGE_TOOL_ID = "read_web_page"; + + public static HashSet NormalizeSelection(IEnumerable selectedToolIds) + { + var normalized = selectedToolIds.ToHashSet(StringComparer.Ordinal); + if (normalized.Contains(WEB_SEARCH_TOOL_ID)) + normalized.Add(READ_WEB_PAGE_TOOL_ID); + + return normalized; + } + + public static bool IsRequiredBySelectedTools(string toolId, IEnumerable selectedToolIds) + { + var normalized = NormalizeSelection(selectedToolIds); + return toolId == READ_WEB_PAGE_TOOL_ID && normalized.Contains(WEB_SEARCH_TOOL_ID); + } +} diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSettingsService.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSettingsService.cs index aec3617c..fa1b592f 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSettingsService.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSettingsService.cs @@ -29,7 +29,10 @@ public sealed class ToolSettingsService(SettingsManager settingsManager, RustSer return values; } - public async Task GetConfigurationStateAsync(ToolDefinition definition) + public async Task GetConfigurationStateAsync( + ToolDefinition definition, + IToolImplementation? implementation = null, + CancellationToken token = default) { var values = await this.GetSettingsAsync(definition); var missing = new List(); @@ -39,10 +42,25 @@ public sealed class ToolSettingsService(SettingsManager settingsManager, RustSer 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 = missing.Count == 0, - MissingRequiredFields = missing, + IsConfigured = true, }; } diff --git a/app/MindWork AI Studio/wwwroot/tool_definitions/read_web_page.json b/app/MindWork AI Studio/wwwroot/tool_definitions/read_web_page.json new file mode 100644 index 00000000..04c78ef8 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/tool_definitions/read_web_page.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "id": "read_web_page", + "implementationKey": "read_web_page", + "visibleIn": { + "chat": true, + "assistants": true + }, + "settingsSchema": { + "type": "object", + "properties": { + "timeoutSeconds": { + "type": "string", + "secret": false + }, + "maxContentCharacters": { + "type": "string", + "secret": false + } + }, + "required": [] + }, + "function": { + "name": "read_web_page", + "description": "Load a single HTTP or HTTPS web page, extract its main content as Markdown, and return structured JSON metadata for the model.", + "strict": true, + "parameters": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "The full HTTP or HTTPS URL of the web page to read." + } + }, + "required": [ + "url" + ], + "additionalProperties": false + } + } +} diff --git a/app/MindWork AI Studio/wwwroot/tool_definitions/web_search.json b/app/MindWork AI Studio/wwwroot/tool_definitions/web_search.json new file mode 100644 index 00000000..1793de24 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/tool_definitions/web_search.json @@ -0,0 +1,103 @@ +{ + "schemaVersion": 1, + "id": "web_search", + "implementationKey": "web_search", + "visibleIn": { + "chat": true, + "assistants": true + }, + "settingsSchema": { + "type": "object", + "properties": { + "baseUrl": { + "type": "string", + "secret": false + }, + "defaultLanguage": { + "type": "string", + "secret": false + }, + "defaultSafeSearch": { + "type": "string", + "enum": [ + "0", + "1", + "2" + ], + "secret": false + }, + "defaultCategories": { + "type": "string", + "secret": false + }, + "defaultEngines": { + "type": "string", + "secret": false + }, + "maxResults": { + "type": "string", + "secret": false + }, + "timeoutSeconds": { + "type": "string", + "secret": false + } + }, + "required": [ + "baseUrl" + ] + }, + "function": { + "name": "web_search", + "description": "Search the web via a configured SearXNG instance. Prefer categories for broad search intent. Use engines only when the user explicitly asks for specific search engines. Returns JSON with request metadata and the SearXNG response. When deeper content is needed, use read_web_page on relevant URLs from response.results.", + "strict": true, + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The search query to send to SearXNG." + }, + "categories": { + "type": "array", + "description": "Optional list of SearXNG categories to use for the search.", + "items": { + "type": "string" + } + }, + "engines": { + "type": "array", + "description": "Optional list of specific SearXNG engines to use when the user requests them explicitly.", + "items": { + "type": "string" + } + }, + "language": { + "type": "string", + "description": "Optional language code for the search." + }, + "time_range": { + "type": "string", + "description": "Optional time range filter for engines that support it.", + "enum": [ + "day", + "month", + "year" + ] + }, + "page": { + "type": "integer", + "description": "Optional search result page number starting at 1." + }, + "limit": { + "type": "integer", + "description": "Optional maximum number of results to return to the model after local truncation." + } + }, + "required": [ + "query" + ], + "additionalProperties": false + } + } +}