use individual confidence levels for each tool to protect tool callings from using unsafe providers

This commit is contained in:
krut_ni 2026-05-12 17:51:11 +02:00
parent 0cd24c5ec1
commit f2c63ac4b5
No known key found for this signature in database
GPG Key ID: A5C0151B4DDB172C
10 changed files with 134 additions and 9 deletions

View File

@ -81,7 +81,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
// Apply the filters for the message bus: // Apply the filters for the message bus:
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.WORKSPACE_LOADED_CHAT_CHANGED ]); this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.WORKSPACE_LOADED_CHAT_CHANGED, Event.CONFIGURATION_CHANGED ]);
// Configure the spellchecking for the user input: // Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
@ -1007,6 +1007,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
case Event.WORKSPACE_LOADED_CHAT_CHANGED: case Event.WORKSPACE_LOADED_CHAT_CHANGED:
await this.LoadedChatChanged(); await this.LoadedChatChanged();
break; break;
case Event.CONFIGURATION_CHANGED:
await this.InvokeAsync(this.StateHasChanged);
break;
} }
} }

View File

@ -1,3 +1,4 @@
@using AIStudio.Provider
@using AIStudio.Tools.ToolCallingSystem @using AIStudio.Tools.ToolCallingSystem
@inherits SettingsPanelBase @inherits SettingsPanelBase
@ -11,6 +12,7 @@
<MudTh>@T("Icon")</MudTh> <MudTh>@T("Icon")</MudTh>
<MudTh>@T("Name")</MudTh> <MudTh>@T("Name")</MudTh>
<MudTh>@T("Description")</MudTh> <MudTh>@T("Description")</MudTh>
<MudTh>@T("Minimum provider confidence")</MudTh>
<MudTh>@T("State")</MudTh> <MudTh>@T("State")</MudTh>
<MudTh>@T("Settings")</MudTh> <MudTh>@T("Settings")</MudTh>
</HeaderContent> </HeaderContent>
@ -24,6 +26,16 @@
<MudTd> <MudTd>
<MudText Typo="Typo.body2">@context.Implementation.GetDescription()</MudText> <MudText Typo="Typo.body2">@context.Implementation.GetDescription()</MudText>
</MudTd> </MudTd>
<MudTd>
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)" Disabled="@this.IsToolConfidenceManaged()">
@foreach (var confidenceLevel in this.GetSelectableConfidenceLevels())
{
<MudMenuItem OnClick="@(async () => await this.ChangeMinimumProviderConfidence(context, confidenceLevel))">
@this.GetConfidenceLevelName(confidenceLevel)
</MudMenuItem>
}
</MudMenu>
</MudTd>
<MudTd> <MudTd>
@if (context.ConfigurationState.IsConfigured) @if (context.ConfigurationState.IsConfigured)
{ {

View File

@ -1,4 +1,6 @@
using AIStudio.Provider;
using AIStudio.Dialogs.Settings; using AIStudio.Dialogs.Settings;
using AIStudio.Settings;
using AIStudio.Tools; using AIStudio.Tools;
using AIStudio.Tools.ToolCallingSystem; using AIStudio.Tools.ToolCallingSystem;
@ -15,8 +17,9 @@ public partial class SettingsPanelTools : SettingsPanelBase
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await base.OnInitializedAsync(); this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
this.items = await this.ToolRegistry.GetCatalogAsync(this.ToolRegistry.GetAllDefinitions()); this.items = await this.ToolRegistry.GetCatalogAsync(this.ToolRegistry.GetAllDefinitions());
await base.OnInitializedAsync();
} }
private async Task OpenSettings(string toolId) private async Task OpenSettings(string toolId)
@ -47,4 +50,40 @@ public partial class SettingsPanelTools : SettingsPanelBase
return item.Implementation.GetSettingsFieldLabel(fieldName, fieldDefinition); return item.Implementation.GetSettingsFieldLabel(fieldName, fieldDefinition);
} }
private IEnumerable<ConfidenceLevel> GetSelectableConfidenceLevels() =>
Enum.GetValues<ConfidenceLevel>().OrderBy(x => x).Where(x => x is not ConfidenceLevel.UNKNOWN);
private string GetCurrentConfidenceLevelName(ToolCatalogItem item) => this.GetConfidenceLevelName(this.GetMinimumProviderConfidence(item));
private string GetConfidenceLevelName(ConfidenceLevel confidenceLevel) => confidenceLevel is ConfidenceLevel.NONE
? this.T("No minimum confidence level chosen")
: confidenceLevel.GetName();
private string SetCurrentConfidenceLevelColorStyle(ToolCatalogItem item) =>
$"background-color: {this.GetMinimumProviderConfidence(item).GetColor(this.SettingsManager)};";
private bool IsToolConfidenceManaged() =>
ManagedConfiguration.TryGet(x => x.Tools, x => x.MinimumProviderConfidenceByToolId, out var meta) && meta.IsLocked;
private ConfidenceLevel GetMinimumProviderConfidence(ToolCatalogItem item) => this.SettingsManager.GetMinimumProviderConfidenceForTool(item.Definition.Id);
private async Task ChangeMinimumProviderConfidence(ToolCatalogItem item, ConfidenceLevel confidenceLevel)
{
this.SettingsManager.SetMinimumProviderConfidenceForTool(item.Definition.Id, confidenceLevel);
await this.SettingsManager.StoreSettings();
this.items = await this.ToolRegistry.GetCatalogAsync(this.ToolRegistry.GetAllDefinitions());
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
switch (triggeredEvent)
{
case Event.CONFIGURATION_CHANGED:
this.items = await this.ToolRegistry.GetCatalogAsync(this.ToolRegistry.GetAllDefinitions());
await this.InvokeAsync(this.StateHasChanged);
break;
}
}
} }

