Share the checks that decide whether a tool can be offered

This commit is contained in:
Thorsten Sommer committed 2026-09-24 14:42:52 +02:00
1 parent df257714f2
commit 38d4520e80
3 files changed
+332 -28

No files matched your search

@@ -0,0 +1,48 @@
namespace AIStudio.Tools.ToolCallingSystem;
/// <summary>
/// What keeps a tool from being offered to a model, if anything.
/// </summary>
/// <remarks>
/// A reason rather than a yes or no, because whoever asks has to say something different for each
/// of them: a model which cannot use tools is a matter of the provider settings, a tool switched off
/// by the organization is nothing the user can change, and missing settings are something they can
/// fill in themselves.
/// </remarks>
public enum ToolOfferBlockReason
{
/// <summary>
/// Nothing is in the way, the tool can be offered.
/// </summary>
NONE,
/// <summary>
/// The organization switched tools off altogether.
/// </summary>
TOOLS_SWITCHED_OFF,
/// <summary>
/// The selected model or its provider cannot use tools, or no provider is selected at all.
/// </summary>
MODEL_CANNOT_USE_TOOLS,
/// <summary>
/// This installation does not know the tool, or the tool is not meant for this part of the app.
/// </summary>
NOT_AVAILABLE_HERE,
/// <summary>
/// The organization switched this tool off.
/// </summary>
TOOL_SWITCHED_OFF,
/// <summary>
/// A setting the tool cannot work without is missing or invalid.
/// </summary>
NOT_CONFIGURED,
/// <summary>
/// The provider is not trusted enough for this tool.
/// </summary>
PROVIDER_CONFIDENCE_TOO_LOW,
}
@@ -22,6 +22,14 @@ public sealed class ToolRegistry
private readonly Dictionary<string, ToolDefinition> definitionsById = new(StringComparer.Ordinal);
private readonly Dictionary<string, IToolImplementation> implementationsByKey = new(StringComparer.Ordinal);
/// <summary>
/// What the checks of a single tool found.
/// </summary>
/// <param name="BlockReason">What keeps the tool from being offered, or none.</param>
/// <param name="Implementation">The tool's implementation, once it was found.</param>
/// <param name="MinimumConfidence">The confidence the tool requires and where that requirement came from, once it was read.</param>
private readonly record struct ToolCheck(ToolOfferBlockReason BlockReason, IToolImplementation? Implementation, SettingsManager.ToolMinimumProviderConfidenceResolution? MinimumConfidence);
public ToolRegistry(
IEnumerable<IToolImplementation> implementations,
IEnumerable<IToolDefinitionSource> definitionSources,
@@ -370,36 +378,32 @@ public sealed class ToolRegistry
var result = new List<(ToolDefinition, IToolImplementation)>(definitions.Count);
foreach (var definition in definitions)
{
if (!this.settingsManager.IsToolActive(definition.Id))
var check = await this.CheckToolAsync(definition, providerConfidence);
if (check.MinimumConfidence is { } minimumConfidence)
this.logger.LogDebug("Tool '{ToolId}' uses minimum provider confidence '{ConfidenceLevel}' from {Source}.", definition.Id, minimumConfidence.ConfidenceLevel, minimumConfidence.Source);
switch (check)
{
this.logger.LogDebug("Skipping tool '{ToolId}' because it is disabled by managed configuration.", definition.Id);
continue;
case { BlockReason: ToolOfferBlockReason.NONE, Implementation: { } implementation }:
result.Add((definition, implementation));
break;
case { BlockReason: ToolOfferBlockReason.TOOL_SWITCHED_OFF }:
this.logger.LogDebug("Skipping tool '{ToolId}' because it is disabled by managed configuration.", definition.Id);
break;
case { BlockReason: ToolOfferBlockReason.NOT_CONFIGURED }:
this.logger.LogDebug("Skipping tool '{ToolId}' because it is not configured.", definition.Id);
break;
case { BlockReason: ToolOfferBlockReason.PROVIDER_CONFIDENCE_TOO_LOW }:
this.logger.LogInformation("Skipping tool '{ToolId}' because provider confidence '{ProviderConfidence}' is below the required minimum '{MinimumConfidence}'.", definition.Id, providerConfidence, check.MinimumConfidence?.ConfidenceLevel);
break;
case { BlockReason: ToolOfferBlockReason.NOT_AVAILABLE_HERE }:
this.logger.LogWarning("Skipping tool '{ToolId}' because no implementation is registered.", definition.Id);
break;
}
if (!this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation))
{
this.logger.LogWarning("Skipping tool '{ToolId}' because no implementation is registered.", definition.Id);
continue;
}
var configurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition, implementation);
if (!configurationState.IsConfigured)
{
this.logger.LogDebug("Skipping tool '{ToolId}' because it is not configured.", definition.Id);
continue;
}
var resolution = this.settingsManager.GetMinimumProviderConfidenceResolutionForTool(definition.Id, definition.MinimumProviderConfidence);
var minimumToolConfidence = resolution.ConfidenceLevel;
this.logger.LogDebug("Tool '{ToolId}' uses minimum provider confidence '{ConfidenceLevel}' from {Source}.", definition.Id, minimumToolConfidence, resolution.Source);
if (!ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumToolConfidence))
{
this.logger.LogInformation("Skipping tool '{ToolId}' because provider confidence '{ProviderConfidence}' is below the required minimum '{MinimumConfidence}'.", definition.Id, providerConfidence, minimumToolConfidence);
continue;
}
result.Add((definition, implementation));
}
foreach (var selectedToolId in selectedToolIdSet.Where(selectedToolId => definitions.All(definition => !definition.Id.Equals(selectedToolId, StringComparison.Ordinal))))
@@ -407,4 +411,63 @@ public sealed class ToolRegistry
return result;
}
/// <summary>
/// Whether a tool can be offered to a provider in this component, and if not, what is in the way.
/// </summary>
/// <remarks>
/// Asks the same questions, in the same order, as the preparation of a request does, because
/// whoever decides something on the tool's behalf must not come to another answer than the
/// request will. The RAG process, for instance, leaves the searching of the data sources to
/// Semantic Search only when this says it can be offered; checks of its own which forgot one
/// of these would leave a chat without its data sources.<br/><br/>
/// Two questions stay out. Whether the tool is selected is the caller's business, and whether
/// the tool has anything to offer right now depends on the chat, so only the preparation of a
/// request can answer it.
/// </remarks>
/// <param name="toolId">The tool to check.</param>
/// <param name="provider">The provider the request would go to.</param>
/// <param name="component">Where the request would come from.</param>
/// <returns>ToolOfferBlockReason.NONE when nothing is in the way, otherwise the first obstacle found.</returns>
public async Task<ToolOfferBlockReason> GetOfferBlockReasonAsync(string toolId, AIStudio.Settings.Provider provider, Components component)
{
if (!this.settingsManager.AreToolsEnabled())
return ToolOfferBlockReason.TOOLS_SWITCHED_OFF;
if (!provider.GetToolCallingAvailability().IsAvailable)
return ToolOfferBlockReason.MODEL_CANNOT_USE_TOOLS;
if (this.GetDefinition(toolId) is not { } definition || !definition.VisibleIn.IsVisibleIn(component))
return ToolOfferBlockReason.NOT_AVAILABLE_HERE;
var providerConfidence = provider.UsedLLMProvider.GetConfidence(this.settingsManager).Level;
return (await this.CheckToolAsync(definition, providerConfidence)).BlockReason;
}
/// <summary>
/// Checks one tool on its own, apart from what applies to all tools of a request.
/// </summary>
/// <remarks>
/// Shared by the preparation of a request and by GetOfferBlockReasonAsync, so the two cannot
/// drift apart. It reports rather than logs: the preparation of a request writes down why a
/// tool was left out, while a question asked by the user interface on every render must not.
/// </remarks>
private async Task<ToolCheck> CheckToolAsync(ToolDefinition definition, ConfidenceLevel providerConfidence)
{
if (!this.settingsManager.IsToolActive(definition.Id))
return new(ToolOfferBlockReason.TOOL_SWITCHED_OFF, null, null);
if (!this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation))
return new(ToolOfferBlockReason.NOT_AVAILABLE_HERE, null, null);
var configurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition, implementation);
if (!configurationState.IsConfigured)
return new(ToolOfferBlockReason.NOT_CONFIGURED, implementation, null);
var minimumConfidence = this.settingsManager.GetMinimumProviderConfidenceResolutionForTool(definition.Id, definition.MinimumProviderConfidence);
if (!ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumConfidence.ConfidenceLevel))
return new(ToolOfferBlockReason.PROVIDER_CONFIDENCE_TOO_LOW, implementation, minimumConfidence);
return new(ToolOfferBlockReason.NONE, implementation, minimumConfidence);
}
}