mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 19:13:48 +00:00
Added tool calling support (#731)
Co-authored-by: krut_ni <nils.kruthoff@dlr.de> Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
266 files changed
+11186
-740
No files matched your search
@@ -33,18 +33,27 @@
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled" Color="@state.AuditColor">
|
||||
@state.AuditLabel
|
||||
</MudChip>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(state.SourceLabel))
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled" Color="@state.SourceColor" Icon="@state.SourceIcon">
|
||||
@state.SourceLabel
|
||||
</MudChip>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(state.AvailabilityLabel))
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="@state.AvailabilityColor" Icon="@state.AvailabilityIcon">
|
||||
@state.AvailabilityLabel
|
||||
</MudChip>
|
||||
}
|
||||
|
||||
@if (this.PluginToolIds.Count > 0)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Info" Icon="@Icons.Material.Filled.Handyman">
|
||||
@this.GetToolCountLabel()
|
||||
</MudChip>
|
||||
}
|
||||
</div>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||
@state.Headline
|
||||
@@ -135,6 +144,15 @@
|
||||
</td>
|
||||
<td><MudText Typo="Typo.body2">@state.SourceLabel</MudText></td>
|
||||
</tr>
|
||||
@if (this.PluginToolIds.Count > 0)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<MudText Typo="Typo.body2"><b>@T("Tools")</b></MudText>
|
||||
</td>
|
||||
<td><code style="font-size: 0.8rem;">@string.Join(", ", this.PluginToolIds)</code></td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<td>
|
||||
<MudText Typo="Typo.body2"><b>@T("Current hash")</b></MudText>
|
||||
|
||||
@@ -21,6 +21,17 @@ public partial class AssistantPluginSecurityCard : MSGComponentBase
|
||||
? new PluginAssistantSecurityState()
|
||||
: PluginAssistantSecurityResolver.Resolve(this.SettingsManager, this.Plugin);
|
||||
|
||||
/// <summary>
|
||||
/// The tools this plugin runs with, either in its assistant or in the chat it launches.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Tools are a capability, not a detail: an assistant allowed to search the web or read a page
|
||||
/// can carry what a user typed out of the app. Whoever decides whether to enable this plugin
|
||||
/// should see that beforehand, which is why the count sits in the header next to the audit
|
||||
/// level and the tools themselves are named in the details.
|
||||
/// </remarks>
|
||||
private IReadOnlyList<string> PluginToolIds => this.Plugin?.AssistantToolIds ?? this.Plugin?.ChatLaunchConfiguration?.ToolIds ?? [];
|
||||
|
||||
private CultureInfo currentCultureInfo = CultureInfo.InvariantCulture;
|
||||
private bool showSecurityCard;
|
||||
private bool showDetails;
|
||||
@@ -126,6 +137,10 @@ public partial class AssistantPluginSecurityCard : MSGComponentBase
|
||||
: this.FormatFileTimestamp(auditedAt.Value.ToLocalTime().DateTime);
|
||||
}
|
||||
|
||||
private string GetToolCountLabel() => this.PluginToolIds.Count is 1
|
||||
? this.T("Uses 1 tool")
|
||||
: string.Format(this.T("Uses {0} tools"), this.PluginToolIds.Count);
|
||||
|
||||
private string GetAuditProviderLabel()
|
||||
{
|
||||
var providerName = this.SecurityState.Audit?.AuditProviderName;
|
||||
|
||||
@@ -124,6 +124,11 @@
|
||||
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
|
||||
|
||||
<ProfileSelection MarginLeft="" CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" Disabled="@(!this.currentChatTemplate.AllowProfileUsage)" DisabledText="@T("Profile usage is disabled according to your chat template settings.")"/>
|
||||
|
||||
@if (this.SettingsManager.AreToolsEnabled())
|
||||
{
|
||||
<ToolSelection Component="Components.CHAT" LLMProvider="@this.Provider" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.IsCurrentChatStreaming" />
|
||||
}
|
||||
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using AIStudio.Dialogs;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
using AIStudio.Tools.Media;
|
||||
using AIStudio.Tools.Services;
|
||||
@@ -48,6 +49,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
[Inject]
|
||||
private ILogger<ChatComponent> Logger { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
@@ -76,6 +80,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
private bool mustLoadChat;
|
||||
private LoadChat loadChat;
|
||||
private bool autoSaveEnabled;
|
||||
private HashSet<string> selectedToolIds = [];
|
||||
private bool previousInputForbidden = true;
|
||||
private Guid lastSeenChatId = Guid.Empty;
|
||||
private AIStudio.Settings.Provider lastSeenProvider = AIStudio.Settings.Provider.NONE;
|
||||
@@ -113,7 +118,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
|
||||
|
||||
|
||||
// Apply the filters for the message bus:
|
||||
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED, Event.WORKSPACE_RENAMED, Event.CONFIGURATION_CHANGED ]);
|
||||
|
||||
@@ -128,6 +133,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
|
||||
if (!this.ComposerState.HasUserDraft && !this.ComposerState.HasComposerContent)
|
||||
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
|
||||
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT));
|
||||
|
||||
this.lastAppliedStandardDataSourceOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy();
|
||||
|
||||
@@ -149,6 +155,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
// Use chat thread sent by the user:
|
||||
this.ChatThread = deferredRequest.ChatThread;
|
||||
this.ChatThread.IncludeDateTime = true;
|
||||
this.ApplyToolSelectionOfLoadedChat();
|
||||
|
||||
//
|
||||
// Apply the chat template of the incoming chat to the composer. Like everywhere else,
|
||||
@@ -332,6 +339,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
this.Logger.LogInformation($"The chat '{this.ChatThread!.ChatId}' with title '{this.ChatThread.Name}' ({this.ChatThread.Blocks.Count} messages) was loaded successfully.");
|
||||
|
||||
this.ApplyToolSelectionOfLoadedChat();
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
await this.SelectProviderWhenLoadingChat();
|
||||
}
|
||||
@@ -617,9 +625,8 @@ public partial class ChatComponent : MSGComponentBase
|
||||
{
|
||||
var previousProvider = this.Provider;
|
||||
var previousChatTemplate = this.currentChatTemplate;
|
||||
var chatProviderId = this.ChatThread?.SelectedProvider;
|
||||
|
||||
this.Provider = this.SettingsManager.GetChatProviderForLoadedChat(chatProviderId);
|
||||
this.Provider = this.SettingsManager.GetChatProviderForLoadedChat(this.Provider.Id);
|
||||
if (this.Provider != previousProvider)
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
|
||||
@@ -756,6 +763,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
SelectedProvider = this.Provider.Id,
|
||||
SelectedProfile = this.currentProfile.Id,
|
||||
SelectedChatTemplate = this.currentChatTemplate.Id,
|
||||
SelectedToolIds = [..this.selectedToolIds],
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
WorkspaceId = this.currentWorkspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
@@ -778,6 +786,8 @@ public partial class ChatComponent : MSGComponentBase
|
||||
if (this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner))
|
||||
return;
|
||||
|
||||
await this.RefreshProviderSelectionFromConfigurationAsync();
|
||||
|
||||
if (!this.IsProviderSelected)
|
||||
return;
|
||||
|
||||
@@ -798,6 +808,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
SelectedProvider = this.Provider.Id,
|
||||
SelectedProfile = this.currentProfile.Id,
|
||||
SelectedChatTemplate = this.currentChatTemplate.Id,
|
||||
SelectedToolIds = [..this.selectedToolIds],
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
WorkspaceId = this.currentWorkspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
@@ -899,15 +910,18 @@ public partial class ChatComponent : MSGComponentBase
|
||||
}
|
||||
|
||||
this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'.");
|
||||
this.StateHasChanged();
|
||||
this.ChatThread!.RuntimeComponent = Tools.Components.CHAT;
|
||||
this.ChatThread.SelectedToolIds = [..this.selectedToolIds];
|
||||
this.ChatThread.RuntimeSelectedToolIds = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds);
|
||||
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
|
||||
{
|
||||
ChatThread = this.ChatThread!,
|
||||
ChatThread = this.ChatThread,
|
||||
AIText = aiText,
|
||||
LastUserPrompt = lastUserPrompt,
|
||||
ProviderSettings = this.Provider,
|
||||
IsForeground = true,
|
||||
});
|
||||
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.StateHasChanged();
|
||||
}
|
||||
@@ -917,6 +931,37 @@ public partial class ChatComponent : MSGComponentBase
|
||||
if (this.ChatThread is not null)
|
||||
await this.AIJobService.CancelChatGenerationAsync(this.ChatThread.ChatId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes over the tool selection of the chat that was just loaded or handed to this component.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A thread without a selection means the chat defaults: that is a chat saved before tools
|
||||
/// existed, as well as one a launcher opened without naming any. Both want what the settings
|
||||
/// preselect. Every path that puts a thread into this component has to come through here, or
|
||||
/// the footer would keep showing the tools of the chat before it.
|
||||
/// </remarks>
|
||||
private void ApplyToolSelectionOfLoadedChat() =>
|
||||
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(this.ChatThread?.SelectedToolIds ?? this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT));
|
||||
|
||||
private Task SelectedToolIdsChanged(HashSet<string> updatedToolIds)
|
||||
{
|
||||
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(updatedToolIds);
|
||||
|
||||
//
|
||||
// The thread keeps the selection so that reopening the chat tomorrow brings the same tools
|
||||
// back. What is stored is what the user chose, not what the current provider is allowed to
|
||||
// run: filtering here would quietly drop a tool for good the moment the user switches to a
|
||||
// provider with less confidence.
|
||||
//
|
||||
if (this.ChatThread is not null)
|
||||
{
|
||||
this.ChatThread.SelectedToolIds = [..this.selectedToolIds];
|
||||
this.hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task SaveThread()
|
||||
{
|
||||
@@ -980,6 +1025,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
//
|
||||
this.hasUnsavedChanges = false;
|
||||
this.ComposerState.Clear();
|
||||
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT));
|
||||
this.RefreshCurrentProfileAndChatTemplate();
|
||||
|
||||
//
|
||||
@@ -1029,6 +1075,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
SelectedProvider = this.Provider.Id,
|
||||
SelectedProfile = this.currentProfile.Id,
|
||||
SelectedChatTemplate = this.currentChatTemplate.Id,
|
||||
SelectedToolIds = [..this.selectedToolIds],
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
WorkspaceId = this.currentWorkspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
@@ -1104,6 +1151,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
|
||||
this.ApplyToolSelectionOfLoadedChat();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1123,6 +1171,19 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task RefreshProviderSelectionFromConfigurationAsync()
|
||||
{
|
||||
var updatedProvider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT, this.Provider.Id);
|
||||
var providerChanged = updatedProvider != this.Provider;
|
||||
if (providerChanged)
|
||||
this.Provider = updatedProvider;
|
||||
|
||||
if (!providerChanged)
|
||||
return;
|
||||
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
}
|
||||
|
||||
private async Task ResetState()
|
||||
{
|
||||
@@ -1275,6 +1336,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
this.StateHasChanged();
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1312,4 +1374,4 @@ public partial class ChatComponent : MSGComponentBase
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,26 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
|
||||
[Parameter]
|
||||
public Action<HashSet<TData>> SelectionUpdate { get; set; } = _ => { };
|
||||
|
||||
/// <summary>
|
||||
/// An asynchronous action that is called when the selection changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<HashSet<TData>, Task> SelectionUpdateAsync { get; set; } = _ => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a specific item is locked by a configuration plugin.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<TData, bool> IsItemLocked { get; set; } = _ => false;
|
||||
|
||||
[Parameter]
|
||||
public string? EmptySelectionText { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? SingleSelectionText { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? MultipleSelectionText { get; set; }
|
||||
|
||||
#region Overrides of ConfigurationBase
|
||||
|
||||
@@ -49,11 +64,12 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
|
||||
|
||||
private async Task OptionChanged(IEnumerable<TData?>? updatedValues)
|
||||
{
|
||||
if(updatedValues is null)
|
||||
this.SelectionUpdate([]);
|
||||
else
|
||||
this.SelectionUpdate(updatedValues.Where(n => n is not null).ToHashSet()!);
|
||||
|
||||
// OfType drops the nulls and gives back the non-nullable element type in one step, which
|
||||
// Where cannot: it keeps the nullable type no matter what the predicate proves.
|
||||
var selection = updatedValues is null ? [] : updatedValues.OfType<TData>().ToHashSet();
|
||||
this.SelectionUpdate(selection);
|
||||
await this.SelectionUpdateAsync(selection);
|
||||
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.InformAboutChange();
|
||||
}
|
||||
@@ -61,12 +77,12 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
|
||||
private string GetMultiSelectionText(List<TData?>? selectedValues)
|
||||
{
|
||||
if(selectedValues is null || selectedValues.Count == 0)
|
||||
return T("No preview features selected.");
|
||||
return this.EmptySelectionText ?? T("No items selected.");
|
||||
|
||||
if(selectedValues.Count == 1)
|
||||
return T("You have selected 1 preview feature.");
|
||||
return this.SingleSelectionText ?? T("You have selected 1 item.");
|
||||
|
||||
return string.Format(T("You have selected {0} preview features."), selectedValues.Count);
|
||||
return string.Format(this.MultipleSelectionText ?? T("You have selected {0} items."), selectedValues.Count);
|
||||
}
|
||||
|
||||
private bool IsLockedValue(TData value) => this.IsItemLocked(value);
|
||||
@@ -76,4 +92,4 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
|
||||
"This feature is managed by your organization and has therefore been disabled.",
|
||||
typeof(ConfigurationBase).Namespace,
|
||||
nameof(ConfigurationBase));
|
||||
}
|
||||
}
|
||||
@@ -36,9 +36,11 @@
|
||||
<MudSelectItem T="string" Value="@chatTemplate.Id">@chatTemplate.GetSafeName()</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudSelect T="string" Label="@T("Data sources (Optional)")" MultiSelection="@true" SelectedValues="@this.DataSourceIds" SelectedValuesChanged="@this.SetDataSourceIds" MultiSelectionTextFunc="@this.GetSelectedDataSourceText" Variant="Variant.Outlined" Margin="Margin.Dense" Class="rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Source">
|
||||
<MudSelect T="string" Label="@T("Data sources (Optional)")" MultiSelection="@true" SelectedValues="@this.DataSourceIds" SelectedValuesChanged="@this.SetDataSourceIds" MultiSelectionTextFunc="@this.GetSelectedDataSourceText" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Source">
|
||||
@foreach (var dataSource in this.SettingsManager.ConfigurationData.DataSources)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@dataSource.Id">@dataSource.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudSelect>
|
||||
|
||||
<ToolSelectionField Component="Components.CHAT" SelectedToolIds="@this.ToolIds" SelectedToolIdsChanged="@this.SetToolIds" Label="@T("Tools (Optional)")" Help="@T("These tools are preselected when the chat opens. Users can change the selection in the chat, and every tool has to meet the confidence requirements of the provider in use.")"/>
|
||||
@@ -61,6 +61,19 @@ public partial class DirectChatLauncherForm : MSGComponentBase
|
||||
[Parameter]
|
||||
public EventCallback<IEnumerable<string>> DataSourceIdsChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tools preselected for the chat. An empty selection keeps the normal chat defaults.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A preselection, not a limit: the user can switch tools in the chat as usual. What a tool
|
||||
/// may actually do is decided there, by the confidence of the provider in use.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public HashSet<string> ToolIds { get; set; } = [];
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<HashSet<string>> ToolIdsChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates the workspace name. The hosts differ here: the Builder requires a name only while
|
||||
/// its launcher switch is on, whereas the settings dialog always requires one.
|
||||
@@ -135,6 +148,12 @@ public partial class DirectChatLauncherForm : MSGComponentBase
|
||||
await this.DataSourceIdsChanged.InvokeAsync(selectedDataSourceIds);
|
||||
}
|
||||
|
||||
private async Task SetToolIds(HashSet<string> toolIds)
|
||||
{
|
||||
this.ToolIds = toolIds;
|
||||
await this.ToolIdsChanged.InvokeAsync(toolIds);
|
||||
}
|
||||
|
||||
private string GetSelectedDataSourceText(List<string?>? selectedValues)
|
||||
{
|
||||
if (selectedValues is null || selectedValues.Count == 0)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<MudTreeView T="JsonTreeNode"
|
||||
Items="@this.items"
|
||||
ReadOnly="true"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
ExpandOnClick="true">
|
||||
<ItemTemplate Context="item">
|
||||
@if (item.Value is { } node)
|
||||
{
|
||||
<MudTreeViewItem T="JsonTreeNode"
|
||||
Icon="@node.Icon"
|
||||
Value="@item.Value"
|
||||
Expanded="@item.Expanded"
|
||||
CanExpand="@node.Expandable"
|
||||
Items="@item.Children">
|
||||
<BodyContent>
|
||||
<MudText Typo="Typo.body2" Style="font-family: monospace; white-space: pre-wrap; overflow-wrap: anywhere;">
|
||||
@node.Text
|
||||
</MudText>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
</ItemTemplate>
|
||||
</MudTreeView>
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class JsonTreeView : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public JsonNode? Value { get; set; }
|
||||
|
||||
private IReadOnlyCollection<TreeItemData<JsonTreeNode>> items = [];
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
this.items = [CreateTreeItem("$", this.Value)];
|
||||
}
|
||||
|
||||
private static TreeItemData<JsonTreeNode> CreateTreeItem(string label, JsonNode? value)
|
||||
{
|
||||
var children = CreateChildren(value);
|
||||
return new TreeItemData<JsonTreeNode>
|
||||
{
|
||||
Expanded = false,
|
||||
Expandable = children.Count > 0,
|
||||
Value = new JsonTreeNode
|
||||
{
|
||||
Text = $"{label}: {FormatValue(value)}",
|
||||
Icon = GetIcon(value),
|
||||
Expandable = children.Count > 0,
|
||||
},
|
||||
Children = children,
|
||||
};
|
||||
}
|
||||
|
||||
private static List<TreeItemData<JsonTreeNode>> CreateChildren(JsonNode? value) => value switch
|
||||
{
|
||||
JsonObject jsonObject => jsonObject
|
||||
.Select(property => CreateTreeItem(JsonSerializer.Serialize(property.Key), property.Value))
|
||||
.ToList(),
|
||||
JsonArray jsonArray => jsonArray
|
||||
.Select((item, index) => CreateTreeItem($"[{index}]", item))
|
||||
.ToList(),
|
||||
_ => [],
|
||||
};
|
||||
|
||||
private static string FormatValue(JsonNode? value) => value switch
|
||||
{
|
||||
JsonObject jsonObject when jsonObject.Count == 0 => "{}",
|
||||
JsonObject => "{...}",
|
||||
JsonArray jsonArray when jsonArray.Count == 0 => "[]",
|
||||
JsonArray => "[...]",
|
||||
null => "null",
|
||||
_ => value.ToJsonString(),
|
||||
};
|
||||
|
||||
private static string GetIcon(JsonNode? value) => value switch
|
||||
{
|
||||
JsonObject => Icons.Material.Filled.DataObject,
|
||||
JsonArray => Icons.Material.Filled.DataArray,
|
||||
_ => Icons.Material.Filled.Code,
|
||||
};
|
||||
|
||||
private sealed class JsonTreeNode
|
||||
{
|
||||
public string Text { get; init; } = string.Empty;
|
||||
|
||||
public string Icon { get; init; } = string.Empty;
|
||||
|
||||
public bool Expandable { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
@if (this.NeedsToolCallingProvider)
|
||||
{
|
||||
@* Nothing runs at all, so the other two would only add noise: *@
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="@true" Class="@this.Class">
|
||||
@T("Tools were selected for this run, but the chosen model cannot use tools. It runs without them. Please choose a model which supports tools.")
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* Two independent reasons a tool stays out of reach, so both may show at once: *@
|
||||
@if (this.ToolsNeedingConfiguration.Count > 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="@true" Class="@this.Class">
|
||||
@(string.Format(T("Some tools selected for this run are not fully configured and stay unused: {0}. Please complete their settings."), string.Join(", ", this.ToolsNeedingConfiguration)))
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@if (this.ToolsBeyondProviderConfidence.Count > 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="@true" Class="@this.Class">
|
||||
@(string.Format(T("Not all tools selected for this run can be used with the chosen AI provider: {0}. Please choose a provider with a higher confidence level to use all of them."), string.Join(", ", this.ToolsBeyondProviderConfidence)))
|
||||
</MudAlert>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Says when tools an assistant was told to use cannot reach the selected provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Whoever named these tools — a document analysis policy, an assistant plugin — did so without
|
||||
/// knowing which provider the user would pick. The user cannot switch a blocked tool on either,
|
||||
/// because there is no selection to switch. Saying nothing would let the run quietly proceed
|
||||
/// without them, which is why this belongs next to the provider selection: choosing another
|
||||
/// provider is what resolves it.
|
||||
/// </remarks>
|
||||
public partial class ManagedToolsWarning : MSGComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public AIStudio.Tools.Components Component { get; set; } = AIStudio.Tools.Components.CHAT;
|
||||
|
||||
/// <summary>
|
||||
/// The tools of this run, as named by the assistant's own rules.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IReadOnlySet<string> ToolIds { get; set; } = new HashSet<string>();
|
||||
|
||||
[Parameter]
|
||||
public AIStudio.Settings.Provider ProviderSettings { get; set; } = AIStudio.Settings.Provider.NONE;
|
||||
|
||||
[Parameter]
|
||||
public string Class { get; set; } = "mb-3";
|
||||
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; init; } = null!;
|
||||
|
||||
private IReadOnlyList<ToolCatalogItem> availableTools = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether this run expects tools while the selected provider cannot call any.
|
||||
/// </summary>
|
||||
private bool NeedsToolCallingProvider => this.ToolIds.Count > 0 && this.SettingsManager.AreToolsEnabled() && !this.ProviderSettings.GetToolCallingAvailability().IsAvailable;
|
||||
|
||||
/// <summary>
|
||||
/// The tools of this run whose settings are incomplete, so they cannot run at all.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Unlike the confidence case, no provider resolves this: the tool itself is missing something,
|
||||
/// such as the web search without a server address. Tools an organization switched off are left
|
||||
/// out, because completing their settings would not bring them back either.
|
||||
/// </remarks>
|
||||
private IReadOnlyList<string> ToolsNeedingConfiguration
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.ToolIds.Count is 0 || !this.SettingsManager.AreToolsEnabled())
|
||||
return [];
|
||||
|
||||
return this.availableTools
|
||||
.Where(x => this.ToolIds.Contains(x.Definition.Id) && x.IsActive && !x.ConfigurationState.IsConfigured)
|
||||
.Select(x => x.Implementation.GetDisplayName())
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The tools of this run which the selected provider is not trusted enough to receive.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Tools switched off in the settings are not counted: choosing another provider would not
|
||||
/// bring them back, so naming them here would send the user after the wrong fix.
|
||||
/// </remarks>
|
||||
private IReadOnlyList<string> ToolsBeyondProviderConfidence
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.ToolIds.Count is 0 || !this.SettingsManager.AreToolsEnabled())
|
||||
return [];
|
||||
|
||||
var providerConfidence = this.ProviderSettings == AIStudio.Settings.Provider.NONE
|
||||
? ConfidenceLevel.NONE
|
||||
: this.ProviderSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).Level;
|
||||
|
||||
return this.availableTools
|
||||
.Where(x => this.ToolIds.Contains(x.Definition.Id) && x.IsActive)
|
||||
.Where(x => !ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, x.MinimumProviderConfidence))
|
||||
.Select(x => x.Implementation.GetDisplayName())
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.availableTools = await this.ToolRegistry.GetCatalogAsync(this.Component);
|
||||
|
||||
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
this.availableTools = await this.ToolRegistry.GetCatalogAsync(this.Component);
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,4 @@ public partial class MudTextList : ComponentBase
|
||||
public string Class { get; set; } = string.Empty;
|
||||
|
||||
private string Classes => $"mud-text-list {this.Class}";
|
||||
}
|
||||
|
||||
public readonly record struct TextItem(string Header, string Text);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using AIStudio.Agents;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Tools.Security;
|
||||
using AIStudio.Tools.Web;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@@ -8,9 +9,21 @@ namespace AIStudio.Components;
|
||||
|
||||
public partial class ReadWebContent : MSGComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// How long loading one page may take.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The user is watching a progress indicator while this runs, so it is shorter than what the
|
||||
/// tools allow themselves for a page fetched in the background.
|
||||
/// </remarks>
|
||||
private const int TIMEOUT_SECONDS = 60;
|
||||
|
||||
[Inject]
|
||||
private HTMLParser HTMLParser { get; init; } = null!;
|
||||
|
||||
private WebPageRetrievalService WebPageRetrievalService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<ReadWebContent> Logger { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AgentTextContentCleaner AgentTextContentCleaner { get; init; } = null!;
|
||||
|
||||
@@ -85,12 +98,23 @@ public partial class ReadWebContent : MSGComponentBase
|
||||
{
|
||||
this.processStep = this.process[ReadWebContentSteps.LOADING];
|
||||
this.StateHasChanged();
|
||||
|
||||
var html = await this.HTMLParser.LoadWebContentHTML(new Uri(this.providedURL));
|
||||
|
||||
|
||||
//
|
||||
// The same retrieval the read web page tool uses, so a page is fetched and read one
|
||||
// way throughout AI Studio. The difference is the target policy: here the user typed
|
||||
// the URL, so their own network is not off limits.
|
||||
//
|
||||
var retrievedPage = await this.WebPageRetrievalService.RetrieveAsync(
|
||||
new Uri(this.providedURL),
|
||||
new WebPageRetrievalOptions
|
||||
{
|
||||
TimeoutSeconds = TIMEOUT_SECONDS,
|
||||
TargetChosenByUser = true,
|
||||
});
|
||||
|
||||
this.processStep = this.process[ReadWebContentSteps.PARSING];
|
||||
this.StateHasChanged();
|
||||
markdown = this.HTMLParser.ParseToMarkdown(html);
|
||||
markdown = retrievedPage.ExtractedPage.Markdown;
|
||||
markdown = await this.PromptInjectionGuardService.SanitizeAsync(markdown, PromptInjectionSource.WebContent(this.providedURL));
|
||||
|
||||
if (this.PreselectContentCleanerAgent && this.providerSettings != AIStudio.Settings.Provider.NONE)
|
||||
@@ -125,7 +149,7 @@ public partial class ReadWebContent : MSGComponentBase
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (this.AgentIsRunning)
|
||||
{
|
||||
@@ -134,6 +158,14 @@ public partial class ReadWebContent : MSGComponentBase
|
||||
await this.AgentIsRunningChanged.InvokeAsync(this.AgentIsRunning);
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
//
|
||||
// Say why nothing was loaded. An empty text field looks like a page without content,
|
||||
// and the reasons a page cannot be read are things the user can act on: a link to a
|
||||
// PDF rather than a page, a host that does not answer, a server refusing the request.
|
||||
//
|
||||
this.Logger.LogWarning(exception, "Could not load the web content from '{ProvidedUrl}'.", this.providedURL);
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.CloudOff, string.Format(this.T("The content of '{0}' could not be loaded: {1}"), this.providedURL, exception.Message)));
|
||||
}
|
||||
|
||||
this.Content = markdown;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList();
|
||||
if (availablePreviewFeatures.Count > 0)
|
||||
{
|
||||
<ConfigurationMultiSelect OptionDescription="@T("Select preview features")" SelectedValues="@this.GetSelectedPreviewFeatures" Data="@availablePreviewFeatures" SelectionUpdate="@this.UpdateEnabledPreviewFeatures" OptionHelp="@T("Which preview features would you like to enable?")" IsItemLocked="@this.IsPluginContributedPreviewFeature" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationMultiSelect OptionDescription="@T("Select preview features")" SelectedValues="@this.GetSelectedPreviewFeatures" Data="@availablePreviewFeatures" SelectionUpdate="@this.UpdateEnabledPreviewFeatures" OptionHelp="@T("Which preview features would you like to enable?")" IsItemLocked="@this.IsPluginContributedPreviewFeature" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.IsLocked" EmptySelectionText="@T("No preview features selected.")" SingleSelectionText="@T("You have selected 1 preview feature.")" MultipleSelectionText="@T("You have selected {0} preview features.")"/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
@inherits SettingsPanelBase
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Build" HeaderText="@T("Tool Settings")">
|
||||
<MudText Typo="Typo.body1" Class="mb-4">
|
||||
@T("Configure global settings for each tool.")
|
||||
</MudText>
|
||||
|
||||
<MudTable Items="@this.items" Hover="@true" Dense="@true">
|
||||
<HeaderContent>
|
||||
<MudTh>@T("Icon")</MudTh>
|
||||
<MudTh>@T("Name")</MudTh>
|
||||
<MudTh>@T("Description")</MudTh>
|
||||
<MudTh>@T("Minimum provider confidence")</MudTh>
|
||||
<MudTh>@T("Status")</MudTh>
|
||||
<MudTh>@T("Settings")</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>
|
||||
<MudIcon Icon="@context.Implementation.Icon" Color="Color.Info" />
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudText Typo="Typo.body1">@context.Implementation.GetDisplayName()</MudText>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudText Typo="Typo.body2">@context.Implementation.GetDescription()</MudText>
|
||||
</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>
|
||||
@if (!context.IsActive)
|
||||
{
|
||||
<MudTooltip Text="@T("This tool has been disabled by your organization.")">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Error" />
|
||||
</MudTooltip>
|
||||
}
|
||||
else if (context.ConfigurationState.IsConfigured)
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="@this.GetConfigurationTooltip(context)">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" />
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Settings" OnClick="@(async () => await this.OpenSettings(context.Definition.Id))" />
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</ExpansionPanel>
|
||||
@@ -0,0 +1,89 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Dialogs.Settings;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.Settings;
|
||||
|
||||
public partial class SettingsPanelTools : SettingsPanelBase
|
||||
{
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; init; } = null!;
|
||||
|
||||
private IReadOnlyList<ToolCatalogItem> items = [];
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
|
||||
this.items = await this.ToolRegistry.GetCatalogAsync(this.ToolRegistry.GetAllDefinitions());
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private async Task OpenSettings(string toolId)
|
||||
{
|
||||
var parameters = new DialogParameters<ToolSettingsDialog>
|
||||
{
|
||||
{ x => x.ToolId, toolId },
|
||||
};
|
||||
|
||||
var dialog = await this.DialogService.ShowAsync<ToolSettingsDialog>(null, parameters, Dialogs.DialogOptions.FULLSCREEN);
|
||||
await dialog.Result;
|
||||
this.items = await this.ToolRegistry.GetCatalogAsync(this.ToolRegistry.GetAllDefinitions());
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private string GetConfigurationTooltip(ToolCatalogItem item) => item.ConfigurationState.MissingRequiredFields.Count switch
|
||||
{
|
||||
_ when !string.IsNullOrWhiteSpace(item.ConfigurationState.Message) => item.ConfigurationState.Message,
|
||||
0 => this.T("This tool still needs to be configured."),
|
||||
_ => string.Format(this.T("Missing required settings: {0}"), string.Join(", ", item.ConfigurationState.MissingRequiredFields.Select(fieldName => this.GetFieldDisplayName(item, fieldName))))
|
||||
};
|
||||
|
||||
private string GetFieldDisplayName(ToolCatalogItem item, string fieldName)
|
||||
{
|
||||
var fieldDefinition = item.Definition.SettingsSchema.Properties.GetValueOrDefault(fieldName);
|
||||
if (fieldDefinition is null)
|
||||
return fieldName;
|
||||
|
||||
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(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: {GetMinimumProviderConfidence(item).GetColor(this.SettingsManager)};";
|
||||
|
||||
private bool IsToolConfidenceManaged() =>
|
||||
ManagedConfiguration.TryGet(x => x.Tools, x => x.MinimumProviderConfidenceByToolId, out var meta) && meta.IsLocked;
|
||||
|
||||
// The catalog already carries the resolved level, so there is nothing to look up again:
|
||||
private static ConfidenceLevel GetMinimumProviderConfidence(ToolCatalogItem item) => item.MinimumProviderConfidence;
|
||||
|
||||
private async Task ChangeMinimumProviderConfidence(ToolCatalogItem item, ConfidenceLevel confidenceLevel)
|
||||
{
|
||||
this.SettingsManager.SetMinimumProviderConfidenceForTool(item.Definition.Id, confidenceLevel, item.Definition.MinimumProviderConfidence);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public readonly record struct TextItem(string Header, string Text);
|
||||
@@ -0,0 +1,10 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
@if (this.availableTools.Count > 0)
|
||||
{
|
||||
@if (this.Component is not Components.CHAT && this.IncludeVisibilityToggle)
|
||||
{
|
||||
<ConfigurationOption OptionDescription="@T("Show tool selection in this assistant?")" LabelOn="@T("Tool selection is visible")" LabelOff="@T("Tool selection is hidden")" State="@(() => this.SettingsManager.IsToolSelectionVisible(this.Component))" StateUpdate="@(value => this.SettingsManager.SetToolSelectionVisibility(this.Component, value))" />
|
||||
}
|
||||
<ConfigurationMultiSelect TData="string" OptionDescription="@this.OptionTitle" SelectedValues="@this.GetSelectedValues" Data="@this.availableTools" SelectionUpdate="@this.UpdateSelection" OptionHelp="@this.OptionHelp" Disabled="@(() => this.AreDefaultToolsDisabled)" IsItemLocked="@this.IsToolDisabled" EmptySelectionText="@T("No tools selected.")" SingleSelectionText="@T("You have selected 1 tool.")" MultipleSelectionText="@T("You have selected {0} tools.")" />
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class ToolDefaultsConfiguration : MSGComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public AIStudio.Tools.Components Component { get; set; } = AIStudio.Tools.Components.CHAT;
|
||||
|
||||
[Parameter]
|
||||
public bool IncludeVisibilityToggle { get; set; } = true;
|
||||
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; init; } = null!;
|
||||
|
||||
private List<ConfigurationSelectData<string>> availableTools = [];
|
||||
|
||||
private string OptionTitle => this.Component is AIStudio.Tools.Components.CHAT ? this.T("Default tools for chat") : this.T("Default tools for this assistant");
|
||||
|
||||
private string OptionHelp => this.Component is AIStudio.Tools.Components.CHAT
|
||||
? this.T("Choose which tools should be preselected for new chats.")
|
||||
: this.T("Choose which tools should be preselected for new runs of this assistant.");
|
||||
|
||||
/// <summary>
|
||||
/// Whether preselecting tools is pointless right now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only where the toggle above decides whether the user ever sees a tool selection: a hidden
|
||||
/// selection makes its defaults meaningless. Without that toggle the assistant reaches its
|
||||
/// tools some other way — from a form field of its own, for instance — and the defaults do
|
||||
/// apply.
|
||||
/// </remarks>
|
||||
private bool AreDefaultToolsDisabled =>
|
||||
this.IncludeVisibilityToggle &&
|
||||
this.Component is not AIStudio.Tools.Components.CHAT &&
|
||||
!this.SettingsManager.IsToolSelectionVisible(this.Component);
|
||||
|
||||
private bool IsToolDisabled(string toolId) => !this.SettingsManager.IsToolActive(toolId);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.availableTools = (await this.ToolRegistry.GetCatalogAsync(this.Component))
|
||||
.Select(x => new ConfigurationSelectData<string>(x.Implementation.GetDisplayName(), x.Definition.Id))
|
||||
.ToList();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private HashSet<string> GetSelectedValues() => this.SettingsManager.GetDefaultToolIds(this.Component);
|
||||
|
||||
private void UpdateSelection(HashSet<string> values) => this.SettingsManager.ConfigurationData.Tools.DefaultToolIdsByComponent[this.Component.ToString()] = [..ToolSelectionRules.NormalizeSelection(values)];
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<div class="d-flex">
|
||||
<MudTooltip Text="@this.ToolButtonTooltip" Placement="Placement.Top">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Build" Class="@this.PopoverButtonClasses" OnClick="@this.ToggleSelection"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudPopover Open="@this.showSelection" AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" DropShadow="@true" Class="border-solid border-4 rounded-lg">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">@T("Tool Selection")</MudText>
|
||||
<MudSpacer />
|
||||
</MudStack>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent Style="min-width: 28em; max-height: 60vh; max-width: 48vw; overflow: auto;">
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Tools allow the LLM to perform targeted additional actions such as web searches or reading web pages.")
|
||||
</MudText>
|
||||
@if (!this.SupportsTools)
|
||||
{
|
||||
<MudText Typo="Typo.body1">@this.UnsupportedToolsMessage</MudText>
|
||||
}
|
||||
else if (this.Disabled)
|
||||
{
|
||||
<MudAlert Dense="@true" Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-3">
|
||||
@T("Tool changes are locked while a response is running. Your current selection is shown below and applies again from the next message once the run is finished.")
|
||||
</MudAlert>
|
||||
}
|
||||
else if (this.catalog.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1">@T("No tools are available in this context.")</MudText>
|
||||
}
|
||||
|
||||
@if (this.SupportsTools && this.catalog.Count > 0)
|
||||
{
|
||||
@foreach (var item in this.catalog)
|
||||
{
|
||||
var isSelected = this.SelectedToolIds.Contains(item.Definition.Id);
|
||||
var isConfigured = item.ConfigurationState.IsConfigured;
|
||||
var providerConfidenceHint = this.GetProviderConfidenceHint(item);
|
||||
<MudPaper Class="pa-2 mb-2 border rounded-lg">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
@*
|
||||
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-2 py-1 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="2">
|
||||
@*
|
||||
The switch only shows the state; the surrounding button does the switching.
|
||||
It therefore takes no pointer events at all: its label reaches past the visible
|
||||
switch and would otherwise swallow the clicks landing in that strip.
|
||||
*@
|
||||
<MudSwitch T="bool" Color="Color.Primary" Value="@isSelected" ReadOnly="@true" Disabled="@this.IsRowDisabled(item)" Style="pointer-events: none;" />
|
||||
<MudIcon Icon="@item.Implementation.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.Implementation.GetDescription()">
|
||||
<MudText Typo="Typo.body1">@item.Implementation.GetDisplayName()</MudText>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</MudButton>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Settings" OnClick="@(async () => await this.OpenSettings(item.Definition.Id))" />
|
||||
</MudStack>
|
||||
@if (!isConfigured)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Warning">@(string.IsNullOrWhiteSpace(item.ConfigurationState.Message) ? T("Required settings are missing. Configure this tool before enabling it.") : item.ConfigurationState.Message)</MudText>
|
||||
}
|
||||
@if (!item.IsActive)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Warning">@T("This tool has been disabled by your organization.")</MudText>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(providerConfidenceHint))
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Warning">@providerConfidenceHint</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
}
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudSpacer />
|
||||
<MudButton Variant="Variant.Filled" OnClick="@this.Hide">@T("Close")</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</MudPopover>
|
||||
</div>
|
||||
@@ -0,0 +1,155 @@
|
||||
using AIStudio.Dialogs.Settings;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class ToolSelection : MSGComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public AIStudio.Tools.Components Component { get; set; } = AIStudio.Tools.Components.CHAT;
|
||||
|
||||
[Parameter]
|
||||
public required AIStudio.Settings.Provider LLMProvider { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public HashSet<string> SelectedToolIds { get; set; } = [];
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<HashSet<string>> SelectedToolIdsChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string PopoverButtonClasses { get; set; } = string.Empty;
|
||||
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
private bool showSelection;
|
||||
private IReadOnlyList<ToolCatalogItem> catalog = [];
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
this.SelectedToolIds = ToolSelectionRules.NormalizeSelection(this.SelectedToolIds);
|
||||
base.OnParametersSet();
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private ToolCallingAvailability ToolCallingAvailability => this.LLMProvider.GetToolCallingAvailability();
|
||||
|
||||
private bool SupportsTools => this.ToolCallingAvailability.IsAvailable;
|
||||
|
||||
private string ToolButtonTooltip => this.SupportsTools
|
||||
? this.T("Select tools")
|
||||
: this.UnsupportedToolsMessage;
|
||||
|
||||
private string UnsupportedToolsMessage => this.ToolCallingAvailability.Message;
|
||||
|
||||
private ConfidenceLevel ProviderConfidence => this.LLMProvider == AIStudio.Settings.Provider.NONE
|
||||
? ConfidenceLevel.NONE
|
||||
: this.LLMProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level;
|
||||
|
||||
private async Task ToggleSelection()
|
||||
{
|
||||
this.showSelection = !this.showSelection;
|
||||
if (this.showSelection)
|
||||
this.catalog = await this.ToolRegistry.GetCatalogAsync(this.Component);
|
||||
}
|
||||
|
||||
private void Hide() => this.showSelection = false;
|
||||
|
||||
/// <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,
|
||||
/// 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.Disabled || !this.SupportsTools;
|
||||
|
||||
/// <summary>
|
||||
/// Switches a tool when the user clicks anywhere in its row.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Hitting the switch itself is needless precision work, so the text, the icon, and the empty
|
||||
/// space count as well. Only the settings button is left out, because it sits outside the
|
||||
/// button that spans the rest of the row.
|
||||
/// </remarks>
|
||||
private async Task ToggleToolFromRow(ToolCatalogItem item)
|
||||
{
|
||||
if (this.IsRowDisabled(item))
|
||||
return;
|
||||
|
||||
await this.ChangeSelection(item.Definition.Id, !this.SelectedToolIds.Contains(item.Definition.Id));
|
||||
}
|
||||
|
||||
private async Task ChangeSelection(string toolId, bool isSelected)
|
||||
{
|
||||
if (isSelected && !this.SettingsManager.IsToolActive(toolId))
|
||||
return;
|
||||
|
||||
var updated = new HashSet<string>(this.SelectedToolIds, StringComparer.Ordinal);
|
||||
if (isSelected)
|
||||
updated.Add(toolId);
|
||||
else
|
||||
updated.Remove(toolId);
|
||||
|
||||
updated = ToolSelectionRules.NormalizeSelection(updated);
|
||||
this.SelectedToolIds = updated;
|
||||
await this.SelectedToolIdsChanged.InvokeAsync(updated);
|
||||
}
|
||||
|
||||
// The catalog already carries the resolved level, so there is nothing to look up again:
|
||||
private static ConfidenceLevel GetMinimumProviderConfidence(ToolCatalogItem item) => item.MinimumProviderConfidence;
|
||||
|
||||
private bool IsBlockedByProviderConfidence(ToolCatalogItem item) => !ToolSelectionRules.IsProviderConfidenceAllowed(this.ProviderConfidence, GetMinimumProviderConfidence(item));
|
||||
|
||||
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}."),
|
||||
GetMinimumProviderConfidence(item).GetName(),
|
||||
this.ProviderConfidence.GetName());
|
||||
}
|
||||
|
||||
private async Task OpenSettings(string toolId)
|
||||
{
|
||||
var parameters = new DialogParameters<ToolSettingsDialog>
|
||||
{
|
||||
{ x => x.ToolId, toolId },
|
||||
};
|
||||
|
||||
var dialog = await this.DialogService.ShowAsync<ToolSettingsDialog>(null, parameters, Dialogs.DialogOptions.FULLSCREEN);
|
||||
await dialog.Result;
|
||||
this.catalog = await this.ToolRegistry.GetCatalogAsync(this.Component);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
@if (this.availableTools.Count > 0)
|
||||
{
|
||||
<ConfigurationMultiSelect TData="string"
|
||||
OptionDescription="@this.Label"
|
||||
OptionHelp="@this.Help"
|
||||
Data="@this.availableTools"
|
||||
SelectedValues="@(() => this.SelectedToolIds)"
|
||||
SelectionUpdateAsync="@this.OptionChangedAsync"
|
||||
Disabled="@(() => this.ReadOnly || this.Disabled)"
|
||||
IsItemLocked="@this.IsToolLocked"
|
||||
EmptySelectionText="@T("No tools selected")"
|
||||
SingleSelectionText="@T("1 tool selected")"
|
||||
MultipleSelectionText="@T("{0} tools selected")"/>
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Picks the tools of a run as an ordinary form field, next to the settings they belong to.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The counterpart to the tool selection in the footer, which floats above a whole chat or
|
||||
/// assistant. Where the tools belong to one specific setting — the instructions of a batch job,
|
||||
/// say — they are easier to grasp right there, and a read-only field is the honest way to show
|
||||
/// tools somebody else decided on.
|
||||
/// </remarks>
|
||||
public partial class ToolSelectionField : MSGComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public AIStudio.Tools.Components Component { get; set; } = AIStudio.Tools.Components.CHAT;
|
||||
|
||||
[Parameter]
|
||||
public HashSet<string> SelectedToolIds { get; set; } = [];
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<HashSet<string>> SelectedToolIdsChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Shows the tools without letting the user change them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For tools that were decided elsewhere, such as by a document analysis policy. The user
|
||||
/// still gets to see what the run will do.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string Help { get; set; } = string.Empty;
|
||||
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; init; } = null!;
|
||||
|
||||
private List<ConfigurationSelectData<string>> availableTools = [];
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.availableTools = (await this.ToolRegistry.GetCatalogAsync(this.Component))
|
||||
.Select(x => new ConfigurationSelectData<string>(x.Implementation.GetDisplayName(), x.Definition.Id))
|
||||
.ToList();
|
||||
|
||||
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private bool IsToolLocked(string toolId) => !this.SettingsManager.IsToolActive(toolId);
|
||||
|
||||
private async Task OptionChangedAsync(HashSet<string> updatedToolIds)
|
||||
{
|
||||
this.SelectedToolIds = ToolSelectionRules.NormalizeSelection(updatedToolIds);
|
||||
await this.SelectedToolIdsChanged.InvokeAsync(this.SelectedToolIds);
|
||||
}
|
||||
|
||||
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
this.availableTools = (await this.ToolRegistry.GetCatalogAsync(this.Component))
|
||||
.Select(x => new ConfigurationSelectData<string>(x.Implementation.GetDisplayName(), x.Definition.Id))
|
||||
.ToList();
|
||||
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user