View File

@ -40,10 +40,12 @@
var isSelected = this.SelectedToolIds.Contains(item.Definition.Id); var isSelected = this.SelectedToolIds.Contains(item.Definition.Id);
var isConfigured = item.ConfigurationState.IsConfigured; var isConfigured = item.ConfigurationState.IsConfigured;
var dependencyHint = this.GetDependencyHint(item.Definition.Id); var dependencyHint = this.GetDependencyHint(item.Definition.Id);
var providerConfidenceHint = this.GetProviderConfidenceHint(item);
var isBlockedByProviderConfidence = this.IsBlockedByProviderConfidence(item);
<MudPaper Class="pa-2 mb-2 border rounded-lg"> <MudPaper Class="pa-2 mb-2 border rounded-lg">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween"> <MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudSwitch T="bool" Color="Color.Primary" Value="@isSelected" ValueChanged="@(value => this.ChangeSelection(item.Definition.Id, value))" Disabled="@(!isConfigured || this.Disabled || !this.SupportsTools || this.IsSelectionLockedByDependency(item.Definition.Id))" /> <MudSwitch T="bool" Color="Color.Primary" Value="@isSelected" ValueChanged="@(value => this.ChangeSelection(item.Definition.Id, value))" Disabled="@(!isConfigured || isBlockedByProviderConfidence || this.Disabled || !this.SupportsTools || this.IsSelectionLockedByDependency(item.Definition.Id))" />
<MudIcon Icon="@item.Implementation.Icon" Color="Color.Info" /> <MudIcon Icon="@item.Implementation.Icon" Color="Color.Info" />
<MudTooltip Text="@item.Implementation.GetDescription()"> <MudTooltip Text="@item.Implementation.GetDescription()">
<MudText Typo="Typo.body1">@item.Implementation.GetDisplayName()</MudText> <MudText Typo="Typo.body1">@item.Implementation.GetDisplayName()</MudText>
@ -59,6 +61,10 @@
{ {
<MudText Typo="Typo.caption" Color="Color.Info">@dependencyHint</MudText> <MudText Typo="Typo.caption" Color="Color.Info">@dependencyHint</MudText>
} }
@if (!string.IsNullOrWhiteSpace(providerConfidenceHint))
{
<MudText Typo="Typo.caption" Color="Color.Warning">@providerConfidenceHint</MudText>
}
</MudPaper> </MudPaper>
} }
} }

View File

