mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 19:08:45 +00:00
Added a way to switch off tools you never need (#1115)
This commit is contained in:
1 parent
dcba62b45c
commit
04546c25ac
31 files changed
+638
-37
No files matched your search
@@ -188,8 +188,9 @@ public static class ConfigurationSnippetImport
|
||||
if (toolIds.Count == 0)
|
||||
return [];
|
||||
|
||||
// A tool the user switched off exists here all the same, and switching it on again makes the import use it:
|
||||
var availableToolIds = (await toolRegistry.GetCatalogAsync(component)).Select(item => item.Id).ToHashSet(StringComparer.Ordinal);
|
||||
return toolIds.Where(id => !availableToolIds.Contains(id)).Order(StringComparer.Ordinal).ToList();
|
||||
return toolIds.Where(id => !availableToolIds.Contains(id) && !toolRegistry.IsSwitchedOffByUser(id)).Order(StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
private static T MakeLocal<T>(T configObject) where T : ConfigurationBaseObject => (T)((ConfigurationBaseObject)configObject with
|
||||
|
||||
@@ -290,7 +290,8 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Tools an organization switched off are left out: an assistant naming one would run without
|
||||
/// it, and neither the model nor the user could tell from the plugin why. Whether a tool is
|
||||
/// it, and neither the model nor the user could tell from the plugin why. Tools the user switched
|
||||
/// off do not even reach the catalog, since the user never needs them. Whether a tool is
|
||||
/// fully configured is deliberately not part of this, because settings can be completed later
|
||||
/// and the assistant then works as written.
|
||||
/// </remarks>
|
||||
|
||||
@@ -194,7 +194,8 @@ public sealed class DataRestoreService(ILogger<DataRestoreService> logger, Setti
|
||||
/// A setting the organization fixed comes from its configuration plugin, and one the tool does
|
||||
/// not show for the saved settings means nothing for them. Which of the remaining ones held a
|
||||
/// value nobody knows: a backup holds no secrets, so the dialog asks to enter them again if they
|
||||
/// were used.
|
||||
/// were used. A tool the user switched off is left out, since they never need it; should they
|
||||
/// switch it on again, its settings say what is missing.
|
||||
/// </remarks>
|
||||
private List<DataRestoreToolSecret> FindToolSecrets()
|
||||
{
|
||||
@@ -204,6 +205,9 @@ public sealed class DataRestoreService(ILogger<DataRestoreService> logger, Setti
|
||||
if (toolRegistry.GetImplementation(definition.ImplementationKey) is not { } implementation)
|
||||
continue;
|
||||
|
||||
if (toolRegistry.IsSwitchedOffByUser(definition.Id))
|
||||
continue;
|
||||
|
||||
if (!settingsManager.ConfigurationData.Tools.Settings.TryGetValue(definition.Id, out var storedValues))
|
||||
continue;
|
||||
|
||||
|
||||
@@ -19,3 +19,5 @@ Tool implementations must treat model-provided arguments as untrusted input. Val
|
||||
A tool which belongs to a preview feature returns false from `IToolImplementation.IsAvailable` while the preview is switched off; the registry then leaves it out of every list, every request, and the token count, so no component has to check that preview for the tool. Every tool declares in `IToolImplementation.OutboundData` where its arguments go: a chat which holds internal content, such as mails, passages of a data source, a search of the wiki, or an internal web page, keeps the tools whose data goes further than its restriction allows from being offered and from running, see `ToolSelectionRules.IsOutboundDataAllowed`. A tool which brings internal content into the chat raises `ToolExecutionResult.RequiredOutboundDataRestriction`, next to `RequiredProviderConfidence` and `RequiredDataSecurity`, names the source in its `OutboundDataOrigin`, and tells the model beforehand that it may end web search, worded by `WebSearchFirstRule`. "Outbound Data Restrictions" in `documentation/Tools.md` explains the sources and the levels, and "Searching Mailboxes" there the mail tools.
|
||||
|
||||
A tool which offers itself from the context of a chat instead of being selected, such as `semantic_search`, sets `Activation = ToolActivation.CONTEXT` and tailors its function to each request in `ResolveFunctionAsync`.
|
||||
|
||||
A tool needs nothing of its own to be switched off by the user or by an organization: `ToolRegistry.IsToolActive` checks both `DataTools.UserDisabledToolIds` and `DataTools.DisabledToolIds` for every tool, and the organization comes first. Keep it that way; a check which asks only one of the two lists lets a switched-off tool through. "Switched-Off Tools" in `documentation/Tools.md` explains the rest.
|
||||
@@ -38,7 +38,20 @@ public sealed class ToolCatalogItem
|
||||
/// </summary>
|
||||
public required ToolConfigurationState ConfigurationState { get; init; }
|
||||
|
||||
public bool IsActive { get; init; }
|
||||
/// <summary>
|
||||
/// Who switched the collection off: tools altogether, the organization, or the user; NONE while it may be used.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A reason rather than a yes or no, because the settings have to tell the user apart from the
|
||||
/// organization: the user can switch on again what they switched off themselves, but nothing
|
||||
/// their organization switched off.
|
||||
/// </remarks>
|
||||
public ToolOfferBlockReason SwitchOffReason { get; init; } = ToolOfferBlockReason.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the collection may be used at all, see ToolRegistry.IsToolActive.
|
||||
/// </summary>
|
||||
public bool IsActive => this.SwitchOffReason is ToolOfferBlockReason.NONE;
|
||||
|
||||
public ConfidenceLevel MinimumProviderConfidence { get; init; } = ConfidenceLevel.NONE;
|
||||
}
|
||||
@@ -37,6 +37,11 @@ public enum ToolOfferBlockReason
|
||||
/// </summary>
|
||||
TOOL_SWITCHED_OFF,
|
||||
|
||||
/// <summary>
|
||||
/// The user switched this tool off in the tool settings, because they never need it.
|
||||
/// </summary>
|
||||
TOOL_SWITCHED_OFF_BY_USER,
|
||||
|
||||
/// <summary>
|
||||
/// A setting the tool cannot work without is missing or invalid.
|
||||
/// </summary>
|
||||
|
||||
@@ -318,22 +318,75 @@ public sealed class ToolRegistry
|
||||
public string GetCollectionId(string toolOrCollectionId) => this.collectionIdsByToolId.GetValueOrDefault(toolOrCollectionId, toolOrCollectionId);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a tool may be used at all: tools are switched on, and the organization did not switch its collection off.
|
||||
/// Whether a tool may be used at all: tools are switched on, and neither the organization nor the user switched its collection off.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An organization switches a collection off by its ID or by the ID of any of its tools. A
|
||||
/// single tool of a collection cannot be switched off: the others would stop making sense, and
|
||||
/// an administrator who names one tool would rather lose the collection than keep the tool.
|
||||
/// The one gate for both lists of switched-off collections. Every list, the preparation of a
|
||||
/// request, and the token count ask here, so a collection the user switched off is left out
|
||||
/// everywhere without a check of its own.<br/><br/>
|
||||
/// A collection is switched off by its ID or by the ID of any of its tools. A single tool of a
|
||||
/// collection cannot be switched off: the others would stop making sense, and an administrator
|
||||
/// who names one tool would rather lose the collection than keep the tool.
|
||||
/// </remarks>
|
||||
/// <param name="toolOrCollectionId">The ID of a tool, or of a collection.</param>
|
||||
/// <returns>True when the tool may be used.</returns>
|
||||
public bool IsToolActive(string toolOrCollectionId)
|
||||
public bool IsToolActive(string toolOrCollectionId) => this.GetSwitchOffReason(toolOrCollectionId) is ToolOfferBlockReason.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user switched the collection of a tool off in the tool settings.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Answers for the user alone, whatever the organization decided: what the user switched off
|
||||
/// stays off once the organization lets go of the collection again.
|
||||
/// </remarks>
|
||||
/// <param name="toolOrCollectionId">The ID of a tool, or of a collection.</param>
|
||||
public bool IsSwitchedOffByUser(string toolOrCollectionId)
|
||||
{
|
||||
var userDisabledIds = this.settingsManager.ConfigurationData.Tools.UserDisabledToolIds;
|
||||
return this.GetSettingsIds(this.GetCollectionId(toolOrCollectionId)).Any(userDisabledIds.Contains);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches the collection of a tool off or on again, as the user chose it in the tool settings.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Whatever tool of a collection the user chose this for, it is stored for the collection.
|
||||
/// Switching it on removes the entries of its tools as well, since any of them would keep the
|
||||
/// collection switched off.
|
||||
/// </remarks>
|
||||
/// <param name="toolOrCollectionId">The ID of a tool, or of a collection.</param>
|
||||
/// <param name="isSwitchedOff">True to switch the collection off, false to switch it on again.</param>
|
||||
public void SetSwitchedOffByUser(string toolOrCollectionId, bool isSwitchedOff)
|
||||
{
|
||||
var userDisabledIds = this.settingsManager.ConfigurationData.Tools.UserDisabledToolIds;
|
||||
var collectionId = this.GetCollectionId(toolOrCollectionId);
|
||||
userDisabledIds.ExceptWith(this.GetSettingsIds(collectionId));
|
||||
if (isSwitchedOff)
|
||||
userDisabledIds.Add(collectionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Who switched the collection of a tool off, if anybody.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The organization comes before the user: the user cannot switch on what their organization
|
||||
/// switched off, so that is the reason which counts while both apply.
|
||||
/// </remarks>
|
||||
/// <param name="toolOrCollectionId">The ID of a tool, or of a collection.</param>
|
||||
/// <returns>TOOLS_SWITCHED_OFF, TOOL_SWITCHED_OFF, TOOL_SWITCHED_OFF_BY_USER, or NONE.</returns>
|
||||
private ToolOfferBlockReason GetSwitchOffReason(string toolOrCollectionId)
|
||||
{
|
||||
if (!this.settingsManager.AreToolsEnabled())
|
||||
return false;
|
||||
return ToolOfferBlockReason.TOOLS_SWITCHED_OFF;
|
||||
|
||||
var disabledIds = this.settingsManager.ConfigurationData.Tools.DisabledToolIds;
|
||||
return !this.GetSettingsIds(this.GetCollectionId(toolOrCollectionId)).Any(disabledIds.Contains);
|
||||
var settingsIds = this.GetSettingsIds(this.GetCollectionId(toolOrCollectionId));
|
||||
if (settingsIds.Any(this.settingsManager.ConfigurationData.Tools.DisabledToolIds.Contains))
|
||||
return ToolOfferBlockReason.TOOL_SWITCHED_OFF;
|
||||
|
||||
if (settingsIds.Any(this.settingsManager.ConfigurationData.Tools.UserDisabledToolIds.Contains))
|
||||
return ToolOfferBlockReason.TOOL_SWITCHED_OFF_BY_USER;
|
||||
|
||||
return ToolOfferBlockReason.NONE;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -449,6 +502,41 @@ public sealed class ToolRegistry
|
||||
.SelectMany(this.GetToolIdsOfCollection)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// The part of a selection a selection field shows: every collection but those the user switched off.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A field lists no collection the user switched off, so it must not count one as selected
|
||||
/// either. Changing the selection afterward goes through KeepSwitchedOffByUser, which puts
|
||||
/// these collections back.
|
||||
/// </remarks>
|
||||
/// <param name="selectedIds">The IDs of the selected tools or collections.</param>
|
||||
/// <returns>The IDs of the collections to show as selected, as a new set.</returns>
|
||||
public HashSet<string> LeaveOutSwitchedOffByUser(IEnumerable<string> selectedIds)
|
||||
{
|
||||
var shown = this.NormalizeSelection(selectedIds);
|
||||
shown.RemoveWhere(this.IsSwitchedOffByUser);
|
||||
return shown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A selection as somebody changed it in a field, plus the collections of the previous selection which the user switched off.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Switching a collection off only hides it. A chat template, a policy, or the defaults of an
|
||||
/// assistant which named it keep naming it, even when somebody changes them while it is off,
|
||||
/// so switching it on again brings it back wherever it was selected.
|
||||
/// </remarks>
|
||||
/// <param name="previousIds">The selection before the change, as stored.</param>
|
||||
/// <param name="changedIds">The selection as the field reports it after the change.</param>
|
||||
/// <returns>The IDs of the collections to store, as a new set.</returns>
|
||||
public HashSet<string> KeepSwitchedOffByUser(IEnumerable<string> previousIds, IEnumerable<string> changedIds)
|
||||
{
|
||||
var kept = this.NormalizeSelection(changedIds);
|
||||
kept.UnionWith(this.NormalizeSelection(previousIds).Where(this.IsSwitchedOffByUser));
|
||||
return kept;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The collections preselected in a component, as the user chose them in the settings.
|
||||
/// </summary>
|
||||
@@ -515,13 +603,17 @@ public sealed class ToolRegistry
|
||||
/// Every selection in the app is built from this list: the one below the message field, the
|
||||
/// defaults, the templates, and the tools the AI picks for a new assistant. A tool which offers
|
||||
/// itself from the context of a chat is left out, because selecting it would change nothing.
|
||||
/// The tool list of the app settings asks for all definitions instead, so an organization can
|
||||
/// still switch such a tool off or set the trust it requires.
|
||||
/// So is a collection the user switched off: they never need it, so it would only take up room.
|
||||
/// One the organization switched off stays, locked, so the user learns why it does not work.
|
||||
/// The tool list of the app settings asks for all definitions instead, so the user can switch
|
||||
/// a collection on again, and an organization can still switch a tool which offers itself off
|
||||
/// or set the trust it requires.
|
||||
/// </remarks>
|
||||
public async Task<IReadOnlyList<ToolCatalogItem>> GetCatalogAsync(Components component)
|
||||
{
|
||||
var definitions = this.GetDefinitionsForComponent(component).Where(x => x.Activation is ToolActivation.SELECTION);
|
||||
return await this.GetCatalogAsync(definitions);
|
||||
var catalog = await this.GetCatalogAsync(definitions);
|
||||
return catalog.Where(item => item.SwitchOffReason is not ToolOfferBlockReason.TOOL_SWITCHED_OFF_BY_USER).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -529,8 +621,8 @@ public sealed class ToolRegistry
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For preselecting tools on someone's behalf, such as when a launcher opens a chat. A tool
|
||||
/// this installation does not know, one an organization switched off, or one whose settings are
|
||||
/// incomplete cannot be enabled by hand either, so handing it over as enabled would show the
|
||||
/// this installation does not know, one an organization or the user switched off, or one whose
|
||||
/// settings are incomplete cannot be enabled by hand either, so handing it over as enabled would show the
|
||||
/// user a state they could not have produced and could not fix from where they are. The
|
||||
/// provider confidence stays out of this: it belongs to the moment a message is sent, not to
|
||||
/// the selection, and it may well be a different provider by then.
|
||||
@@ -612,7 +704,7 @@ public sealed class ToolRegistry
|
||||
DescriptionForLLM = declaredCollection?.Definition.DescriptionForLLM ?? firstTool.Definition.Function.DescriptionForLLM,
|
||||
Tools = tools,
|
||||
ConfigurationState = tools.FirstOrDefault(tool => !tool.ConfigurationState.IsConfigured)?.ConfigurationState ?? new() { IsConfigured = true },
|
||||
IsActive = this.IsToolActive(collectionId),
|
||||
SwitchOffReason = this.GetSwitchOffReason(collectionId),
|
||||
MinimumProviderConfidence = this.GetMinimumProviderConfidence(collectionId),
|
||||
};
|
||||
}
|
||||
@@ -688,6 +780,10 @@ public sealed class ToolRegistry
|
||||
this.logger.LogDebug("Skipping tool '{ToolId}' because it is disabled by managed configuration.", definition.Id);
|
||||
break;
|
||||
|
||||
case { BlockReason: ToolOfferBlockReason.TOOL_SWITCHED_OFF_BY_USER }:
|
||||
this.logger.LogDebug("Skipping tool '{ToolId}' because the user switched it off in the tool settings.", definition.Id);
|
||||
break;
|
||||
|
||||
case { BlockReason: ToolOfferBlockReason.NOT_CONFIGURED }:
|
||||
this.logger.LogDebug("Skipping tool '{ToolId}' because it is not configured.", definition.Id);
|
||||
break;
|
||||
@@ -797,8 +893,9 @@ public sealed class ToolRegistry
|
||||
/// </remarks>
|
||||
private async Task<ToolCheck> CheckToolAsync(ToolDefinition definition, ConfidenceLevel providerConfidence, OutboundDataRestriction outboundDataRestriction)
|
||||
{
|
||||
if (!this.IsToolActive(definition.Id))
|
||||
return new(ToolOfferBlockReason.TOOL_SWITCHED_OFF, null, null);
|
||||
var switchOffReason = this.GetSwitchOffReason(definition.Id);
|
||||
if (switchOffReason is not ToolOfferBlockReason.NONE)
|
||||
return new(switchOffReason, null, null);
|
||||
|
||||
if (!this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation) || !implementation.IsAvailable)
|
||||
return new(ToolOfferBlockReason.NOT_AVAILABLE_HERE, null, null);
|
||||
|
||||
Reference in new issue
Block a user