diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor index ee83a2e0..8936b3c7 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor @@ -175,7 +175,7 @@ } - @if (this.SettingsManager.IsToolSelectionVisible(this.Component)) + @if (this.SettingsManager.AreToolsEnabled() && this.SettingsManager.IsToolSelectionVisible(this.Component)) { } diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 2f8794e8..18979c65 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -3607,6 +3607,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3728248397 -- Tool Settings UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3730473128"] = "Tool Settings" +-- This tool has been disabled by your organization. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3794167684"] = "This tool has been disabled by your organization." + -- Status UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T6222351"] = "Status" @@ -3724,6 +3727,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3119156561"] = "Required -- Close UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3448155331"] = "Close" +-- This tool has been disabled by your organization. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3794167684"] = "This tool has been disabled by your organization." + -- No tools are available in this context. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3904490680"] = "No tools are available in this context." @@ -6535,6 +6541,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3616903110"] -- Tool Settings UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3730473128"] = "Tool Settings" +-- This tool has been disabled by your organization. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3794167684"] = "This tool has been disabled by your organization." + -- The selected tool could not be loaded. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3907843187"] = "The selected tool could not be loaded." @@ -9286,6 +9295,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS: -- Optional HTTP timeout for the search request in seconds. UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3078115445"] = "Optional HTTP timeout for the search request in seconds." +-- The default safe search setting must be 0, 1, or 2. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3187215042"] = "The default safe search setting must be 0, 1, or 2." + -- Search the web with a configured SearXNG instance and retrieve the readable content of the best matching pages. UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3361633224"] = "Search the web with a configured SearXNG instance and retrieve the readable content of the best matching pages." diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor index b6dead4a..a9e6a391 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor +++ b/app/MindWork AI Studio/Components/ChatComponent.razor @@ -125,7 +125,10 @@ - + @if (this.SettingsManager.AreToolsEnabled()) + { + + } @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) { diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor index a7791799..2f83e75c 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTools.razor @@ -37,7 +37,13 @@ - @if (context.ConfigurationState.IsConfigured) + @if (!context.IsActive) + { + + + + } + else if (context.ConfigurationState.IsConfigured) { } diff --git a/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor b/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor index be71c551..22f8f209 100644 --- a/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor +++ b/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor @@ -8,5 +8,5 @@ { } - + } diff --git a/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor.cs b/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor.cs index a63f8734..28b3802b 100644 --- a/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor.cs +++ b/app/MindWork AI Studio/Components/ToolDefaultsConfiguration.razor.cs @@ -29,6 +29,8 @@ public partial class ToolDefaultsConfiguration : MSGComponentBase this.Component is not AIStudio.Tools.Components.CHAT && !this.SettingsManager.IsToolSelectionVisible(this.Component); + private bool IsToolDisabled(string toolId) => !this.SettingsManager.IsToolActive(toolId); + protected override async Task OnInitializedAsync() { this.availableTools = (await this.ToolRegistry.GetCatalogAsync(this.Component)) diff --git a/app/MindWork AI Studio/Components/ToolSelection.razor b/app/MindWork AI Studio/Components/ToolSelection.razor index b7d71973..465adadb 100644 --- a/app/MindWork AI Studio/Components/ToolSelection.razor +++ b/app/MindWork AI Studio/Components/ToolSelection.razor @@ -47,8 +47,14 @@ - + + @if (!item.IsActive) + { + + + + } @item.Implementation.GetDisplayName() @@ -59,6 +65,10 @@ { @(string.IsNullOrWhiteSpace(item.ConfigurationState.Message) ? T("Required settings are missing. Configure this tool before enabling it.") : item.ConfigurationState.Message) } + @if (!item.IsActive) + { + @T("This tool has been disabled by your organization.") + } @if (!string.IsNullOrWhiteSpace(providerConfidenceHint)) { @providerConfidenceHint diff --git a/app/MindWork AI Studio/Components/ToolSelection.razor.cs b/app/MindWork AI Studio/Components/ToolSelection.razor.cs index d2e1cbeb..362fb808 100644 --- a/app/MindWork AI Studio/Components/ToolSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ToolSelection.razor.cs @@ -73,6 +73,9 @@ public partial class ToolSelection : MSGComponentBase private async Task ChangeSelection(string toolId, bool isSelected) { + if (isSelected && !this.SettingsManager.IsToolActive(toolId)) + return; + var updated = new HashSet(this.SelectedToolIds, StringComparer.Ordinal); if (isSelected) updated.Add(toolId); diff --git a/app/MindWork AI Studio/Dialogs/Settings/ToolSettingsDialog.razor b/app/MindWork AI Studio/Dialogs/Settings/ToolSettingsDialog.razor index 1e9e3d24..f18b4f78 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/ToolSettingsDialog.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/ToolSettingsDialog.razor @@ -19,6 +19,11 @@ @this.implementation?.GetDescription() + @if (!this.SettingsManager.IsToolActive(this.toolDefinition.Id)) + { + @T("This tool has been disabled by your organization.") + } + @if (!string.IsNullOrWhiteSpace(this.validationMessage)) { @this.validationMessage diff --git a/app/MindWork AI Studio/Dialogs/Settings/ToolSettingsDialog.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/ToolSettingsDialog.razor.cs index b7cfd7c4..625bba25 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/ToolSettingsDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/ToolSettingsDialog.razor.cs @@ -1,4 +1,3 @@ -using AIStudio.Settings; using AIStudio.Tools.ToolCallingSystem; using Microsoft.AspNetCore.Components; @@ -53,19 +52,8 @@ public partial class ToolSettingsDialog : SettingsDialogBase return string.Format(T("{0} Default: {1}"), description, defaultValue); } - private bool IsFieldDisabled(string fieldName) - { - if (this.toolDefinition?.Id.Equals(ToolSelectionRules.WEB_SEARCH_TOOL_ID, StringComparison.Ordinal) is true && - fieldName.Equals("baseUrl", StringComparison.Ordinal) && - ManagedConfiguration.TryGet(x => x.Tools, x => x.WebSearchBaseUrl, out var webSearchMeta) && - webSearchMeta.IsLocked) - return true; - - return this.toolDefinition?.Id.Equals(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, StringComparison.Ordinal) is true && - fieldName.Equals("allowedPrivateHosts", StringComparison.Ordinal) && - ManagedConfiguration.TryGet(x => x.Tools, x => x.ReadWebPageAllowedPrivateHosts, out var readWebPageMeta) && - readWebPageMeta.IsLocked; - } + private bool IsFieldDisabled(string fieldName) => + this.toolDefinition is not null && this.ToolSettingsService.IsFieldLocked(this.toolDefinition, fieldName); private string GetFieldPlaceholder(string fieldName, ToolSettingsFieldDefinition fieldDefinition) => string.IsNullOrWhiteSpace(this.GetValue(fieldName)) ? this.GetFieldDefaultValue(fieldName, fieldDefinition) : string.Empty; diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 3cdc84ff..b2254f67 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -356,6 +356,15 @@ CONFIG["SETTINGS"] = {} -- Examples are: "CmdOrControl+Shift+D", "Alt+F9", "F8" -- CONFIG["SETTINGS"]["DataApp.ShortcutVoiceRecording"] = "CmdOrControl+1" +-- Configure whether tools are available at all. The default is true. +-- When tools are disabled globally, tool selection is hidden in chats and assistants, +-- but the global tool settings remain available to administrators. +-- CONFIG["SETTINGS"]["DataTools.EnableTools"] = false + +-- Disable individual tools by their stable tool ID. The default is an empty set. +-- Unknown IDs are safely ignored and can be deployed before a future tool is installed. +-- CONFIG["SETTINGS"]["DataTools.DisabledToolIds"] = { "web_search" } + -- Configure the minimum provider confidence level required for individual tools. -- Tool IDs include: web_search, read_web_page -- Allowed values are: NONE, UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH @@ -365,21 +374,47 @@ CONFIG["SETTINGS"] = {} -- ["read_web_page"] = "MEDIUM" -- } --- Configure the SearXNG instance URL used by the Web Search tool. --- You can enter either the instance root URL or the /search endpoint. +-- Configure the Web Search tool. All values are strings. +-- WebSearchBaseUrl: required SearXNG HTTP(S) root URL or /search endpoint; no default. -- CONFIG["SETTINGS"]["DataTools.WebSearchBaseUrl"] = "https://searxng.website/" --- CONFIG["SETTINGS"]["DataTools.WebSearchBaseUrl.AllowUserOverride"] = false +-- WebSearchDefaultLanguage: optional language code; default is empty. +-- CONFIG["SETTINGS"]["DataTools.WebSearchDefaultLanguage"] = "en" +-- WebSearchDefaultSafeSearch: optional SearXNG safe-search level "0", "1", or "2"; default is empty. +-- CONFIG["SETTINGS"]["DataTools.WebSearchDefaultSafeSearch"] = "1" +-- WebSearchDefaultCategories: optional comma-separated categories; default is empty. +-- WebSearchDefaultEngines: optional comma-separated engines; default is empty. +-- Categories and engines cannot both be configured. +-- CONFIG["SETTINGS"]["DataTools.WebSearchDefaultCategories"] = "general, science" +-- CONFIG["SETTINGS"]["DataTools.WebSearchDefaultEngines"] = "" +-- WebSearchMaxResults: positive integer; default 5, effective maximum 20. +-- CONFIG["SETTINGS"]["DataTools.WebSearchMaxResults"] = "5" +-- WebSearchTimeoutSeconds: positive integer; default 20, effective maximum 60. +-- CONFIG["SETTINGS"]["DataTools.WebSearchTimeoutSeconds"] = "20" +-- WebSearchMaxTotalContentCharacters: positive integer; default and maximum 100000. +-- CONFIG["SETTINGS"]["DataTools.WebSearchMaxTotalContentCharacters"] = "100000" +-- WebSearchMinContentCharactersPerResult: positive integer; default and maximum 3000. +-- The total content budget must be at least this value multiplied by the hard limit of 20 results. +-- CONFIG["SETTINGS"]["DataTools.WebSearchMinContentCharactersPerResult"] = "3000" +-- WebSearchPageTimeoutSeconds: positive integer; default and maximum 30. +-- CONFIG["SETTINGS"]["DataTools.WebSearchPageTimeoutSeconds"] = "30" +-- WebSearchRetrievalTimeoutSeconds: positive integer; default and maximum 90. +-- CONFIG["SETTINGS"]["DataTools.WebSearchRetrievalTimeoutSeconds"] = "90" --- Configure private or VPN hosts that the Read Web Page tool may access. --- Public web pages do not need to be listed here. --- Private hosts listed here still require a provider with HIGH confidence before any page content is sent to the model. --- For hosts on this allowlist, AI Studio also tries the current user's operating-system sign-in --- automatically when the server requests integrated authentication (for example Kerberos or NTLM). --- This does not reuse Firefox cookies or an existing browser session. --- Separate host patterns with commas. Wildcards only match subdomains, so add the root domain separately if needed. --- Examples: +-- Configure the Read Web Page tool. All values are strings. +-- ReadWebPageTimeoutSeconds: positive integer; default 30, effective maximum 60. +-- CONFIG["SETTINGS"]["DataTools.ReadWebPageTimeoutSeconds"] = "30" +-- ReadWebPageMaxContentCharacters: positive integer; default 30000, effective maximum 50000. +-- CONFIG["SETTINGS"]["DataTools.ReadWebPageMaxContentCharacters"] = "30000" +-- ReadWebPageAllowedPrivateHosts: optional comma-separated private or VPN host patterns; default is empty. +-- Public pages do not need to be listed. Wildcards only match subdomains, so add the root domain separately. +-- Allowed private hosts require a provider with HIGH confidence. AI Studio tries the current user's +-- operating-system sign-in when integrated authentication is requested, but does not reuse browser cookies. -- CONFIG["SETTINGS"]["DataTools.ReadWebPageAllowedPrivateHosts"] = "dlr.de, *.dlr.de" --- CONFIG["SETTINGS"]["DataTools.ReadWebPageAllowedPrivateHosts.AllowUserOverride"] = false + +-- The 14 Web Search and Read Web Page settings are locked by default. Add +-- ".AllowUserOverride" = true to any of them to provide an editable organization default instead. +-- A saved local value then takes precedence. +-- CONFIG["SETTINGS"]["DataTools.WebSearchBaseUrl.AllowUserOverride"] = true -- Configure the HTTP timeout for external requests, in seconds. -- The default is 3600 (1 hour). diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index 44c83f88..792e65bb 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -107,7 +107,8 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur // var providerConfidence = this.Provider.GetConfidence(settingsManager).Level; var minimumWebSearchConfidence = settingsManager.GetMinimumProviderConfidenceForTool(ToolSelectionRules.WEB_SEARCH_TOOL_ID); - var isWebSearchAllowed = ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumWebSearchConfidence); + var isWebSearchAllowed = settingsManager.IsToolActive(ToolSelectionRules.WEB_SEARCH_TOOL_ID) && + ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumWebSearchConfidence); IList providerTools = modelCapabilities.Contains(Capability.WEB_SEARCH) && isWebSearchAllowed ? [ ProviderTools.WEB_SEARCH ] : []; diff --git a/app/MindWork AI Studio/Settings/DataModel/DataTools.cs b/app/MindWork AI Studio/Settings/DataModel/DataTools.cs index b8e8ae6c..4b034661 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataTools.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataTools.cs @@ -16,6 +16,16 @@ public sealed class DataTools(Expression>? configSelection public HashSet VisibleToolSelectionComponents { get; set; } = []; + public bool EnableTools { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.EnableTools, + true); + + public HashSet DisabledToolIds { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.DisabledToolIds, + []); + public Dictionary MinimumProviderConfidenceByToolId { get; set; } = ManagedConfiguration.Register>( configSelection, x => x.MinimumProviderConfidenceByToolId, @@ -26,6 +36,66 @@ public sealed class DataTools(Expression>? configSelection x => x.WebSearchBaseUrl, string.Empty); + public string WebSearchDefaultLanguage { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.WebSearchDefaultLanguage, + string.Empty); + + public string WebSearchDefaultSafeSearch { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.WebSearchDefaultSafeSearch, + string.Empty); + + public string WebSearchDefaultCategories { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.WebSearchDefaultCategories, + string.Empty); + + public string WebSearchDefaultEngines { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.WebSearchDefaultEngines, + string.Empty); + + public string WebSearchMaxResults { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.WebSearchMaxResults, + string.Empty); + + public string WebSearchTimeoutSeconds { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.WebSearchTimeoutSeconds, + string.Empty); + + public string WebSearchMaxTotalContentCharacters { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.WebSearchMaxTotalContentCharacters, + string.Empty); + + public string WebSearchMinContentCharactersPerResult { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.WebSearchMinContentCharactersPerResult, + string.Empty); + + public string WebSearchPageTimeoutSeconds { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.WebSearchPageTimeoutSeconds, + string.Empty); + + public string WebSearchRetrievalTimeoutSeconds { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.WebSearchRetrievalTimeoutSeconds, + string.Empty); + + public string ReadWebPageTimeoutSeconds { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.ReadWebPageTimeoutSeconds, + string.Empty); + + public string ReadWebPageMaxContentCharacters { get; set; } = ManagedConfiguration.Register( + configSelection, + x => x.ReadWebPageMaxContentCharacters, + string.Empty); + public string ReadWebPageAllowedPrivateHosts { get; set; } = ManagedConfiguration.Register( configSelection, x => x.ReadWebPageAllowedPrivateHosts, diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index d3088408..d2194ef5 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -594,6 +594,9 @@ public sealed class SettingsManager public HashSet FilterToolIdsForProvider(AIStudio.Settings.Provider provider, IEnumerable selectedToolIds) { + if (!this.AreToolsEnabled()) + return []; + var toolCallingAvailability = provider.GetToolCallingAvailability(); if (!toolCallingAvailability.IsAvailable) return []; @@ -610,6 +613,12 @@ public sealed class SettingsManager foreach (var toolId in filtered.ToList()) { + if (!this.IsToolActive(toolId)) + { + filtered.Remove(toolId); + continue; + } + var minimumToolConfidence = this.GetMinimumProviderConfidenceForTool(toolId); if (!ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumToolConfidence)) filtered.Remove(toolId); @@ -618,6 +627,12 @@ public sealed class SettingsManager return filtered; } + public bool AreToolsEnabled() => this.ConfigurationData.Tools.EnableTools; + + public bool IsToolActive(string toolId) => + this.AreToolsEnabled() && + !this.ConfigurationData.Tools.DisabledToolIds.Contains(toolId); + public bool IsToolSelectionVisible(AIStudio.Tools.Components component) => component switch { AIStudio.Tools.Components.CHAT => true, @@ -772,4 +787,4 @@ public sealed class SettingsManager // Return the full name of the property, including the class name: return $"{typeof(TIn).Name}.{memberExpr.Member.Name}"; } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index 1690bd58..3915d422 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -199,13 +199,29 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT // Config: global voice recording shortcut ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShortcutVoiceRecording, this.Id, settingsTable, dryRun); + // Config: global tool availability + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.EnableTools, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.DisabledToolIds, this.Id, settingsTable, dryRun); + // Config: minimum provider confidence per tool ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.MinimumProviderConfidenceByToolId, this.Id, settingsTable, dryRun); - // Config: SearXNG base URL for the web search tool + // Config: web search tool settings ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchBaseUrl, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchDefaultLanguage, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchDefaultSafeSearch, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchDefaultCategories, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchDefaultEngines, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchMaxResults, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchTimeoutSeconds, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchMaxTotalContentCharacters, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchMinContentCharactersPerResult, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchPageTimeoutSeconds, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchRetrievalTimeoutSeconds, this.Id, settingsTable, dryRun); - // Config: private hosts allowed for the read web page tool + // Config: read web page tool settings + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.ReadWebPageTimeoutSeconds, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.ReadWebPageMaxContentCharacters, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.ReadWebPageAllowedPrivateHosts, this.Id, settingsTable, dryRun); // Config: timeout for external HTTP requests diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index bcc97fcd..1d8c5316 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -292,15 +292,58 @@ public static partial class PluginFactory if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShortcutVoiceRecording, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; + // Check for global tool availability: + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.EnableTools, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.DisabledToolIds, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + // Check for minimum provider confidence per tool: if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.MinimumProviderConfidenceByToolId, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; - // Check for the SearXNG base URL for the web search tool: + // Check for web search tool settings: if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchBaseUrl, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; - // Check for private hosts allowed for the read web page tool: + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchDefaultLanguage, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchDefaultSafeSearch, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchDefaultCategories, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchDefaultEngines, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchMaxResults, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchTimeoutSeconds, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchMaxTotalContentCharacters, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchMinContentCharactersPerResult, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchPageTimeoutSeconds, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchRetrievalTimeoutSeconds, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + // Check for read web page tool settings: + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.ReadWebPageTimeoutSeconds, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.ReadWebPageMaxContentCharacters, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.ReadWebPageAllowedPrivateHosts, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/SearXNGWebSearchTool.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/SearXNGWebSearchTool.cs index 4bd40fe3..f547074d 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/SearXNGWebSearchTool.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/SearXNGWebSearchTool.cs @@ -113,6 +113,16 @@ public sealed class SearXNGWebSearchTool : IToolImplementation }); } + var defaultSafeSearch = settingsValues.GetValueOrDefault("defaultSafeSearch"); + if (!string.IsNullOrWhiteSpace(defaultSafeSearch) && defaultSafeSearch is not ("0" or "1" or "2")) + { + return Task.FromResult(new ToolConfigurationState + { + IsConfigured = false, + Message = TB("The default safe search setting must be 0, 1, or 2."), + }); + } + if (!ToolSettingsValueParser.TryReadOptionalPositiveInt(settingsValues, "maxResults", positiveIntegerErrorFormat, out _, out var maxResultsError)) { return Task.FromResult(new ToolConfigurationState diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutionModels.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutionModels.cs index 1f5d3a89..376fcba9 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutionModels.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutionModels.cs @@ -98,6 +98,8 @@ public sealed class ToolCatalogItem public required ToolConfigurationState ConfigurationState { get; init; } + public bool IsActive { get; init; } + public ConfidenceLevel MinimumProviderConfidence { get; init; } = ConfidenceLevel.NONE; } diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolRegistry.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolRegistry.cs index 5dd5f489..8527ef92 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolRegistry.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolRegistry.cs @@ -208,6 +208,7 @@ public sealed class ToolRegistry Definition = definition, Implementation = implementation, ConfigurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition, implementation), + IsActive = this.settingsManager.IsToolActive(definition.Id), MinimumProviderConfidence = this.settingsManager.GetMinimumProviderConfidenceForTool(definition.Id), }); } @@ -223,6 +224,12 @@ public sealed class ToolRegistry ConfidenceLevel providerConfidence, bool isToolSelectionVisible) { + if (!this.settingsManager.AreToolsEnabled()) + { + this.logger.LogInformation("Tool calling is skipped because tools are disabled by managed configuration."); + return []; + } + if (!isToolSelectionVisible) { this.logger.LogInformation("Tool calling is skipped for component '{Component}' because tool selection is not visible.", component); @@ -250,6 +257,12 @@ public sealed class ToolRegistry var result = new List<(ToolDefinition, IToolImplementation)>(definitions.Count); foreach (var definition in definitions) { + if (!this.settingsManager.IsToolActive(definition.Id)) + { + this.logger.LogInformation("Skipping tool '{ToolId}' because it is disabled by managed configuration.", definition.Id); + continue; + } + if (!this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation)) { this.logger.LogInformation("Skipping tool '{ToolId}' because no implementation is registered.", definition.Id); diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSettingsService.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSettingsService.cs index 9fbd2edd..9beae89d 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSettingsService.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSettingsService.cs @@ -1,12 +1,30 @@ +using System.Linq.Expressions; + using AIStudio.Settings; +using AIStudio.Settings.DataModel; using AIStudio.Tools.Services; namespace AIStudio.Tools.ToolCallingSystem; public sealed class ToolSettingsService(SettingsManager settingsManager, RustService rustService) { - private const string WEB_SEARCH_BASE_URL_FIELD = "baseUrl"; - private const string READ_WEB_PAGE_ALLOWED_PRIVATE_HOSTS_FIELD = "allowedPrivateHosts"; + private static readonly Dictionary<(string ToolId, string FieldName), ManagedToolSetting> MANAGED_SETTINGS = new() + { + [(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "baseUrl")] = CreateManagedToolSetting(x => x.WebSearchBaseUrl, (tools, value) => tools.WebSearchBaseUrl = value), + [(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "defaultLanguage")] = CreateManagedToolSetting(x => x.WebSearchDefaultLanguage), + [(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "defaultSafeSearch")] = CreateManagedToolSetting(x => x.WebSearchDefaultSafeSearch), + [(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "defaultCategories")] = CreateManagedToolSetting(x => x.WebSearchDefaultCategories), + [(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "defaultEngines")] = CreateManagedToolSetting(x => x.WebSearchDefaultEngines), + [(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "maxResults")] = CreateManagedToolSetting(x => x.WebSearchMaxResults), + [(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "timeoutSeconds")] = CreateManagedToolSetting(x => x.WebSearchTimeoutSeconds), + [(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "maxTotalContentCharacters")] = CreateManagedToolSetting(x => x.WebSearchMaxTotalContentCharacters), + [(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "minContentCharactersPerResult")] = CreateManagedToolSetting(x => x.WebSearchMinContentCharactersPerResult), + [(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "pageTimeoutSeconds")] = CreateManagedToolSetting(x => x.WebSearchPageTimeoutSeconds), + [(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "retrievalTimeoutSeconds")] = CreateManagedToolSetting(x => x.WebSearchRetrievalTimeoutSeconds), + [(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, "timeoutSeconds")] = CreateManagedToolSetting(x => x.ReadWebPageTimeoutSeconds), + [(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, "maxContentCharacters")] = CreateManagedToolSetting(x => x.ReadWebPageMaxContentCharacters), + [(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, "allowedPrivateHosts")] = CreateManagedToolSetting(x => x.ReadWebPageAllowedPrivateHosts, (tools, value) => tools.ReadWebPageAllowedPrivateHosts = value), + }; public async Task> GetSettingsAsync(ToolDefinition definition) { @@ -16,15 +34,16 @@ public sealed class ToolSettingsService(SettingsManager settingsManager, RustSer { var fieldName = property.Key; var fieldDefinition = property.Value; - if (IsWebSearchBaseUrlField(definition, fieldName)) + if (TryGetManagedSetting(definition, fieldName, out var managedSetting)) { - values[fieldName] = settingsManager.ConfigurationData.Tools.WebSearchBaseUrl; - continue; - } + var meta = managedSetting.GetMeta(); + if (meta?.IsLocked is true || managedSetting.SetLegacyLocalValue is not null) + values[fieldName] = managedSetting.GetValue(settingsManager.ConfigurationData.Tools); + else if (storedValues?.TryGetValue(fieldName, out var managedStoredValue) is true) + values[fieldName] = managedStoredValue; + else if (meta?.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT) + values[fieldName] = managedSetting.GetValue(settingsManager.ConfigurationData.Tools); - if (IsReadWebPageAllowedPrivateHostsField(definition, fieldName)) - { - values[fieldName] = settingsManager.ConfigurationData.Tools.ReadWebPageAllowedPrivateHosts; continue; } @@ -103,18 +122,15 @@ public sealed class ToolSettingsService(SettingsManager settingsManager, RustSer values.TryGetValue(fieldName, out var value); value ??= string.Empty; - if (IsWebSearchBaseUrlField(definition, fieldName)) + if (TryGetManagedSetting(definition, fieldName, out var managedSetting)) { - if (!IsWebSearchBaseUrlLocked()) - settingsManager.ConfigurationData.Tools.WebSearchBaseUrl = value; + if (managedSetting.GetMeta()?.IsLocked is true) + continue; - continue; - } - - if (IsReadWebPageAllowedPrivateHostsField(definition, fieldName)) - { - if (!IsReadWebPageAllowedPrivateHostsLocked()) - settingsManager.ConfigurationData.Tools.ReadWebPageAllowedPrivateHosts = value; + if (managedSetting.SetLegacyLocalValue is not null) + managedSetting.SetLegacyLocalValue(settingsManager.ConfigurationData.Tools, value); + else + storedValues[fieldName] = value; continue; } @@ -137,17 +153,26 @@ public sealed class ToolSettingsService(SettingsManager settingsManager, RustSer await MessageBus.INSTANCE.SendMessage(null, Event.CONFIGURATION_CHANGED, null); } - private static bool IsWebSearchBaseUrlField(ToolDefinition definition, string fieldName) => - definition.Id.Equals(ToolSelectionRules.WEB_SEARCH_TOOL_ID, StringComparison.Ordinal) && - fieldName.Equals(WEB_SEARCH_BASE_URL_FIELD, StringComparison.Ordinal); + public bool IsFieldLocked(ToolDefinition definition, string fieldName) => + TryGetManagedSetting(definition, fieldName, out var managedSetting) && + managedSetting.GetMeta()?.IsLocked is true; - private static bool IsWebSearchBaseUrlLocked() => - ManagedConfiguration.TryGet(x => x.Tools, x => x.WebSearchBaseUrl, out var meta) && meta.IsLocked; + private static bool TryGetManagedSetting(ToolDefinition definition, string fieldName, out ManagedToolSetting managedSetting) => + MANAGED_SETTINGS.TryGetValue((definition.Id, fieldName), out managedSetting!); - private static bool IsReadWebPageAllowedPrivateHostsField(ToolDefinition definition, string fieldName) => - definition.Id.Equals(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, StringComparison.Ordinal) && - fieldName.Equals(READ_WEB_PAGE_ALLOWED_PRIVATE_HOSTS_FIELD, StringComparison.Ordinal); + private static ManagedToolSetting CreateManagedToolSetting( + Expression> propertyExpression, + Action? setLegacyLocalValue = null) + { + var getValue = propertyExpression.Compile(); + return new ManagedToolSetting( + getValue, + () => ManagedConfiguration.TryGet(x => x.Tools, propertyExpression, out var meta) ? meta : null, + setLegacyLocalValue); + } - private static bool IsReadWebPageAllowedPrivateHostsLocked() => - ManagedConfiguration.TryGet(x => x.Tools, x => x.ReadWebPageAllowedPrivateHosts, out var meta) && meta.IsLocked; + private sealed record ManagedToolSetting( + Func GetValue, + Func?> GetMeta, + Action? SetLegacyLocalValue); } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.7.4.md b/app/MindWork AI Studio/wwwroot/changelog/v26.7.4.md index 40d2eaf3..7fc91688 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.7.4.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.7.4.md @@ -1 +1,3 @@ # v26.7.4, build 249 (2026-07-xx xx:xx UTC) + +- Added organization-wide management for tool availability and all Web Search and Read Web Page settings. diff --git a/documentation/Tools.md b/documentation/Tools.md index d165205c..285e29cc 100644 --- a/documentation/Tools.md +++ b/documentation/Tools.md @@ -222,7 +222,7 @@ All values must be positive. The total budget must be large enough to reserve th The two tools can be selected independently. Tool policy text tells the model not to call `read_web_page` for a URL already returned by `web_search`, because the search result already contains that page's retrieved content. -For settings that administrators should be able to manage centrally, add the setting to the appropriate `Settings/DataModel` class, register it with `ManagedConfiguration.Register(...)`, process it in `PluginConfiguration`, clean leftovers in `PluginFactory.Loading`, and document it in `Plugins/configuration/plugin.lua`. +Every non-secret tool field that administrators should be able to manage centrally must have an explicit enterprise mapping in `ToolSettingsService`. Add its backing setting to the appropriate `Settings/DataModel` class, register it with `ManagedConfiguration.Register(...)`, process it in `PluginConfiguration`, clean leftovers in `PluginFactory.Loading`, and document its allowed values, default, and limits in `Plugins/configuration/plugin.lua`. Locked enterprise values override the local field, while editable enterprise defaults apply only until a user saves a local value. Secret fields require the existing OS-keyring path and must not be routed through plain enterprise settings. ## Checklist @@ -230,6 +230,7 @@ For settings that administrators should be able to manage centrally, add the set - Add the `IToolImplementation` class. - Register the implementation in `Program.cs`. - Validate settings and model arguments. +- Add the enterprise mapping for each administratively configurable non-secret setting. - Protect secrets and sensitive trace arguments. - Add provider-confidence checks when tool output may contain sensitive data. - Update configuration plugin documentation when admins can manage the setting.