@ -43,11 +43,21 @@ public partial class ToolSelection : MSGComponentBase
base.OnParametersSet(); base.OnParametersSet();
} }
protected override async Task OnInitializedAsync()
{
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
await base.OnInitializedAsync();
}
private bool SupportsTools => private bool SupportsTools =>
this.LLMProvider != AIStudio.Settings.Provider.NONE && this.LLMProvider != AIStudio.Settings.Provider.NONE &&
this.LLMProvider.GetModelCapabilities().Contains(Capability.CHAT_COMPLETION_API) && this.LLMProvider.GetModelCapabilities().Contains(Capability.CHAT_COMPLETION_API) &&
this.LLMProvider.GetModelCapabilities().Contains(Capability.FUNCTION_CALLING); this.LLMProvider.GetModelCapabilities().Contains(Capability.FUNCTION_CALLING);
private ConfidenceLevel ProviderConfidence => this.LLMProvider == AIStudio.Settings.Provider.NONE
? ConfidenceLevel.NONE
: this.LLMProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level;
private async Task ToggleSelection() private async Task ToggleSelection()
{ {
this.showSelection = !this.showSelection; this.showSelection = !this.showSelection;
@ -72,6 +82,10 @@ public partial class ToolSelection : MSGComponentBase
private bool IsSelectionLockedByDependency(string toolId) => ToolSelectionRules.IsRequiredBySelectedTools(toolId, this.SelectedToolIds); private bool IsSelectionLockedByDependency(string toolId) => ToolSelectionRules.IsRequiredBySelectedTools(toolId, this.SelectedToolIds);
private ConfidenceLevel GetMinimumProviderConfidence(ToolCatalogItem item) => this.SettingsManager.GetMinimumProviderConfidenceForTool(item.Definition.Id);
private bool IsBlockedByProviderConfidence(ToolCatalogItem item) => !ToolSelectionRules.IsProviderConfidenceAllowed(this.ProviderConfidence, this.GetMinimumProviderConfidence(item));
private string? GetDependencyHint(string toolId) private string? GetDependencyHint(string toolId)
{ {
if (toolId == ToolSelectionRules.WEB_SEARCH_TOOL_ID) if (toolId == ToolSelectionRules.WEB_SEARCH_TOOL_ID)
@ -83,6 +97,17 @@ public partial class ToolSelection : MSGComponentBase
return null; return null;
} }
private string? GetProviderConfidenceHint(ToolCatalogItem item)
{
if (!this.IsBlockedByProviderConfidence(item))
return null;
return string.Format(
this.T("This tool requires provider confidence {0}. The selected provider has {1}."),
this.GetMinimumProviderConfidence(item).GetName(),
this.ProviderConfidence.GetName());
}
private async Task OpenSettings(string toolId) private async Task OpenSettings(string toolId)
{ {
var parameters = new DialogParameters<ToolSettingsDialog> var parameters = new DialogParameters<ToolSettingsDialog>
@ -95,4 +120,15 @@ public partial class ToolSelection : MSGComponentBase
this.catalog = await this.ToolRegistry.GetCatalogAsync(this.Component); this.catalog = await this.ToolRegistry.GetCatalogAsync(this.Component);
this.StateHasChanged(); this.StateHasChanged();
} }
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
switch (triggeredEvent)
{
case Event.CONFIGURATION_CHANGED when this.showSelection:
this.catalog = await this.ToolRegistry.GetCatalogAsync(this.Component);
await this.InvokeAsync(this.StateHasChanged);
break;
}
}
} }

View File

@ -5,6 +5,7 @@ using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.ToolCallingSystem;
namespace AIStudio.Provider.OpenAI; namespace AIStudio.Provider.OpenAI;
@ -78,11 +79,12 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
// //
// Prepare the tools we want to use: // Prepare the tools we want to use:
// //
IList<ProviderTool> providerTools = modelCapabilities.Contains(Capability.WEB_SEARCH) switch var providerConfidence = this.Provider.GetConfidence(settingsManager).Level;
{ var minimumWebSearchConfidence = settingsManager.GetMinimumProviderConfidenceForTool(ToolSelectionRules.WEB_SEARCH_TOOL_ID);
true => [ ProviderTools.WEB_SEARCH ], var isWebSearchAllowed = ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumWebSearchConfidence);
_ => [] IList<ProviderTool> providerTools = modelCapabilities.Contains(Capability.WEB_SEARCH) && isWebSearchAllowed
}; ? [ ProviderTools.WEB_SEARCH ]
: [];
// Parse the API parameters: // Parse the API parameters:

View File

@ -1,6 +1,7 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
namespace AIStudio.Tools.ToolCallingSystem; namespace AIStudio.Tools.ToolCallingSystem;
@ -90,6 +91,8 @@ public sealed class ToolCatalogItem
public required IToolImplementation Implementation { get; init; } public required IToolImplementation Implementation { get; init; }
public required ToolConfigurationState ConfigurationState { get; init; } public required ToolConfigurationState ConfigurationState { get; init; }
public ConfidenceLevel MinimumProviderConfidence { get; init; } = ConfidenceLevel.NONE;
} }
public sealed class ToolSelectionState public sealed class ToolSelectionState

View File

