Keep context-activated tools out of every tool selection

This commit is contained in:
Thorsten Sommer 2026-09-24 14:32:43 +02:00
parent a9825520f4
commit df257714f2
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
5 changed files with 52 additions and 1 deletions

View File

@ -0,0 +1,23 @@
namespace AIStudio.Tools.ToolCallingSystem;
/// <summary>
/// How a tool comes to be offered to a model.
/// </summary>
public enum ToolActivation
{
/// <summary>
/// Offered when it was selected: by the user, a chat template, a policy, or an assistant.
/// </summary>
SELECTION,
/// <summary>
/// Offered whenever the chat calls for it, without anybody selecting it.
/// </summary>
/// <remarks>
/// For a tool whose use is already decided somewhere else. Semantic Search is such a tool: the
/// user picks the data sources of a chat, and a second switch for searching them would only be
/// a way to contradict the first one. Such a tool never appears in a selection, and it decides
/// on each request whether it has anything to offer.
/// </remarks>
CONTEXT,
}

View File

@ -12,6 +12,11 @@ public sealed class ToolDefinition
public ToolVisibilityDefinition VisibleIn { get; init; } = new(); public ToolVisibilityDefinition VisibleIn { get; init; } = new();
/// <summary>
/// Whether the tool waits to be selected, or offers itself whenever the chat calls for it.
/// </summary>
public ToolActivation Activation { get; init; } = ToolActivation.SELECTION;
public ToolSettingsSchema SettingsSchema { get; init; } = new(); public ToolSettingsSchema SettingsSchema { get; init; } = new();
public string SystemPromptInstructions { get; init; } = string.Empty; public string SystemPromptInstructions { get; init; } = string.Empty;

View File

@ -269,9 +269,19 @@ public sealed class ToolRegistry
return filtered; return filtered;
} }
/// <summary>
/// The tools somebody can select in this component.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public async Task<IReadOnlyList<ToolCatalogItem>> GetCatalogAsync(Components component) public async Task<IReadOnlyList<ToolCatalogItem>> GetCatalogAsync(Components component)
{ {
var definitions = this.GetDefinitionsForComponent(component); var definitions = this.GetDefinitionsForComponent(component).Where(x => x.Activation is ToolActivation.SELECTION);
return await this.GetCatalogAsync(definitions); return await this.GetCatalogAsync(definitions);
} }

View File

@ -9,6 +9,7 @@ public static class ToolSelectionRules
public const string WEB_SEARCH_TOOL_ID = "web_search"; public const string WEB_SEARCH_TOOL_ID = "web_search";
public const string READ_WEB_PAGE_TOOL_ID = "read_web_page"; public const string READ_WEB_PAGE_TOOL_ID = "read_web_page";
public const string SEARCH_CONFLUENCE_TOOL_ID = "search_confluence"; public const string SEARCH_CONFLUENCE_TOOL_ID = "search_confluence";
public const string SEMANTIC_SEARCH_TOOL_ID = "semantic_search";
/// <summary> /// <summary>
/// Turns a set of selected tool IDs into the set which actually runs. /// Turns a set of selected tool IDs into the set which actually runs.
@ -19,6 +20,10 @@ public static class ToolSelectionRules
/// ToolRegistry still drops it when it is switched off or the provider's confidence is too /// ToolRegistry still drops it when it is switched off or the provider's confidence is too
/// low, and Read Web Page reaches a wiki on a private or VPN address only when its host is /// low, and Read Web Page reaches a wiki on a private or VPN address only when its host is
/// allowed there.<br/><br/> /// allowed there.<br/><br/>
/// It also removes the tools nobody selects. Semantic Search offers itself whenever the data
/// sources of a chat call for it, see ToolActivation.CONTEXT; kept in a selection, it would
/// appear on the security card of a plugin and in its audit without the selection having any
/// say in whether it runs.<br/><br/>
/// Every place which shows or stores a selection normalizes it, the tool selection fields /// Every place which shows or stores a selection normalizes it, the tool selection fields
/// included. That way a chat, a template, a policy, or an assistant plugin shows the tools /// included. That way a chat, a template, a policy, or an assistant plugin shows the tools
/// which will actually run, and the audit of a plugin judges exactly those. /// which will actually run, and the audit of a plugin judges exactly those.
@ -29,6 +34,7 @@ public static class ToolSelectionRules
if (normalized.Contains(SEARCH_CONFLUENCE_TOOL_ID)) if (normalized.Contains(SEARCH_CONFLUENCE_TOOL_ID))
normalized.Add(READ_WEB_PAGE_TOOL_ID); normalized.Add(READ_WEB_PAGE_TOOL_ID);
normalized.Remove(SEMANTIC_SEARCH_TOOL_ID);
return normalized; return normalized;
} }

View File

@ -16,6 +16,7 @@ public sealed class ToolSelectionRulesTests
private const string SEARCH_CONFLUENCE = ToolSelectionRules.SEARCH_CONFLUENCE_TOOL_ID; private const string SEARCH_CONFLUENCE = ToolSelectionRules.SEARCH_CONFLUENCE_TOOL_ID;
private const string READ_WEB_PAGE = ToolSelectionRules.READ_WEB_PAGE_TOOL_ID; private const string READ_WEB_PAGE = ToolSelectionRules.READ_WEB_PAGE_TOOL_ID;
private const string WEB_SEARCH = ToolSelectionRules.WEB_SEARCH_TOOL_ID; private const string WEB_SEARCH = ToolSelectionRules.WEB_SEARCH_TOOL_ID;
private const string SEMANTIC_SEARCH = ToolSelectionRules.SEMANTIC_SEARCH_TOOL_ID;
[Test] [Test]
public void SearchConfluenceBringsReadWebPageAlong() public void SearchConfluenceBringsReadWebPageAlong()
@ -30,6 +31,12 @@ public sealed class ToolSelectionRulesTests
Assert.That(ToolSelectionRules.NormalizeSelection([toolId]), Is.EquivalentTo(new[] { toolId }), "Only Search Confluence depends on another tool. Read Web Page in particular does not pull the search in."); Assert.That(ToolSelectionRules.NormalizeSelection([toolId]), Is.EquivalentTo(new[] { toolId }), "Only Search Confluence depends on another tool. Read Web Page in particular does not pull the search in.");
} }
[Test]
public void SemanticSearchIsNeverPartOfASelection()
{
Assert.That(ToolSelectionRules.NormalizeSelection([SEMANTIC_SEARCH, WEB_SEARCH]), Is.EquivalentTo(new[] { WEB_SEARCH }), "Semantic Search offers itself from the data sources of a chat. A template or a plugin naming it would put a tool on the security card that the selection has no say over.");
}
[Test] [Test]
public void NormalizingTwiceChangesNothing() public void NormalizingTwiceChangesNothing()
{ {