mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-17 06:38:00 +00:00
Added mailboxes as local data sources (#1022)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
This commit is contained in:
1 parent
93be539e50
commit
c4400c0ff6
343 files changed
+26051
-3122
No files matched your search
@@ -13,7 +13,10 @@ namespace AIStudio.Tools.ToolCallingSystem;
|
||||
/// <remarks>
|
||||
/// Definitions arrive through tool definition sources — the app's own tools from code, later the
|
||||
/// ones plugin authors write. Every definition passes the same validation regardless of where it
|
||||
/// came from, which matters most for the ones AI Studio does not control.
|
||||
/// came from, which matters most for the ones AI Studio does not control.<br/><br/>
|
||||
/// Every tool belongs to exactly one collection, see ToolCollectionDefinition: a declared one, or
|
||||
/// one of its own under its own ID. Whether a tool is switched off and which confidence it needs
|
||||
/// are questions about its collection, so they are answered here, where the collections are known.
|
||||
/// </remarks>
|
||||
public sealed class ToolRegistry
|
||||
{
|
||||
@@ -22,6 +25,15 @@ public sealed class ToolRegistry
|
||||
private readonly ToolSettingsService toolSettingsService;
|
||||
private readonly Dictionary<string, ToolDefinition> definitionsById = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, IToolImplementation> implementationsByKey = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, RegisteredCollection> collectionsById = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, string> collectionIdsByToolId = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// A declared collection as registered, holding only the tools which are registered themselves.
|
||||
/// </summary>
|
||||
/// <param name="Definition">The definition, reduced to the registered tools.</param>
|
||||
/// <param name="Collection">The collection, which presents the definition.</param>
|
||||
private sealed record RegisteredCollection(ToolCollectionDefinition Definition, IToolCollection Collection);
|
||||
|
||||
/// <summary>
|
||||
/// What the checks of a single tool found.
|
||||
@@ -34,6 +46,7 @@ public sealed class ToolRegistry
|
||||
public ToolRegistry(
|
||||
IEnumerable<IToolImplementation> implementations,
|
||||
IEnumerable<IToolDefinitionSource> definitionSources,
|
||||
IEnumerable<IToolCollection> collections,
|
||||
SettingsManager settingsManager,
|
||||
ToolSettingsService toolSettingsService,
|
||||
ILogger<ToolRegistry> logger)
|
||||
@@ -88,6 +101,66 @@ public sealed class ToolRegistry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After the tools, since a collection can only gather tools which are registered:
|
||||
foreach (var collection in collections)
|
||||
this.RegisterCollection(collection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a declared tool collection, with those of its tools which are registered.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A tool belongs to one collection at most, or switching one collection off would take a tool
|
||||
/// of another one along. A collection may not take the ID of a tool either, since a tool which
|
||||
/// belongs to no collection forms one under its own ID. A tool which offers itself from the
|
||||
/// context of a chat stays out, because nobody selects it, see ToolActivation.CONTEXT.<br/><br/>
|
||||
/// A tool the collection names but which is not registered is left out with a warning, so the
|
||||
/// others still work as one. A collection left without any tool is skipped.
|
||||
/// </remarks>
|
||||
private void RegisterCollection(IToolCollection collection)
|
||||
{
|
||||
var definition = collection.GetDefinition();
|
||||
if (string.IsNullOrWhiteSpace(definition.Id))
|
||||
{
|
||||
this.logger.LogWarning("Skipping a tool collection with an empty ID.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.definitionsById.ContainsKey(definition.Id))
|
||||
{
|
||||
this.logger.LogWarning("Skipping tool collection '{CollectionId}' because a tool has the same ID.", definition.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.collectionsById.ContainsKey(definition.Id))
|
||||
{
|
||||
this.logger.LogWarning("Skipping duplicate tool collection ID '{CollectionId}'.", definition.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var toolIds = new List<string>(definition.ToolIds.Count);
|
||||
foreach (var toolId in definition.ToolIds.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
if (this.definitionsById.GetValueOrDefault(toolId) is not { } toolDefinition)
|
||||
this.logger.LogWarning("Leaving tool '{ToolId}' out of tool collection '{CollectionId}' because the tool is not registered.", toolId, definition.Id);
|
||||
else if (toolDefinition.Activation is not ToolActivation.SELECTION)
|
||||
this.logger.LogWarning("Leaving tool '{ToolId}' out of tool collection '{CollectionId}' because nobody selects the tool.", toolId, definition.Id);
|
||||
else if (this.collectionIdsByToolId.TryGetValue(toolId, out var otherCollectionId))
|
||||
this.logger.LogWarning("Leaving tool '{ToolId}' out of tool collection '{CollectionId}' because it belongs to tool collection '{OtherCollectionId}' already.", toolId, definition.Id, otherCollectionId);
|
||||
else
|
||||
toolIds.Add(toolId);
|
||||
}
|
||||
|
||||
if (toolIds.Count == 0)
|
||||
{
|
||||
this.logger.LogWarning("Skipping tool collection '{CollectionId}' because none of its tools is registered.", definition.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
this.collectionsById[definition.Id] = new(definition with { ToolIds = toolIds }, collection);
|
||||
foreach (var toolId in toolIds)
|
||||
this.collectionIdsByToolId[toolId] = definition.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -214,46 +287,199 @@ public sealed class ToolRegistry
|
||||
public IReadOnlyList<ToolDefinition> GetDefinitionsForComponent(Components component)
|
||||
{
|
||||
return this.definitionsById.Values
|
||||
.Where(x => x.VisibleIn.IsVisibleIn(component))
|
||||
.Where(x => x.VisibleIn.IsVisibleIn(component) && !this.IsUnavailable(x))
|
||||
.OrderBy(x => this.implementationsByKey.GetValueOrDefault(x.ImplementationKey)?.GetDisplayName(), StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public IReadOnlyList<ToolDefinition> GetAllDefinitions() => this.definitionsById.Values
|
||||
.Where(x => !this.IsUnavailable(x))
|
||||
.OrderBy(x => this.implementationsByKey.GetValueOrDefault(x.ImplementationKey)?.GetDisplayName(), StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the implementation of a tool says it does not exist right now, e.g., while its preview is switched off.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A definition without an implementation is not unavailable in this sense: the lists keep it
|
||||
/// as before, and every check leaves it out as a tool nobody knows.
|
||||
/// </remarks>
|
||||
private bool IsUnavailable(ToolDefinition definition) => this.implementationsByKey.GetValueOrDefault(definition.ImplementationKey) is { IsAvailable: false };
|
||||
|
||||
public ToolDefinition? GetDefinition(string toolId) => this.definitionsById.GetValueOrDefault(toolId);
|
||||
|
||||
public IToolImplementation? GetImplementation(string implementationKey) => this.implementationsByKey.GetValueOrDefault(implementationKey);
|
||||
|
||||
/// <summary>
|
||||
/// The provider confidence a tool needs: its own minimum, unless the user or an administrator
|
||||
/// raised or lowered it.
|
||||
/// The ID of the collection a tool belongs to.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the place that knows both halves — the definition's own minimum and the stored
|
||||
/// overrides — so callers holding only a tool ID come here instead of to the settings.
|
||||
/// A tool which belongs to no declared collection forms one of its own, so this is its own ID
|
||||
/// then. The ID of a collection stays as it is, and so does an ID the registry does not know,
|
||||
/// such as one of a tool from another installation.
|
||||
/// </remarks>
|
||||
public ConfidenceLevel GetMinimumProviderConfidence(string toolId) => this.GetDefinition(toolId) is { } definition
|
||||
? this.GetMinimumProviderConfidence(definition)
|
||||
: ConfidenceLevel.NONE;
|
||||
|
||||
public ConfidenceLevel GetMinimumProviderConfidence(ToolDefinition definition) =>
|
||||
this.settingsManager.GetMinimumProviderConfidenceForTool(definition.Id, definition.MinimumProviderConfidence);
|
||||
/// <param name="toolOrCollectionId">The ID of a tool, or of a collection.</param>
|
||||
/// <returns>The ID of the collection.</returns>
|
||||
public string GetCollectionId(string toolOrCollectionId) => this.collectionIdsByToolId.GetValueOrDefault(toolOrCollectionId, toolOrCollectionId);
|
||||
|
||||
/// <summary>
|
||||
/// Narrows a selection of tool IDs to those the given provider may actually use.
|
||||
/// Whether a tool may be used at all: tools are switched on, and the organization did not switch 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.
|
||||
/// </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)
|
||||
{
|
||||
if (!this.settingsManager.AreToolsEnabled())
|
||||
return false;
|
||||
|
||||
var disabledIds = this.settingsManager.ConfigurationData.Tools.DisabledToolIds;
|
||||
return !this.GetSettingsIds(this.GetCollectionId(toolOrCollectionId)).Any(disabledIds.Contains);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The provider confidence a tool needs: the minimum of its collection, unless the user or an
|
||||
/// administrator raised or lowered it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the place that knows both halves — the collection's own minimum and the stored
|
||||
/// overrides — so callers holding only a tool ID come here instead of to the settings.
|
||||
/// </remarks>
|
||||
/// <param name="toolOrCollectionId">The ID of a tool, or of a collection.</param>
|
||||
public ConfidenceLevel GetMinimumProviderConfidence(string toolOrCollectionId) => this.GetMinimumProviderConfidenceResolution(toolOrCollectionId).ConfidenceLevel;
|
||||
|
||||
public ConfidenceLevel GetMinimumProviderConfidence(ToolDefinition definition) => this.GetMinimumProviderConfidence(definition.Id);
|
||||
|
||||
/// <summary>
|
||||
/// Stores which provider confidence the collection of a tool needs, as the user chose it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Whatever tool of a collection the level is chosen for, it is stored for the collection, so
|
||||
/// all of its tools need the same level afterward.
|
||||
/// </remarks>
|
||||
/// <param name="toolOrCollectionId">The ID of a tool, or of a collection.</param>
|
||||
/// <param name="confidenceLevel">The level the user chose. Choosing the collection's own minimum removes the override.</param>
|
||||
public void SetMinimumProviderConfidence(string toolOrCollectionId, ConfidenceLevel confidenceLevel)
|
||||
{
|
||||
var collectionId = this.GetCollectionId(toolOrCollectionId);
|
||||
this.settingsManager.SetMinimumProviderConfidence(this.GetSettingsIds(collectionId), confidenceLevel, this.GetDefaultMinimumProviderConfidence(collectionId));
|
||||
}
|
||||
|
||||
private SettingsManager.ToolMinimumProviderConfidenceResolution GetMinimumProviderConfidenceResolution(string toolOrCollectionId)
|
||||
{
|
||||
var collectionId = this.GetCollectionId(toolOrCollectionId);
|
||||
return this.settingsManager.GetMinimumProviderConfidenceResolution(this.GetSettingsIds(collectionId), this.GetDefaultMinimumProviderConfidence(collectionId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The minimum a collection asks for itself: the one it declares, or that of the tool which forms it.
|
||||
/// </summary>
|
||||
private ConfidenceLevel GetDefaultMinimumProviderConfidence(string collectionId)
|
||||
{
|
||||
if (this.collectionsById.TryGetValue(collectionId, out var collection))
|
||||
return collection.Definition.MinimumProviderConfidence;
|
||||
|
||||
return this.GetDefinition(collectionId)?.MinimumProviderConfidence ?? ConfidenceLevel.NONE;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The IDs which stand for a collection in the settings: its own first, then those of its tools.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The ID of a tool stands for its collection, so an entry made for the tool before it joined
|
||||
/// the collection, or by an administrator who named the tool, still counts.
|
||||
/// </remarks>
|
||||
private IReadOnlyList<string> GetSettingsIds(string collectionId) => [collectionId, ..this.GetToolIdsOfCollection(collectionId).Where(toolId => toolId != collectionId)];
|
||||
|
||||
/// <summary>
|
||||
/// The IDs of the tools of a collection: those of a declared one, or the ID of the tool which forms it.
|
||||
/// </summary>
|
||||
private IReadOnlyList<string> GetToolIdsOfCollection(string collectionId) => this.collectionsById.TryGetValue(collectionId, out var collection)
|
||||
? collection.Definition.ToolIds
|
||||
: [collectionId];
|
||||
|
||||
/// <summary>
|
||||
/// Whether this installation knows a tool or a tool collection by this ID.
|
||||
/// </summary>
|
||||
/// <param name="toolOrCollectionId">The ID of a tool, or of a collection.</param>
|
||||
public bool IsKnown(string toolOrCollectionId) => this.definitionsById.ContainsKey(toolOrCollectionId) || this.collectionsById.ContainsKey(toolOrCollectionId);
|
||||
|
||||
/// <summary>
|
||||
/// Turns a selection into the collections which actually run.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every ID turns into the ID of its collection: a selection which names one tool of a
|
||||
/// collection, such as one stored before the tool joined it, selects the whole collection.
|
||||
/// Duplicates go, and so do 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. An ID this installation does not know stays, because the tool may
|
||||
/// arrive with a plugin installed later.<br/><br/>
|
||||
/// It also adds what a collection depends on: Search Confluence only finds pages, so it brings
|
||||
/// Read Web Page along to open them. An added collection keeps its own rules. It is still
|
||||
/// dropped 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 allowed there.<br/><br/>
|
||||
/// 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 collections
|
||||
/// which will actually run.
|
||||
/// </remarks>
|
||||
/// <param name="selectedIds">The IDs of the selected tools or collections.</param>
|
||||
/// <returns>The IDs of the collections, as a new set.</returns>
|
||||
public HashSet<string> NormalizeSelection(IEnumerable<string> selectedIds)
|
||||
{
|
||||
var normalized = selectedIds.Select(this.GetCollectionId).ToHashSet(StringComparer.Ordinal);
|
||||
if (normalized.Contains(this.GetCollectionId(ToolSelectionRules.SEARCH_CONFLUENCE_TOOL_ID)))
|
||||
normalized.Add(this.GetCollectionId(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID));
|
||||
|
||||
normalized.RemoveWhere(id => this.GetDefinition(id) is { Activation: ToolActivation.CONTEXT });
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The tools a selection runs: each tool of each collection it selects.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The one place where a collection turns into its tools. People select and configure
|
||||
/// collections, but the model sees every tool on its own, so whatever prepares or counts a
|
||||
/// request, and whatever shows what a plugin will run, comes here. An ID this installation does
|
||||
/// not know stays as it is.
|
||||
/// </remarks>
|
||||
/// <param name="selectedIds">The IDs of the selected tools or collections.</param>
|
||||
/// <returns>The IDs of the tools, as a new set.</returns>
|
||||
public HashSet<string> ExpandSelection(IEnumerable<string> selectedIds) => this.NormalizeSelection(selectedIds)
|
||||
.SelectMany(this.GetToolIdsOfCollection)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// The collections preselected in a component, as the user chose them in the settings.
|
||||
/// </summary>
|
||||
public HashSet<string> GetDefaultToolIds(Components component) => this.settingsManager.ConfigurationData.Tools.DefaultToolIdsByComponent.TryGetValue(component.ToString(), out var toolIds)
|
||||
? this.NormalizeSelection(toolIds)
|
||||
: [];
|
||||
|
||||
/// <summary>
|
||||
/// Narrows a selection to the tools the given provider may actually use in this chat.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used before a request is sent, so the chat records what will really be available rather
|
||||
/// than what the user once ticked. Lives here because judging a tool needs its definition:
|
||||
/// the settings know the overrides, the definition knows the tool's own minimum.
|
||||
/// than what the user once ticked, and by the token count below the message field, so a tool
|
||||
/// the request leaves out does not count. Lives here because judging a tool needs its
|
||||
/// collection: the settings know the overrides, the collection knows its own minimum.
|
||||
/// Where the chat may still send data is judged with the rule the preparation of a request
|
||||
/// uses, see CheckToolAsync.<br/><br/>
|
||||
/// Returns tools rather than collections, since a collection may lose some of its tools here,
|
||||
/// e.g., one which a mailbox read by the chat keeps back. Handed to a request again, they turn
|
||||
/// into their whole collection once more, and the request leaves out the same tools again.
|
||||
/// </remarks>
|
||||
/// <param name="provider">The provider the request goes to.</param>
|
||||
/// <param name="selectedToolIds">The tools the user selected.</param>
|
||||
/// <returns>The subset that is enabled, active, and allowed by the provider's confidence.</returns>
|
||||
public HashSet<string> FilterToolIdsForProvider(AIStudio.Settings.Provider provider, IEnumerable<string> selectedToolIds)
|
||||
/// <param name="selectedToolIds">The tools or collections the user selected.</param>
|
||||
/// <param name="outboundDataRestriction">Where the chat may still send data, see ChatThread.RequiredOutboundDataRestriction.</param>
|
||||
/// <returns>The IDs of the selected tools that are enabled, active, available, allowed by the provider's confidence, and allowed by the outbound data restriction.</returns>
|
||||
public HashSet<string> FilterToolIdsForProvider(AIStudio.Settings.Provider provider, IEnumerable<string> selectedToolIds, OutboundDataRestriction outboundDataRestriction)
|
||||
{
|
||||
if (!this.settingsManager.AreToolsEnabled())
|
||||
return [];
|
||||
@@ -262,16 +488,25 @@ public sealed class ToolRegistry
|
||||
return [];
|
||||
|
||||
var providerConfidence = provider.UsedLLMProvider.GetConfidence(this.settingsManager).Level;
|
||||
var filtered = ToolSelectionRules.NormalizeSelection(selectedToolIds);
|
||||
var filtered = this.ExpandSelection(selectedToolIds);
|
||||
foreach (var toolId in filtered.ToList())
|
||||
{
|
||||
if (!this.settingsManager.IsToolActive(toolId))
|
||||
if (!this.IsToolActive(toolId))
|
||||
{
|
||||
filtered.Remove(toolId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, this.GetMinimumProviderConfidence(toolId)))
|
||||
{
|
||||
filtered.Remove(toolId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.GetDefinition(toolId) is not { } definition || !this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation))
|
||||
continue;
|
||||
|
||||
if (!implementation.IsAvailable || !ToolSelectionRules.IsOutboundDataAllowed(outboundDataRestriction, implementation))
|
||||
filtered.Remove(toolId);
|
||||
}
|
||||
|
||||
@@ -279,7 +514,7 @@ public sealed class ToolRegistry
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The tools somebody can select in this component.
|
||||
/// The collections 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
|
||||
@@ -295,7 +530,7 @@ public sealed class ToolRegistry
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reduces a set of tool IDs to the tools a user could switch on themselves in this component.
|
||||
/// Reduces a set of IDs to the collections a user could switch on themselves in this component.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For preselecting tools on someone's behalf, such as when a launcher opens a chat. A tool
|
||||
@@ -307,37 +542,84 @@ public sealed class ToolRegistry
|
||||
/// </remarks>
|
||||
public async Task<HashSet<string>> FilterSelectableToolIdsAsync(Components component, IEnumerable<string> toolIds)
|
||||
{
|
||||
var wantedToolIds = ToolSelectionRules.NormalizeSelection(toolIds);
|
||||
if (wantedToolIds.Count is 0 || !this.settingsManager.AreToolsEnabled())
|
||||
var wantedIds = this.NormalizeSelection(toolIds);
|
||||
if (wantedIds.Count is 0 || !this.settingsManager.AreToolsEnabled())
|
||||
return [];
|
||||
|
||||
var catalog = await this.GetCatalogAsync(component);
|
||||
return catalog
|
||||
.Where(x => wantedToolIds.Contains(x.Definition.Id) && x is { IsActive: true, ConfigurationState.IsConfigured: true })
|
||||
.Select(x => x.Definition.Id)
|
||||
.Where(x => wantedIds.Contains(x.Id) && x is { IsActive: true, ConfigurationState.IsConfigured: true })
|
||||
.Select(x => x.Id)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The entry of one collection as people see it, with all of its tools which exist here.
|
||||
/// </summary>
|
||||
/// <param name="toolOrCollectionId">The ID of the collection, or of one of its tools.</param>
|
||||
/// <returns>The entry, or null when none of its tools exists here.</returns>
|
||||
public async Task<ToolCatalogItem?> GetCatalogItemAsync(string toolOrCollectionId)
|
||||
{
|
||||
var definitions = this.GetToolIdsOfCollection(this.GetCollectionId(toolOrCollectionId))
|
||||
.Select(this.GetDefinition)
|
||||
.OfType<ToolDefinition>();
|
||||
|
||||
return (await this.GetCatalogAsync(definitions)).SingleOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The entries for these tools as people see them: one per collection, ordered by name.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A collection holds only those of its tools which are among the given ones and exist right
|
||||
/// now, so it disappears with the last of them, e.g., while the preview of its tools is off.
|
||||
/// </remarks>
|
||||
public async Task<IReadOnlyList<ToolCatalogItem>> GetCatalogAsync(IEnumerable<ToolDefinition> definitions)
|
||||
{
|
||||
var definitionList = definitions.ToList();
|
||||
var items = new List<ToolCatalogItem>(definitionList.Count);
|
||||
foreach (var definition in definitionList)
|
||||
var toolsByCollectionId = new Dictionary<string, List<ToolCatalogTool>>(StringComparer.Ordinal);
|
||||
foreach (var definition in definitions)
|
||||
{
|
||||
if (!this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation))
|
||||
if (!this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation) || !implementation.IsAvailable)
|
||||
continue;
|
||||
|
||||
items.Add(new ToolCatalogItem
|
||||
var collectionId = this.GetCollectionId(definition.Id);
|
||||
if (!toolsByCollectionId.TryGetValue(collectionId, out var tools))
|
||||
{
|
||||
Definition = definition,
|
||||
Implementation = implementation,
|
||||
ConfigurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition, implementation),
|
||||
IsActive = this.settingsManager.IsToolActive(definition.Id),
|
||||
MinimumProviderConfidence = this.GetMinimumProviderConfidence(definition),
|
||||
});
|
||||
tools = [];
|
||||
toolsByCollectionId[collectionId] = tools;
|
||||
}
|
||||
|
||||
tools.Add(new(definition, implementation, await this.toolSettingsService.GetConfigurationStateAsync(definition, implementation)));
|
||||
}
|
||||
|
||||
return items;
|
||||
return toolsByCollectionId
|
||||
.Select(entry => this.CreateCatalogItem(entry.Key, entry.Value))
|
||||
.OrderBy(item => item.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes one collection with the given tools: a declared one as it presents itself, otherwise the tool which forms it.
|
||||
/// </summary>
|
||||
private ToolCatalogItem CreateCatalogItem(string collectionId, List<ToolCatalogTool> tools)
|
||||
{
|
||||
var declaredCollection = this.collectionsById.GetValueOrDefault(collectionId);
|
||||
if (declaredCollection is not null)
|
||||
tools = declaredCollection.Definition.ToolIds.Join(tools, toolId => toolId, tool => tool.Definition.Id, (_, tool) => tool).ToList();
|
||||
|
||||
var firstTool = tools[0];
|
||||
return new()
|
||||
{
|
||||
Id = collectionId,
|
||||
Icon = declaredCollection?.Collection.Icon ?? firstTool.Implementation.Icon,
|
||||
DisplayName = declaredCollection?.Collection.GetDisplayName() ?? firstTool.Implementation.GetDisplayName(),
|
||||
Description = declaredCollection?.Collection.GetDescription() ?? firstTool.Implementation.GetDescription(),
|
||||
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),
|
||||
MinimumProviderConfidence = this.GetMinimumProviderConfidence(collectionId),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -347,13 +629,13 @@ public sealed class ToolRegistry
|
||||
/// 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.<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 and
|
||||
/// IToolImplementation.ResolveSystemPromptInstructionsAsync.
|
||||
/// The candidates are the tools of the selected collections 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
|
||||
/// and IToolImplementation.ResolveSystemPromptInstructionsAsync.
|
||||
/// </remarks>
|
||||
/// <param name="context">The request being prepared.</param>
|
||||
/// <param name="selectedToolIds">The tools selected for the request.</param>
|
||||
/// <param name="selectedToolIds">The tools or collections 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>
|
||||
@@ -386,17 +668,18 @@ public sealed class ToolRegistry
|
||||
return [];
|
||||
}
|
||||
|
||||
var selectedToolIdSet = ToolSelectionRules.NormalizeSelection(selectedToolIds);
|
||||
var selectedToolIdSet = this.ExpandSelection(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 => x.Activation is ToolActivation.CONTEXT || selectedToolIdSet.Contains(x.Id))
|
||||
.ToList();
|
||||
|
||||
var outboundDataRestriction = context.ChatThread.RequiredOutboundDataRestriction.Restriction;
|
||||
var result = new List<(ToolDefinition, IToolImplementation)>(definitions.Count);
|
||||
foreach (var definition in definitions)
|
||||
{
|
||||
var check = await this.CheckToolAsync(definition, providerConfidence);
|
||||
var check = await this.CheckToolAsync(definition, providerConfidence, outboundDataRestriction);
|
||||
if (check.MinimumConfidence is { } minimumConfidence)
|
||||
this.logger.LogDebug("Tool '{ToolId}' uses minimum provider confidence '{ConfidenceLevel}' from {Source}.", definition.Id, minimumConfidence.ConfidenceLevel, minimumConfidence.Source);
|
||||
|
||||
@@ -420,6 +703,10 @@ public sealed class ToolRegistry
|
||||
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.OUTBOUND_DATA_RESTRICTED }:
|
||||
this.logger.LogInformation("Skipping tool '{ToolId}' because the chat read from a mailbox which restricts outbound data to '{OutboundDataRestriction}'.", definition.Id, outboundDataRestriction);
|
||||
break;
|
||||
|
||||
case { BlockReason: ToolOfferBlockReason.NOT_AVAILABLE_HERE }:
|
||||
this.logger.LogWarning("Skipping tool '{ToolId}' because no implementation is registered.", definition.Id);
|
||||
break;
|
||||
@@ -441,9 +728,12 @@ public sealed class ToolRegistry
|
||||
/// 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
|
||||
/// Three 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.
|
||||
/// request can answer it. Where the chat may still send data belongs to the chat as well, see
|
||||
/// ChatThread.RequiredOutboundDataRestriction, so the tool is judged as for a chat which read
|
||||
/// no mailbox. Semantic Search, which the RAG process asks about, only reaches services
|
||||
/// configured in AI Studio, and no restriction ever keeps it back.
|
||||
/// </remarks>
|
||||
/// <param name="toolId">The tool to check.</param>
|
||||
/// <param name="provider">The provider the request would go to.</param>
|
||||
@@ -461,7 +751,7 @@ public sealed class ToolRegistry
|
||||
return ToolOfferBlockReason.NOT_AVAILABLE_HERE;
|
||||
|
||||
var providerConfidence = provider.UsedLLMProvider.GetConfidence(this.settingsManager).Level;
|
||||
return (await this.CheckToolAsync(definition, providerConfidence)).BlockReason;
|
||||
return (await this.CheckToolAsync(definition, providerConfidence, OutboundDataRestriction.UNRESTRICTED)).BlockReason;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -498,22 +788,25 @@ public sealed class ToolRegistry
|
||||
/// 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)
|
||||
private async Task<ToolCheck> CheckToolAsync(ToolDefinition definition, ConfidenceLevel providerConfidence, OutboundDataRestriction outboundDataRestriction)
|
||||
{
|
||||
if (!this.settingsManager.IsToolActive(definition.Id))
|
||||
if (!this.IsToolActive(definition.Id))
|
||||
return new(ToolOfferBlockReason.TOOL_SWITCHED_OFF, null, null);
|
||||
|
||||
if (!this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation))
|
||||
if (!this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation) || !implementation.IsAvailable)
|
||||
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);
|
||||
var minimumConfidence = this.GetMinimumProviderConfidenceResolution(definition.Id);
|
||||
if (!ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumConfidence.ConfidenceLevel))
|
||||
return new(ToolOfferBlockReason.PROVIDER_CONFIDENCE_TOO_LOW, implementation, minimumConfidence);
|
||||
|
||||
if (!ToolSelectionRules.IsOutboundDataAllowed(outboundDataRestriction, implementation))
|
||||
return new(ToolOfferBlockReason.OUTBOUND_DATA_RESTRICTED, implementation, minimumConfidence);
|
||||
|
||||
return new(ToolOfferBlockReason.NONE, implementation, minimumConfidence);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user