@ -1,6 +1,7 @@
using System.Text.Json; using System.Text.Json;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@ -9,6 +10,7 @@ namespace AIStudio.Tools.ToolCallingSystem;
public sealed class ToolRegistry public sealed class ToolRegistry
{ {
private readonly ILogger<ToolRegistry> logger; private readonly ILogger<ToolRegistry> logger;
private readonly SettingsManager settingsManager;
private readonly ToolSettingsService toolSettingsService; private readonly ToolSettingsService toolSettingsService;
private readonly Dictionary<string, ToolDefinition> definitionsById = new(StringComparer.Ordinal); private readonly Dictionary<string, ToolDefinition> definitionsById = new(StringComparer.Ordinal);
private readonly Dictionary<string, IToolImplementation> implementationsByKey = new(StringComparer.Ordinal); private readonly Dictionary<string, IToolImplementation> implementationsByKey = new(StringComparer.Ordinal);
@ -16,10 +18,12 @@ public sealed class ToolRegistry
public ToolRegistry( public ToolRegistry(
IWebHostEnvironment webHostEnvironment, IWebHostEnvironment webHostEnvironment,
IEnumerable<IToolImplementation> implementations, IEnumerable<IToolImplementation> implementations,
SettingsManager settingsManager,
ToolSettingsService toolSettingsService, ToolSettingsService toolSettingsService,
ILogger<ToolRegistry> logger) ILogger<ToolRegistry> logger)
{ {
this.logger = logger; this.logger = logger;
this.settingsManager = settingsManager;
this.toolSettingsService = toolSettingsService; this.toolSettingsService = toolSettingsService;
foreach (var implementation in implementations) foreach (var implementation in implementations)
@ -101,6 +105,7 @@ public sealed class ToolRegistry
Definition = definition, Definition = definition,
Implementation = implementation, Implementation = implementation,
ConfigurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition, implementation), ConfigurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition, implementation),
MinimumProviderConfidence = this.settingsManager.GetMinimumProviderConfidenceForTool(definition.Id),
}); });
} }
@ -111,6 +116,7 @@ public sealed class ToolRegistry
AIStudio.Tools.Components component, AIStudio.Tools.Components component,
IEnumerable<string> selectedToolIds, IEnumerable<string> selectedToolIds,
IReadOnlyCollection<Capability> modelCapabilities, IReadOnlyCollection<Capability> modelCapabilities,
ConfidenceLevel providerConfidence,
bool isToolSelectionVisible) bool isToolSelectionVisible)
{ {
if (!isToolSelectionVisible) if (!isToolSelectionVisible)
@ -131,6 +137,10 @@ public sealed class ToolRegistry
if (!configurationState.IsConfigured) if (!configurationState.IsConfigured)
continue; continue;
var minimumToolConfidence = this.settingsManager.GetMinimumProviderConfidenceForTool(definition.Id);
if (!ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumToolConfidence))
continue;
result.Add((definition, implementation)); result.Add((definition, implementation));
} }

View File

@ -1,6 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AIStudio.Provider;
namespace AIStudio.Tools.ToolCallingSystem; namespace AIStudio.Tools.ToolCallingSystem;
public static class ToolSelectionRules public static class ToolSelectionRules
@ -22,4 +24,14 @@ public static class ToolSelectionRules
var normalized = NormalizeSelection(selectedToolIds); var normalized = NormalizeSelection(selectedToolIds);
return toolId == READ_WEB_PAGE_TOOL_ID && normalized.Contains(WEB_SEARCH_TOOL_ID); return toolId == READ_WEB_PAGE_TOOL_ID && normalized.Contains(WEB_SEARCH_TOOL_ID);
} }
public static ConfidenceLevel GetDefaultMinimumProviderConfidence(string toolId) => toolId switch
{
WEB_SEARCH_TOOL_ID => ConfidenceLevel.MEDIUM,
READ_WEB_PAGE_TOOL_ID => ConfidenceLevel.MEDIUM,
_ => ConfidenceLevel.NONE,
};
public static bool IsProviderConfidenceAllowed(ConfidenceLevel providerConfidence, ConfidenceLevel minimumToolConfidence) =>
minimumToolConfidence is ConfidenceLevel.NONE || providerConfidence >= minimumToolConfidence;
} }

View File

@ -7,6 +7,7 @@
- Added a start-page setting, so AI Studio can now open directly on your preferred page when the app starts. Configuration plugins can also provide and optionally lock this default for organizations. - Added a start-page setting, so AI Studio can now open directly on your preferred page when the app starts. Configuration plugins can also provide and optionally lock this default for organizations.
- Added math rendering in chats for LaTeX display formulas, including block formats such as `$$ ... $$` and `\[ ... \]`. - Added math rendering in chats for LaTeX display formulas, including block formats such as `$$ ... $$` and `\[ ... \]`.
- Added the latest OpenAI models. - Added the latest OpenAI models.
- Added minimum provider confidence settings for tools, so sensitive tools such as web search can be limited to trusted providers. Configuration plugins can also manage these defaults for organizations.
- Released the document analysis assistant after an intense testing phase. - Released the document analysis assistant after an intense testing phase.
- Improved enterprise deployment for organizations: administrators can now provide up to 10 centrally managed enterprise configuration slots, use policy files on Linux and macOS, and continue using older configuration formats as a fallback during migration. - Improved enterprise deployment for organizations: administrators can now provide up to 10 centrally managed enterprise configuration slots, use policy files on Linux and macOS, and continue using older configuration formats as a fallback during migration.
- Improved the profile selection for assistants and the chat. You can now explicitly choose between the app default profile, no profile, or a specific profile. - Improved the profile selection for assistants and the chat. You can now explicitly choose between the app default profile, no profile, or a specific profile.