mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 21:07:21 +00:00
Improved the tool selection by showing unavailable tools on request (#1116)
This commit is contained in:
1 parent
04546c25ac
commit
1a23479f33
9 files changed
+369
-62
No files matched your search
@@ -1,3 +1,4 @@
|
||||
@using AIStudio.Tools.ToolCallingSystem
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<div class="d-flex">
|
||||
@@ -36,61 +37,49 @@
|
||||
|
||||
@if (this.SupportsTools && this.catalog.Count > 0)
|
||||
{
|
||||
@*
|
||||
The striping sits on this wrapper: the rows share their parent with the
|
||||
introduction and the occasional alert, which would shift the parity.
|
||||
*@
|
||||
<div class="tool-selection-rows">
|
||||
@foreach (var item in this.catalog)
|
||||
{
|
||||
var isSelected = this.SelectedToolIds.Contains(item.Id);
|
||||
var warningText = this.GetWarningText(item);
|
||||
<div class="tool-selection-row">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="1">
|
||||
@*
|
||||
Everything but the settings button switches the tool, so aiming for the
|
||||
small switch is optional. The button spans that part of the row, which
|
||||
keeps the settings button outside of it without any event plumbing.
|
||||
*@
|
||||
<MudButton Variant="Variant.Text" Color="Color.Default" Class="px-1 py-0 justify-start"
|
||||
Style="min-width:auto; text-transform:none; flex-grow:1;"
|
||||
Disabled="@this.IsRowDisabled(item)" OnClick="@(async () => await this.ToggleToolFromRow(item))">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
@*
|
||||
A checkbox rather than a switch, because this row is one entry of a set the
|
||||
user picks from, not a setting of its own -- the same question the data source
|
||||
selection next to it asks, and it should not look like a different one.
|
||||
var itemsShownFirst = this.GetItemsShownFirst();
|
||||
var unavailableItems = this.GetUnavailableItems();
|
||||
|
||||
The checkbox only shows the state; the surrounding button does the switching.
|
||||
It therefore takes no pointer events at all: its label reaches past the visible
|
||||
box and would otherwise swallow the clicks landing in that strip.
|
||||
*@
|
||||
<MudCheckBox T="bool" Size="Size.Small" Dense="@true" Color="Color.Primary" Value="@isSelected" ReadOnly="@true" Disabled="@this.IsRowDisabled(item)" Style="pointer-events: none;" />
|
||||
<MudIcon Icon="@item.Icon" Color="Color.Info" />
|
||||
@if (!item.IsActive)
|
||||
{
|
||||
<MudTooltip Text="@T("This tool has been disabled by your organization.")">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Error" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudTooltip Text="@item.Description">
|
||||
<MudText Typo="Typo.body1">@item.DisplayName</MudText>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</MudButton>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Settings" Size="Size.Small" OnClick="@(async () => await this.OpenSettings(item))" />
|
||||
</MudStack>
|
||||
@if (!string.IsNullOrWhiteSpace(warningText))
|
||||
@*
|
||||
Each list has a wrapper of its own, which gives its rows their compact padding:
|
||||
the rows share their parent with nothing else, neither the introduction nor the
|
||||
button between both lists.
|
||||
*@
|
||||
@if (itemsShownFirst.Count > 0)
|
||||
{
|
||||
<div class="tool-selection-rows">
|
||||
@foreach (var item in itemsShownFirst)
|
||||
{
|
||||
@this.RenderRow(item)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body1">@T("No tool can be selected right now.")</MudText>
|
||||
}
|
||||
|
||||
@if (unavailableItems.Count > 0)
|
||||
{
|
||||
<MudButton StartIcon="@(this.showUnavailableTools ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
|
||||
Size="Size.Small"
|
||||
Variant="Variant.Text"
|
||||
Class="mt-1"
|
||||
OnClick="@this.ToggleUnavailableTools">
|
||||
@this.GetUnavailableToolsButtonText(unavailableItems)
|
||||
</MudButton>
|
||||
<MudCollapse Expanded="@this.showUnavailableTools">
|
||||
<div class="tool-selection-rows">
|
||||
@foreach (var item in unavailableItems)
|
||||
{
|
||||
@*
|
||||
A caption renders as an inline span by default, and an inline element
|
||||
cannot be justified. The div makes it a block of its own.
|
||||
*@
|
||||
<MudJustifiedText Typo="Typo.caption" HtmlTag="div" Color="Color.Warning" Class="ml-2">@warningText</MudJustifiedText>
|
||||
@this.RenderRow(item)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<MudJustifiedText Typo="Typo.caption" HtmlTag="div" Class="mt-2 ml-2">
|
||||
@T("You can switch off tools you never need in the app settings, under Tool Settings. They then no longer appear here.")
|
||||
</MudJustifiedText>
|
||||
</MudCollapse>
|
||||
}
|
||||
}
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
@@ -99,4 +88,58 @@
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</MudPopover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// One row of the selection, in either list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Both lists show the same row, so it is written once. It stays in this component, which keeps
|
||||
/// the translations of its texts: a component of its own would give each of them a new key.
|
||||
/// </remarks>
|
||||
private RenderFragment RenderRow(ToolCatalogItem item) => @<div class="tool-selection-row">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="1">
|
||||
@*
|
||||
Everything but the settings button switches the tool, so aiming for the
|
||||
small switch is optional. The button spans that part of the row, which
|
||||
keeps the settings button outside of it without any event plumbing.
|
||||
*@
|
||||
<MudButton Variant="Variant.Text" Color="Color.Default" Class="px-1 py-0 justify-start"
|
||||
Style="min-width:auto; text-transform:none; flex-grow:1;"
|
||||
Disabled="@this.IsRowDisabled(item)" OnClick="@(async () => await this.ToggleToolFromRow(item))">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
@*
|
||||
A checkbox rather than a switch, because this row is one entry of a set the
|
||||
user picks from, not a setting of its own -- the same question the data source
|
||||
selection next to it asks, and it should not look like a different one.
|
||||
|
||||
The checkbox only shows the state; the surrounding button does the switching.
|
||||
It therefore takes no pointer events at all: its label reaches past the visible
|
||||
box and would otherwise swallow the clicks landing in that strip.
|
||||
*@
|
||||
<MudCheckBox T="bool" Size="Size.Small" Dense="@true" Color="Color.Primary" Value="@this.SelectedToolIds.Contains(item.Id)" ReadOnly="@true" Disabled="@this.IsRowDisabled(item)" Style="pointer-events: none;" />
|
||||
<MudIcon Icon="@item.Icon" Color="Color.Info" />
|
||||
@if (!item.IsActive)
|
||||
{
|
||||
<MudTooltip Text="@T("This tool has been disabled by your organization.")">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Error" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudTooltip Text="@item.Description">
|
||||
<MudText Typo="Typo.body1">@item.DisplayName</MudText>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</MudButton>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Settings" Size="Size.Small" OnClick="@(async () => await this.OpenSettings(item))" />
|
||||
</MudStack>
|
||||
@if (this.GetWarningText(item) is var warningText && !string.IsNullOrWhiteSpace(warningText))
|
||||
{
|
||||
@*
|
||||
A caption renders as an inline span by default, and an inline element
|
||||
cannot be justified. The div makes it a block of its own.
|
||||
*@
|
||||
<MudJustifiedText Typo="Typo.caption" HtmlTag="div" Color="Color.Warning" Class="ml-2">@warningText</MudJustifiedText>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
@@ -45,6 +45,7 @@ public partial class ToolSelection : MSGComponentBase
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
private bool showSelection;
|
||||
private bool showUnavailableTools;
|
||||
private IReadOnlyList<ToolCatalogItem> catalog = [];
|
||||
|
||||
protected override void OnParametersSet()
|
||||
@@ -77,22 +78,72 @@ public partial class ToolSelection : MSGComponentBase
|
||||
{
|
||||
this.showSelection = !this.showSelection;
|
||||
if (this.showSelection)
|
||||
{
|
||||
this.showUnavailableTools = false;
|
||||
this.catalog = await this.ToolRegistry.GetCatalogAsync(this.Component);
|
||||
}
|
||||
}
|
||||
|
||||
private void Hide() => this.showSelection = false;
|
||||
|
||||
private void ToggleUnavailableTools() => this.showUnavailableTools = !this.showUnavailableTools;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the selection keeps the tools which cannot be selected right now apart, behind a button.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not while a response is running: no tool can be switched then, and the list shows the current
|
||||
/// selection as a whole, as the note above it says.
|
||||
/// </remarks>
|
||||
private bool KeepsUnavailableToolsApart => !this.Disabled;
|
||||
|
||||
/// <summary>
|
||||
/// The tools the selection lists first: those the user can select right now, or all of them while a response is running.
|
||||
/// </summary>
|
||||
private List<ToolCatalogItem> GetItemsShownFirst() => this.KeepsUnavailableToolsApart
|
||||
? [..this.catalog.Where(this.IsSelectable)]
|
||||
: [..this.catalog];
|
||||
|
||||
/// <summary>
|
||||
/// The tools which cannot be selected right now, selected ones included, which the selection lists behind a button.
|
||||
/// </summary>
|
||||
private List<ToolCatalogItem> GetUnavailableItems() => this.KeepsUnavailableToolsApart
|
||||
? [..this.catalog.Where(item => !this.IsSelectable(item))]
|
||||
: [];
|
||||
|
||||
/// <summary>
|
||||
/// The text of the button which shows or hides the tools that cannot be selected right now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A selected tool may end up behind the button, for example, when the user switches to a provider
|
||||
/// with less confidence. The text then says so, because the selection would otherwise seem to have
|
||||
/// lost it.
|
||||
/// </remarks>
|
||||
private string GetUnavailableToolsButtonText(IReadOnlyCollection<ToolCatalogItem> unavailableItems)
|
||||
{
|
||||
if (this.showUnavailableTools)
|
||||
return T("Hide unavailable tools");
|
||||
|
||||
var selectedCount = unavailableItems.Count(item => this.SelectedToolIds.Contains(item.Id));
|
||||
if (unavailableItems.Count == 1)
|
||||
return selectedCount == 0 ? T("Show 1 unavailable tool") : T("Show 1 unavailable tool, which is selected");
|
||||
|
||||
return selectedCount == 0
|
||||
? string.Format(T("Show {0} unavailable tools"), unavailableItems.Count.CompactCount())
|
||||
: string.Format(T("Show {0} unavailable tools, {1} of them selected"), unavailableItems.Count.CompactCount(), selectedCount.CompactCount());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this tool can be switched at all right now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The switch and the row click share this, so both agree on when a tool is out of reach: the
|
||||
/// organization disabled it, it is not configured, the provider lacks the confidence it needs,
|
||||
/// the chat read a mailbox which keeps the tool back, a response is running, or the model
|
||||
/// cannot call tools in the first place.
|
||||
/// tool itself cannot be selected right now, see ToolSelectionRules.IsSelectable, a response is
|
||||
/// running, or the model cannot call tools in the first place.
|
||||
/// </remarks>
|
||||
private bool IsRowDisabled(ToolCatalogItem item) => !item.IsActive || !item.ConfigurationState.IsConfigured || this.IsBlockedByProviderConfidence(item) ||
|
||||
this.IsBlockedByOutboundDataRestriction(item) || this.Disabled || !this.SupportsTools;
|
||||
private bool IsRowDisabled(ToolCatalogItem item) => !this.IsSelectable(item) || this.Disabled || !this.SupportsTools;
|
||||
|
||||
private bool IsSelectable(ToolCatalogItem item) => ToolSelectionRules.IsSelectable(item, this.ProviderConfidence, this.RequiredOutboundDataRestriction.Restriction);
|
||||
|
||||
/// <summary>
|
||||
/// Switches a tool when the user clicks anywhere in its row.
|
||||
@@ -134,11 +185,7 @@ public partial class ToolSelection : MSGComponentBase
|
||||
/// <summary>
|
||||
/// Whether the mailboxes the chat read from keep every tool of this entry back.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only then is there nothing left to select. Should a mailbox keep back some tools of a
|
||||
/// collection, the others still run, and the request leaves out the rest on its own.
|
||||
/// </remarks>
|
||||
private bool IsBlockedByOutboundDataRestriction(ToolCatalogItem item) => item.Tools.All(tool => !ToolSelectionRules.IsOutboundDataAllowed(this.RequiredOutboundDataRestriction.Restriction, tool.Implementation));
|
||||
private bool IsBlockedByOutboundDataRestriction(ToolCatalogItem item) => !ToolSelectionRules.IsOutboundDataAllowed(this.RequiredOutboundDataRestriction.Restriction, item);
|
||||
|
||||
private string? GetProviderConfidenceHint(ToolCatalogItem item)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user