mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 10:13:48 +00:00
Let tools tailor what they offer to each request
This commit is contained in:
12 files changed
+452
-126
No files matched your search
@@ -10,6 +10,8 @@ namespace AIStudio.Tools.ToolCallingSystem;
|
||||
/// them all the same way.<br/><br/>
|
||||
/// A source is asked once while the registry is being built. Definitions do not change while the
|
||||
/// app runs; a plugin that was loaded later needs the registry rebuilt, not the source re-read.
|
||||
/// What a tool offers in a single request may still differ from its definition: the tool tailors
|
||||
/// its function to the request then, see IToolImplementation.ResolveFunctionAsync.
|
||||
/// </remarks>
|
||||
public interface IToolDefinitionSource
|
||||
{
|
||||
|
||||
@@ -19,6 +19,31 @@ public interface IToolImplementation
|
||||
/// </remarks>
|
||||
public ToolDefinition GetDefinition();
|
||||
|
||||
/// <summary>
|
||||
/// The function this tool offers the model in the request being prepared, or null when it has
|
||||
/// nothing to offer there.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A definition is registered once, but some tools cannot say what they offer until they know
|
||||
/// the request. Semantic Search describes the data sources of the chat, and only those the
|
||||
/// provider may search; without any of them, it has nothing to offer, and the model should not
|
||||
/// learn about a tool which can only come back empty. Most tools offer the same function every
|
||||
/// time, which is what this returns unless a tool says otherwise.<br/><br/>
|
||||
/// Asked for every request, after every check of ToolRegistry has passed, so it only decides
|
||||
/// what an allowed tool offers, never whether it is allowed. For the same reason, only the
|
||||
/// description and the parameters of what comes back are used: the function keeps the name and
|
||||
/// the strict mode it was registered with, and the definition everything else. A tool which
|
||||
/// throws is left out of the request.<br/><br/>
|
||||
/// Keep the result stable while the chat stays the same, down to the order of what it lists:
|
||||
/// the providers cache a request from its beginning, and the tools are part of that beginning.
|
||||
/// </remarks>
|
||||
/// <param name="definition">The definition as registered.</param>
|
||||
/// <param name="context">The request being prepared.</param>
|
||||
/// <param name="token">The cancellation token of the request.</param>
|
||||
/// <returns>The function to offer, or null to leave the tool out of this request.</returns>
|
||||
public ValueTask<ToolFunctionDefinition?> ResolveFunctionAsync(ToolDefinition definition, ToolResolutionContext context, CancellationToken token = default) =>
|
||||
ValueTask.FromResult<ToolFunctionDefinition?>(definition.Function);
|
||||
|
||||
public string Icon => Icons.Material.Filled.Build;
|
||||
|
||||
public IReadOnlySet<string> SensitiveTraceArgumentNames { get; }
|
||||
|
||||
@@ -2,7 +2,14 @@ using AIStudio.Provider;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
public sealed class ToolDefinition
|
||||
/// <summary>
|
||||
/// What a tool is: what the model may call, which settings it needs, and where it may be used.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A record, so that the registry can hand out a definition whose function a tool tailored to one
|
||||
/// request while everything else stays as registered, see IToolImplementation.ResolveFunctionAsync.
|
||||
/// </remarks>
|
||||
public sealed record ToolDefinition
|
||||
{
|
||||
public int SchemaVersion { get; init; } = 1;
|
||||
|
||||
|
||||
@@ -2,7 +2,14 @@ using System.Text.Json;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
public sealed class ToolFunctionDefinition
|
||||
/// <summary>
|
||||
/// The function a tool offers the model: its name, what it does, and the arguments it takes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A record, so that a tool tailoring its function to a request changes only what it has to, e.g.
|
||||
/// definition.Function with { DescriptionForLLM = … }, see IToolImplementation.ResolveFunctionAsync.
|
||||
/// </remarks>
|
||||
public sealed record ToolFunctionDefinition
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
|
||||
@@ -339,14 +339,27 @@ public sealed class ToolRegistry
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The tools a request offers the model, each with the function it offers in this request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Model capabilities are not a parameter on purpose: they are read from the given provider,
|
||||
/// which carries the user's expert capability overrides. Passing them in separately allowed a
|
||||
/// caller to gate tools on capabilities that differed from the ones the availability check saw.
|
||||
/// caller to gate tools on capabilities that differed from the ones the availability check saw.<br/><br/>
|
||||
/// The candidates are the selected tools and every tool which offers itself from the context of
|
||||
/// the chat, see ToolActivation. Each one passes the same checks, and only then is it asked what
|
||||
/// it offers in this request, see IToolImplementation.ResolveFunctionAsync.
|
||||
/// </remarks>
|
||||
public async Task<IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)>> GetRunnableToolsAsync(AIStudio.Settings.Provider provider,
|
||||
Components component, IEnumerable<string> selectedToolIds, ConfidenceLevel providerConfidence, bool mayRunTools)
|
||||
/// <param name="context">The request being prepared.</param>
|
||||
/// <param name="selectedToolIds">The tools selected for the request.</param>
|
||||
/// <param name="mayRunTools">Whether the request may run tools at all, as its caller decides.</param>
|
||||
/// <param name="token">The cancellation token of the request.</param>
|
||||
/// <returns>The runnable tools, with their definitions as offered in this request.</returns>
|
||||
public async Task<IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)>> GetRunnableToolsAsync(ToolResolutionContext context, IEnumerable<string> selectedToolIds, bool mayRunTools, CancellationToken token = default)
|
||||
{
|
||||
var provider = context.Provider;
|
||||
var component = context.Component;
|
||||
var providerConfidence = context.ProviderConfidence;
|
||||
if (!this.settingsManager.AreToolsEnabled())
|
||||
{
|
||||
this.logger.LogDebug("Tool calling is skipped because tools are disabled by managed configuration.");
|
||||
@@ -374,7 +387,10 @@ public sealed class ToolRegistry
|
||||
var selectedToolIdSet = ToolSelectionRules.NormalizeSelection(selectedToolIds);
|
||||
this.logger.LogDebug("Resolving runnable tools for provider '{Provider}' with model '{ModelId}'. Selected tool IDs: [{ToolIds}].", provider.InstanceName, provider.Model.Id, string.Join(", ", selectedToolIdSet.OrderBy(x => x, StringComparer.Ordinal)));
|
||||
|
||||
var definitions = this.GetDefinitionsForComponent(component).Where(x => selectedToolIdSet.Contains(x.Id)).ToList();
|
||||
var definitions = this.GetDefinitionsForComponent(component)
|
||||
.Where(x => x.Activation is ToolActivation.CONTEXT || selectedToolIdSet.Contains(x.Id))
|
||||
.ToList();
|
||||
|
||||
var result = new List<(ToolDefinition, IToolImplementation)>(definitions.Count);
|
||||
foreach (var definition in definitions)
|
||||
{
|
||||
@@ -385,7 +401,9 @@ public sealed class ToolRegistry
|
||||
switch (check)
|
||||
{
|
||||
case { BlockReason: ToolOfferBlockReason.NONE, Implementation: { } implementation }:
|
||||
result.Add((definition, implementation));
|
||||
if (await this.ResolveAsync(definition, implementation, context, token) is { } offeredDefinition)
|
||||
result.Add((offeredDefinition, implementation));
|
||||
|
||||
break;
|
||||
|
||||
case { BlockReason: ToolOfferBlockReason.TOOL_SWITCHED_OFF }:
|
||||
@@ -470,4 +488,58 @@ public sealed class ToolRegistry
|
||||
|
||||
return new(ToolOfferBlockReason.NONE, implementation, minimumConfidence);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asks a tool which passed every check what it offers in this request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only the description and the parameters of the answer are taken. The name and the strict
|
||||
/// mode stay as registered, because the model's calls find their tool by that name, and the
|
||||
/// rest of the definition was checked a moment ago and must not change after that.
|
||||
/// </remarks>
|
||||
/// <returns>The definition as offered in this request, or null when the tool has nothing to offer or could not say what.</returns>
|
||||
private async Task<ToolDefinition?> ResolveAsync(ToolDefinition definition, IToolImplementation implementation, ToolResolutionContext context, CancellationToken token)
|
||||
{
|
||||
ToolFunctionDefinition? function;
|
||||
try
|
||||
{
|
||||
function = await implementation.ResolveFunctionAsync(definition, context, token);
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
this.logger.LogError(exception, "Skipping tool '{ToolId}' because it could not say what it offers in this request.", definition.Id);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (function is null)
|
||||
{
|
||||
this.logger.LogDebug("Skipping tool '{ToolId}' because it has nothing to offer in this request.", definition.Id);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(function, definition.Function))
|
||||
return definition;
|
||||
|
||||
if (function.Parameters.ValueKind is not JsonValueKind.Object)
|
||||
{
|
||||
this.logger.LogWarning("Tool '{ToolId}' offered parameters which are not a JSON object schema. It is offered as registered instead.", definition.Id);
|
||||
return definition;
|
||||
}
|
||||
|
||||
if (!string.Equals(function.Name, definition.Function.Name, StringComparison.Ordinal) || function.Strict != definition.Function.Strict)
|
||||
this.logger.LogWarning("Tool '{ToolId}' changed the name or the strict mode of its function for a request. Both stay as registered.", definition.Id);
|
||||
|
||||
return definition with
|
||||
{
|
||||
Function = function with
|
||||
{
|
||||
Name = definition.Function.Name,
|
||||
Strict = definition.Function.Strict,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider;
|
||||
|
||||
namespace AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
/// <summary>
|
||||
/// The request a tool is being prepared for.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// What a tool may look at when it tailors its function to a request, see
|
||||
/// IToolImplementation.ResolveFunctionAsync. Semantic Search, for instance, reads the data sources
|
||||
/// of the chat and describes exactly those which this provider may search.
|
||||
/// </remarks>
|
||||
public sealed class ToolResolutionContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The provider the request goes to, with the expert settings of the user.
|
||||
/// </summary>
|
||||
public required AIStudio.Settings.Provider Provider { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The part of the app the request comes from.
|
||||
/// </summary>
|
||||
public required Components Component { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// How much the provider is trusted.
|
||||
/// </summary>
|
||||
public required ConfidenceLevel ProviderConfidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The chat the request continues.
|
||||
/// </summary>
|
||||
public required ChatThread ChatThread { get; init; }
|
||||
}
|
||||
Reference in new issue
Block a user