mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-14 21:48:27 +00:00
Added the Model Comparison assistant for blind comparisons of two models (#1009)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
12f26445ba
commit
b7255694cd
221 files changed
+19989
-90
No files matched your search
@@ -0,0 +1,30 @@
|
||||
@typeparam TMode
|
||||
<MudGrid Spacing="3" Class="mb-3">
|
||||
@foreach (var option in this.Options)
|
||||
{
|
||||
<MudItem xs="12" md="6">
|
||||
<MudCard Outlined="@true" Style="height: 100%; display: flex; flex-direction: column;">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudStack AlignItems="AlignItems.Center" Row="@true">
|
||||
<MudIcon Icon="@option.Icon" Size="Size.Large" Color="Color.Primary"/>
|
||||
<MudText Typo="Typo.h6">
|
||||
@option.Title
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent Style="flex-grow: 1;">
|
||||
<MudJustifiedText Typo="Typo.body1">
|
||||
@option.Description
|
||||
</MudJustifiedText>
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@option.Icon" OnClick="@(async () => await this.Choose(option))">
|
||||
@option.Title
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Shows one card per mode with which an assistant can start, and reports the mode the user chose.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The assistant shows nothing else until the user chose, so the texts of the options come from the assistant: they
|
||||
/// tell what it does in each mode. Generic, so that every assistant names its modes with an enum of its own.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TMode">The type of the modes of the assistant.</typeparam>
|
||||
public partial class AssistantModeChoice<TMode> : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The options to choose from, in the order the cards show them.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IReadOnlyList<AssistantModeOption<TMode>> Options { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Called with the mode of the option the user chose.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<TMode> ModeChosen { get; set; }
|
||||
|
||||
private async Task Choose(AssistantModeOption<TMode> option) => await this.ModeChosen.InvokeAsync(option.Mode);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// One of the modes an assistant offers before it starts, e.g., to compare models on one request or on whole tasks.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMode">The type of the modes of the assistant.</typeparam>
|
||||
/// <param name="Mode">The mode the assistant works in once the user chose this option.</param>
|
||||
/// <param name="Icon">The icon of the option.</param>
|
||||
/// <param name="Title">The short name of the option.</param>
|
||||
/// <param name="Description">What the assistant does in this mode.</param>
|
||||
public readonly record struct AssistantModeOption<TMode>(TMode Mode, string Icon, string Title, string Description);
|
||||
@@ -1,5 +1,5 @@
|
||||
<MudExpansionPanels Dense="@true" Elevation="0" Class="@this.Class">
|
||||
<MudExpansionPanel Dense="@true" Class="border-solid border rounded pt-n4" Style="border-color: #BDBDBD" Expanded="@this.Expanded" ExpandedChanged="@this.ExpandedChanged">
|
||||
<MudExpansionPanel Dense="@true" Class="border-solid border rounded pt-n4" Style="border-color: #BDBDBD" Expanded="@this.Expanded" ExpandedChanged="@this.ExpandedChanged" KeepContentAlive="@this.KeepContentAlive">
|
||||
<TitleContent>
|
||||
<div class="d-flex align-center">
|
||||
<MudIcon Icon="@this.HeaderIcon" Color="@this.IconColor" Class="mr-3"/>
|
||||
|
||||
@@ -34,6 +34,16 @@ public partial class CompactExpansionPanel : ComponentBase
|
||||
[Parameter]
|
||||
public EventCallback<bool> ExpandedChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the content stays rendered while the panel is collapsed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default, a collapsed panel drops its content, and a form around it no longer knows the fields inside. A panel
|
||||
/// with fields which the form has to check also while it is collapsed sets this.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool KeepContentAlive { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
@inherits MSGComponentBase
|
||||
<ConfigurationSelect IsLocked="@this.IsLocked" OptionDescription="@T("Preselected provider")" Disabled="@(() => this.Disabled())" OptionHelp="@this.HelpText()" Data="@this.FilteredData()" SelectedValue="@this.SelectedValue" SelectionUpdate="@this.SelectionUpdate">
|
||||
<ConfigurationSelect IsLocked="@this.IsLocked" OptionDescription="@(this.OptionDescription ?? T("Preselected provider"))" Disabled="@(() => this.Disabled())" OptionHelp="@this.HelpText()" Data="@this.FilteredData()" SelectedValue="@this.SelectedValue" SelectionUpdate="@this.SelectionUpdate">
|
||||
<ItemTemplate Context="providerData">
|
||||
@if (this.GetProvider(providerData.Value) is { } provider)
|
||||
{
|
||||
@@ -10,4 +10,4 @@
|
||||
@providerData.Name
|
||||
}
|
||||
</ItemTemplate>
|
||||
</ConfigurationSelect>
|
||||
</ConfigurationSelect>
|
||||
@@ -33,8 +33,27 @@ public partial class ConfigurationProviderSelection : MSGComponentBase
|
||||
[Parameter]
|
||||
public Func<bool> IsLocked { get; set; } = () => false;
|
||||
|
||||
/// <summary>
|
||||
/// The label of the option, or null for "Preselected provider".
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? OptionDescription { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The text of an entry which preselects no provider at all, or null to offer no such entry.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The entry stands before "Use app default" and selects the ID of Provider.NONE, because an empty text already
|
||||
/// means the provider of the whole app.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public string? NoPreselectionText { get; set; }
|
||||
|
||||
private IEnumerable<ConfigurationSelectData<string>> FilteredData()
|
||||
{
|
||||
if (this.NoPreselectionText is not null)
|
||||
yield return new(this.NoPreselectionText, AIStudio.Settings.Provider.NONE.Id);
|
||||
|
||||
if(this.Component is not Tools.Components.NONE and not Tools.Components.APP_SETTINGS)
|
||||
yield return new(T("Use app default"), string.Empty);
|
||||
|
||||
@@ -70,6 +89,10 @@ public partial class ConfigurationProviderSelection : MSGComponentBase
|
||||
|
||||
if(string.IsNullOrWhiteSpace(this.SelectedValue()))
|
||||
break;
|
||||
|
||||
// Preselecting no provider stays valid, whichever providers exist:
|
||||
if (this.NoPreselectionText is not null && this.SelectedValue() == AIStudio.Settings.Provider.NONE.Id)
|
||||
break;
|
||||
|
||||
// Check if the selected value is still valid:
|
||||
if (this.Data.All(x => x.Value != this.SelectedValue()))
|
||||
|
||||
@@ -83,6 +83,7 @@ public partial class DataFolders : MSGComponentBase
|
||||
DataBackupEntryKind.TOKENIZERS => TA("The tokenizers you chose for your providers."),
|
||||
DataBackupEntryKind.VISUAL_BRIEFINGS => TA("Your projects in the Visual Briefing Assistant."),
|
||||
DataBackupEntryKind.BEYOND_FACTS_LAB => TA("Your preparations in the BeyondFacts Lab, with their material and conversations."),
|
||||
DataBackupEntryKind.MODEL_COMPARISONS => TA("Your presets, results, and comparison series with their chats in the Model Comparison assistant."),
|
||||
DataBackupEntryKind.DATA_SOURCE_INDEX => TA("The index of your local data sources: which files AI Studio embedded, and their text."),
|
||||
DataBackupEntryKind.VECTOR_STORES => TA("The embeddings of your local data sources."),
|
||||
DataBackupEntryKind.MEDIA_TRANSCRIPTS => TA("The transcripts of the audio and video files in your local data sources."),
|
||||
|
||||
@@ -70,10 +70,13 @@
|
||||
<MudTextSwitch Label="@T("Semantic Search")" Value="@this.IsSemanticSearchPreferred" LabelOn="@TA("Yes, the AI searches your data sources itself, whenever a question calls for it (Semantic Search).")" LabelOff="@TA("No, AI Studio searches your data sources with every message, before the AI answers (classic RAG).")" ValueChanged="@this.SemanticSearchChanged" Dense="@true" Disabled="@this.ReadOnly"/>
|
||||
}
|
||||
|
||||
<MudTextSwitch Label="@T("AI-based data source selection")" Value="@this.aiBasedSourceSelection" LabelOn="@T("Yes, let the AI decide which data sources are needed.")" LabelOff="@T("No, I manually decide which data source to use.")" ValueChanged="@this.AutoModeChanged" Dense="@true" Disabled="@this.ReadOnly"/>
|
||||
@if (this.AllowsAutomaticSelection)
|
||||
{
|
||||
<MudTextSwitch Label="@T("AI-based data source selection")" Value="@this.aiBasedSourceSelection" LabelOn="@T("Yes, let the AI decide which data sources are needed.")" LabelOff="@T("No, I manually decide which data source to use.")" ValueChanged="@this.AutoModeChanged" Dense="@true" Disabled="@this.ReadOnly"/>
|
||||
}
|
||||
|
||||
@* With Semantic Search, no agent takes part: nothing would validate the retrieved data. *@
|
||||
@if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation && !this.IsSemanticSearchEffective)
|
||||
@if (this.AllowsAutomaticSelection && this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation && !this.IsSemanticSearchEffective)
|
||||
{
|
||||
<MudTextSwitch Label="@T("AI-based data validation")" Value="@this.aiBasedValidation" LabelOn="@T("Yes, let the AI validate & filter the retrieved data.")" LabelOff="@T("No, use all data retrieved from the data sources.")" ValueChanged="@this.ValidationModeChanged" Dense="@true" Disabled="@this.ReadOnly"/>
|
||||
}
|
||||
|
||||
@@ -68,6 +68,42 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
[Parameter]
|
||||
public bool ConfiguresChatDefaults { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Where the chats run which search these data sources. It decides, among other things, whether the models may
|
||||
/// search the data sources themselves, see ToolRegistry.GetEffectiveRetrievalModeAsync.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Tools.Components Component { get; set; } = Tools.Components.CHAT;
|
||||
|
||||
/// <summary>
|
||||
/// Further models which chat with the same data source options, e.g., the second model of a comparison series.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The selection then offers only the data sources each model may use, and Semantic Search only when each model
|
||||
/// can search itself, see SharedSelectionRules. Without further models, it offers what LLMProvider alone may use.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public IReadOnlyList<AIStudio.Settings.Provider> AdditionalProviders { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Further models which read what the data sources gave without searching them, e.g., the judge of a comparison
|
||||
/// series, which reads both chats. Each of them must be allowed every data source the selection offers, but they
|
||||
/// have no say in how the data sources are searched.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IReadOnlyList<AIStudio.Settings.Provider> AdditionalReaders { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user may let an AI select the data sources and validate what they gave.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Without, the user selects the data sources by hand, and the selection neither shows the switches for the AI nor
|
||||
/// listens for the data sources an AI selected elsewhere: e.g., a comparison series which gives both models the
|
||||
/// same data sources. Options which switch the AI on count as switched off then.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool AllowsAutomaticSelection { get; set; } = true;
|
||||
|
||||
[Inject]
|
||||
private DataSourceService DataSourceService { get; init; } = null!;
|
||||
|
||||
@@ -97,13 +133,13 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.RAG_AUTO_DATA_SOURCES_SELECTED ]);
|
||||
|
||||
this.ApplyFilters([], this.AllowsAutomaticSelection ? [ Event.RAG_AUTO_DATA_SOURCES_SELECTED ] : []);
|
||||
|
||||
//
|
||||
// Load the settings:
|
||||
//
|
||||
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
|
||||
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
|
||||
this.aiBasedSourceSelection = this.IsAutomaticSelectionOn;
|
||||
this.aiBasedValidation = this.IsAutomaticValidationOn;
|
||||
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
|
||||
this.retrievalMode = this.DataSourceOptions.RetrievalMode;
|
||||
this.waitingForDataSources = this.areDataSourcesEnabled && this.SelectionMode is not DataSourceSelectionMode.CONFIGURATION_MODE;
|
||||
@@ -124,8 +160,8 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
{
|
||||
if (!this.internalChange)
|
||||
{
|
||||
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
|
||||
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
|
||||
this.aiBasedSourceSelection = this.IsAutomaticSelectionOn;
|
||||
this.aiBasedValidation = this.IsAutomaticValidationOn;
|
||||
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
|
||||
this.retrievalMode = this.DataSourceOptions.RetrievalMode;
|
||||
this.selectedDataSources = this.GetDataSourcesFromConfiguredIds();
|
||||
@@ -177,6 +213,21 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether an AI selects the data sources, see AllowsAutomaticSelection.
|
||||
/// </summary>
|
||||
private bool IsAutomaticSelectionOn => this.AllowsAutomaticSelection && this.DataSourceOptions.AutomaticDataSourceSelection;
|
||||
|
||||
/// <summary>
|
||||
/// Whether an AI validates what the data sources gave, see AllowsAutomaticSelection.
|
||||
/// </summary>
|
||||
private bool IsAutomaticValidationOn => this.AllowsAutomaticSelection && this.DataSourceOptions.AutomaticValidation;
|
||||
|
||||
/// <summary>
|
||||
/// The models which chat with these data sources.
|
||||
/// </summary>
|
||||
private IReadOnlyList<AIStudio.Settings.Provider> ChattingProviders => [this.LLMProvider, .. this.AdditionalProviders];
|
||||
|
||||
private SelectionMode GetListSelectionMode() => this.aiBasedSourceSelection ? MudBlazor.SelectionMode.SingleSelection : MudBlazor.SelectionMode.MultiSelection;
|
||||
|
||||
private IReadOnlyCollection<DataSourceAgentSelected> GetSelectedDataSourcesWithAI() => this.DataSourcesAISelected.Where(n => n.Selected).ToList();
|
||||
@@ -189,9 +240,9 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
{
|
||||
this.DataSourceOptions = options;
|
||||
this.DataSourcesAISelected = aiSelectedDataSources ?? [];
|
||||
|
||||
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
|
||||
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
|
||||
|
||||
this.aiBasedSourceSelection = this.IsAutomaticSelectionOn;
|
||||
this.aiBasedValidation = this.IsAutomaticValidationOn;
|
||||
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
|
||||
this.retrievalMode = this.DataSourceOptions.RetrievalMode;
|
||||
this.selectedDataSources = this.GetDataSourcesFromConfiguredIds();
|
||||
@@ -263,14 +314,24 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
// would never come back, while the RAG process keeps reading it from the preselection.
|
||||
//
|
||||
// How the chat searches decides whether the agents of the RAG process see the data, and
|
||||
// with that which data sources the provider of the chat may use at all. This component
|
||||
// only ever selects for a chat:
|
||||
// with that which data sources the provider of the chat may use at all. With several
|
||||
// models, all of them search the same way, and each must be allowed every data source:
|
||||
//
|
||||
var effectiveMode = await this.ToolRegistry.GetEffectiveRetrievalModeAsync(this.DataSourceOptions, this.LLMProvider, Tools.Components.CHAT);
|
||||
var sources = await this.DataSourceService.GetDataSources(this.LLMProvider, this.DataSourceOptions, effectiveMode.Mode, this.GetDataSourcesFromConfiguredIds());
|
||||
var preselectedDataSources = this.GetDataSourcesFromConfiguredIds();
|
||||
List<EffectiveRetrievalMode> modes = [];
|
||||
foreach (var provider in this.ChattingProviders)
|
||||
modes.Add(await this.ToolRegistry.GetEffectiveRetrievalModeAsync(this.DataSourceOptions, provider, this.Component));
|
||||
|
||||
var effectiveMode = SharedSelectionRules.RetrievalModeOfAll(modes);
|
||||
List<AllowedSelectedDataSources> results = [];
|
||||
foreach (var provider in this.ChattingProviders.Concat(this.AdditionalReaders))
|
||||
results.Add(await this.DataSourceService.GetDataSources(provider, this.DataSourceOptions, effectiveMode.Mode, preselectedDataSources));
|
||||
|
||||
if (generation != this.loadAndApplyFiltersGeneration)
|
||||
return;
|
||||
|
||||
var sources = SharedSelectionRules.DataSourcesOfAll(results);
|
||||
|
||||
this.effectiveRetrievalMode = effectiveMode;
|
||||
this.availableDataSources = sources.AllowedDataSources;
|
||||
this.dataSourcesAwaitingReindex = sources.DataSourcesAwaitingReindex;
|
||||
@@ -392,6 +453,8 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
private string GetSemanticSearchUnavailableMessage() => this.effectiveRetrievalMode.SemanticSearchBlockReason switch
|
||||
{
|
||||
ToolOfferBlockReason.TOOLS_SWITCHED_OFF => TA("Your organization has switched tools off, so AI Studio uses classic RAG instead and searches your data sources with every message."),
|
||||
ToolOfferBlockReason.MODEL_CANNOT_USE_TOOLS when this.AdditionalProviders.Count > 0 => TA("One of the selected models cannot use tools, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message."),
|
||||
ToolOfferBlockReason.PROVIDER_CONFIDENCE_TOO_LOW when this.AdditionalProviders.Count > 0 => TA("One of the selected providers is not trusted enough for Semantic Search, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message."),
|
||||
ToolOfferBlockReason.MODEL_CANNOT_USE_TOOLS => TA("The selected model cannot use tools, so AI Studio uses classic RAG instead and searches your data sources with every message."),
|
||||
ToolOfferBlockReason.TOOL_SWITCHED_OFF => TA("Your organization has switched Semantic Search off, so AI Studio uses classic RAG instead and searches your data sources with every message."),
|
||||
ToolOfferBlockReason.TOOL_SWITCHED_OFF_BY_USER => TA("You switched Semantic Search off in the tool settings, so AI Studio uses classic RAG instead and searches your data sources with every message."),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@inherits MudComponentBase
|
||||
|
||||
<MudTextField T="string" Disabled="@this.Disabled" Text="@this.text" TextChanged="@this.OnTextChanged" Validation="@this.ValidationFunc" Label="@this.Label" Variant="Variant.Outlined" Lines="@this.Lines" MaxLines="@this.MaxLines" Sizing="@(this.MaxLines > this.Lines ? InputSizing.Auto : InputSizing.Fixed)" Margin="Margin.Dense" UserAttributes="@this.Attributes" HelperText="@this.HelpText" Placeholder="@this.Placeholder" Class="mb-3" Immediate="true" Adornment="Adornment.Start" AdornmentIcon="@this.Icon"/>
|
||||
@* Bound by its value, the field checks itself only once touched. Bound by its text, it would check its first text, too, and show an issue as soon as it appears, e.g., when a switch shows it later: *@
|
||||
<MudTextField T="string" Disabled="@this.Disabled" Value="@this.text" ValueChanged="@this.OnTextChanged" Validation="@this.ValidationFunc" Label="@this.Label" Variant="Variant.Outlined" Lines="@this.Lines" MaxLines="@this.MaxLines" Sizing="@(this.MaxLines > this.Lines ? InputSizing.Auto : InputSizing.Fixed)" Margin="Margin.Dense" UserAttributes="@this.Attributes" HelperText="@this.HelpText" Placeholder="@this.Placeholder" Class="mb-3" Immediate="true" Adornment="Adornment.Start" AdornmentIcon="@this.Icon"/>
|
||||
@@ -64,12 +64,22 @@ public partial class DebouncedTextField : MudComponentBase, IDisposable
|
||||
this.debounceTimer.Elapsed += (_, _) =>
|
||||
{
|
||||
this.debounceTimer.Stop();
|
||||
this.InvokeAsync(async () => await this.TextChanged.InvokeAsync(this.text)).Observe($"{nameof(DebouncedTextField)}: notifying about changed text");
|
||||
this.InvokeAsync(async () =>
|
||||
{
|
||||
// The parent hands this text back with its next render, which is no change of its own, cf. OnParametersSetAsync:
|
||||
this.lastParameterText = this.text;
|
||||
await this.TextChanged.InvokeAsync(this.text);
|
||||
}).Observe($"{nameof(DebouncedTextField)}: notifying about changed text");
|
||||
this.InvokeAsync(async () => await this.WhenTextChangedAsync(this.text)).Observe($"{nameof(DebouncedTextField)}: handling changed text asynchronously");
|
||||
this.InvokeAsync(() => this.WhenTextCanged(this.text)).Observe($"{nameof(DebouncedTextField)}: handling changed text");
|
||||
};
|
||||
|
||||
this.isInitialized = true;
|
||||
|
||||
// A text the field starts with, e.g., one sent from another assistant, is handled like a typed one:
|
||||
if (!string.IsNullOrWhiteSpace(this.text))
|
||||
this.debounceTimer.Start();
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
@@ -82,16 +92,19 @@ public partial class DebouncedTextField : MudComponentBase, IDisposable
|
||||
if(Math.Abs(this.debounceTimer.Interval - this.DebounceTime.TotalMilliseconds) > 1)
|
||||
this.debounceTimer.Interval = this.DebounceTime.TotalMilliseconds;
|
||||
|
||||
// Only sync when the parent's parameter actually changed since the last change:
|
||||
//
|
||||
// Only a text which the parent changed itself, e.g., by a reset, is synced and handled like a typed one. Every
|
||||
// other render of the parent leaves the timer alone: restarting it there let its notification render the parent
|
||||
// again, which restarted the timer, and so on, as long as the assistant stayed open.
|
||||
//
|
||||
if (this.Text != this.lastParameterText)
|
||||
{
|
||||
this.text = this.Text;
|
||||
this.lastParameterText = this.Text;
|
||||
this.debounceTimer.Stop();
|
||||
this.debounceTimer.Start();
|
||||
}
|
||||
|
||||
this.debounceTimer.Stop();
|
||||
this.debounceTimer.Start();
|
||||
|
||||
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
@using AIStudio.Chat
|
||||
@inherits MSGComponentBase
|
||||
|
||||
@if (this.conversation is null)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-3"/>
|
||||
}
|
||||
else if (this.conversation.Thread is not { } thread)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Variant="Variant.Outlined" Class="mb-3">
|
||||
@T("AI Studio cannot open this conversation. Its file may be damaged, or a newer version of AI Studio stored it.")
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (!this.conversation.HasVisibleMessages && !this.IsRunning && !string.IsNullOrWhiteSpace(this.Introduction))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mb-3">@this.Introduction</MudText>
|
||||
}
|
||||
|
||||
@foreach (var block in thread.Blocks)
|
||||
{
|
||||
@if (block is { HideFromUser: false, Content: not null })
|
||||
{
|
||||
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content" ExportTitle="@T("Export message")" ExportFileName="@this.ExportFileName"/>
|
||||
}
|
||||
}
|
||||
|
||||
@if (!this.IsReadOnly)
|
||||
{
|
||||
@if (this.ProviderIssue is { } issue)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Warning" Class="mb-2">@issue</MudText>
|
||||
}
|
||||
|
||||
@* Laid out like the input of the chat page: sending through the icon in the field, and the tokens as its helper text. *@
|
||||
<MudTextField T="string"
|
||||
@bind-Value="@this.userInput"
|
||||
Immediate="true"
|
||||
Lines="3"
|
||||
Sizing="InputSizing.Auto"
|
||||
MaxLines="12"
|
||||
Variant="Variant.Outlined"
|
||||
Label="@TA("Your message")"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@AppIcons.SEND"
|
||||
AdornmentColor="@(this.CanSendMessage ? Color.Primary : Color.Default)"
|
||||
AdornmentAriaLabel="@T("Send")"
|
||||
OnAdornmentClick="@this.SendAsync"
|
||||
OnKeyUp="@this.InputKeyEventAsync"
|
||||
UserAttributes="@this.inputAttributes"
|
||||
HelperText="@this.TokenText"
|
||||
Class="mt-2"
|
||||
Disabled="@(!this.CanSend)"/>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="mt-2">
|
||||
@if (this.AllowsAttachments)
|
||||
{
|
||||
<AttachDocuments Name="File Attachments" DocumentPaths="@this.attachments" DocumentPathsChanged="@this.AttachmentsChanged" CatchAllDocuments="true" UseSmallForm="true" ShowMediaStatus="false" Provider="@this.Provider" AllowedFileTypes="@this.AllowedFileTypes" Disabled="@(!this.CanSend)"/>
|
||||
}
|
||||
@if (this.IsRunning)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@AppIcons.STOP" OnClick="@this.CancelAsync">@T("Stop")</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Chat.Embedded;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A chat which an assistant shows on its own page, such as one of the two chats of a task of the Model Comparison
|
||||
/// assistant.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The chat itself lives in EmbeddedChatService, so an answer goes on while the user looks elsewhere; this component
|
||||
/// shows it and renders whenever the service says it changed. What a request carries beside the conversation, such as
|
||||
/// the system prompt and the tools, the assistant decides through PrepareAsync. The count of tokens leaves out the
|
||||
/// context window of the model on purpose: it would tell models apart which the user compares without knowing them.
|
||||
/// </remarks>
|
||||
public partial class EmbeddedChat : MSGComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets where the chat lives.
|
||||
/// </summary>
|
||||
[Parameter, EditorRequired]
|
||||
public IEmbeddedChatStore Store { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the assistant which shows the chat.
|
||||
/// </summary>
|
||||
[Parameter, EditorRequired]
|
||||
public Tools.Components Component { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the chat.
|
||||
/// </summary>
|
||||
[Parameter, EditorRequired]
|
||||
public Guid ChatId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how a chat which has no stored thread yet starts.
|
||||
/// </summary>
|
||||
[Parameter, EditorRequired]
|
||||
public Func<ChatThread> CreateThread { get; set; } = () => new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the provider which answers.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public AIStudio.Settings.Provider Provider { get; set; } = AIStudio.Settings.Provider.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how a request is readied before the message goes in, see EmbeddedChatRequest.PrepareAsync.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<ChatThread, Task<bool>> PrepareAsync { get; set; } = _ => Task.FromResult(true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether messages to the user leave out which provider and which model answer.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool HidesModelIdentity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the chat only shows its messages, without a field to write another one.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool IsReadOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the user may attach files to a message.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool AllowsAttachments { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets which files the user may attach, or null for every file a chat takes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public FileTypeFilter[]? AllowedFileTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a message which the chat sends by itself when it shows a conversation without any message of the
|
||||
/// user, e.g., the first message of another chat on the same task. Sent once per chat, when the chat loads or, when
|
||||
/// the message arrives later, as soon as it does; when it cannot be sent, it waits in the field, so that the user
|
||||
/// can send it. See EmbeddedChatOpening.SendsNow.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The chat stores the message as it is, so give it a copy of its own, see ContentText.DeepClone.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public ContentText? OpeningMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets why the provider cannot answer, or null when it can.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? ProviderIssue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets what the chat says before its conversation begins, or nothing.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? Introduction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name a message gets when the user exports it.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string ExportFileName { get; set; } = string.Empty;
|
||||
|
||||
[Inject]
|
||||
private EmbeddedChatService ChatService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ConversationTokenCounter TokenCounter { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Ends a count when the component goes away.
|
||||
/// </summary>
|
||||
private readonly CancellationTokenSource countCancellation = new();
|
||||
|
||||
/// <summary>
|
||||
/// The token of countCancellation, taken while the source still lives, see BflChat.
|
||||
/// </summary>
|
||||
private CancellationToken countToken;
|
||||
|
||||
/// <summary>
|
||||
/// What the next request carries, as far as it was counted.
|
||||
/// </summary>
|
||||
private ConversationTokens tokens = ConversationTokens.UNAVAILABLE;
|
||||
|
||||
/// <summary>
|
||||
/// The provider the tokens were counted for, to count again when the parent chooses another one.
|
||||
/// </summary>
|
||||
private string countedProviderId = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The chat shown, or null while it loads.
|
||||
/// </summary>
|
||||
private EmbeddedConversation? conversation;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the chat shown, to notice when the parent shows another one.
|
||||
/// </summary>
|
||||
private Guid shownChatId;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the chat which sent its opening message already, so that it sends the message only once.
|
||||
/// </summary>
|
||||
private Guid openedChatId;
|
||||
|
||||
/// <summary>
|
||||
/// The message the user writes.
|
||||
/// </summary>
|
||||
private string userInput = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The files attached to the message the user writes.
|
||||
/// </summary>
|
||||
private HashSet<FileAttachment> attachments = [];
|
||||
|
||||
/// <summary>
|
||||
/// The attributes of the message field, such as the spell checking the user chose.
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, object?> inputAttributes = new();
|
||||
|
||||
private bool IsRunning => this.conversation?.IsRunning ?? false;
|
||||
|
||||
private bool CanSend => this.conversation is { IsAvailable: true, IsRunning: false } && this.ProviderIssue is null;
|
||||
|
||||
private bool CanSendMessage => this.CanSend && !string.IsNullOrWhiteSpace(this.userInput);
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.countToken = this.countCancellation.Token;
|
||||
this.ChatService.ConversationChanged += this.OnConversationChangedAsync;
|
||||
this.SettingsManager.InjectSpellchecking(this.inputAttributes);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await base.OnParametersSetAsync();
|
||||
if (this.shownChatId != this.ChatId)
|
||||
{
|
||||
this.shownChatId = this.ChatId;
|
||||
this.conversation = null;
|
||||
this.userInput = string.Empty;
|
||||
this.attachments = [];
|
||||
this.conversation = await this.ChatService.GetAsync(this.Store, this.Component, this.ChatId, this.CreateThread);
|
||||
await this.RecountAsync();
|
||||
}
|
||||
else if (this.countedProviderId != this.Provider.Id)
|
||||
await this.RecountAsync();
|
||||
|
||||
// Not awaited: the answer arrives through ConversationChanged, and the parameters of the next render must not wait for it.
|
||||
if (EmbeddedChatOpening.SendsNow(this.OpeningMessage, this.conversation, this.openedChatId == this.ChatId))
|
||||
{
|
||||
this.openedChatId = this.ChatId;
|
||||
_ = this.SendOpeningMessageAsync(this.OpeningMessage!);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
this.ChatService.ConversationChanged -= this.OnConversationChangedAsync;
|
||||
this.countCancellation.Cancel();
|
||||
this.countCancellation.Dispose();
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Renders the chat again when the service changed it, and counts again once an answer is complete.
|
||||
/// </summary>
|
||||
/// <param name="chatId">The ID of the chat which changed.</param>
|
||||
private Task OnConversationChangedAsync(Guid chatId)
|
||||
{
|
||||
if (chatId != this.ChatId)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return this.InvokeAsync(async () =>
|
||||
{
|
||||
if (!this.IsRunning)
|
||||
await this.RecountAsync();
|
||||
|
||||
this.StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts what the conversation carries so far, while no answer runs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The system prompt counts as the chat stores it: what PrepareAsync adds for a request is not known before. The
|
||||
/// count is therefore an estimate, and the message being written is left out.
|
||||
/// </remarks>
|
||||
private async Task RecountAsync()
|
||||
{
|
||||
this.countedProviderId = this.Provider.Id;
|
||||
if (this.IsReadOnly || this.conversation is not { Thread: { } thread, IsRunning: false } || this.ProviderIssue is not null || this.Provider.UsedLLMProvider is LLMProviders.NONE)
|
||||
{
|
||||
this.tokens = ConversationTokens.UNAVAILABLE;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var parts = ConversationParts.Of(thread, thread.SystemPrompt, string.Empty, null, false, null);
|
||||
this.tokens = await this.TokenCounter.CountAsync(this.Provider, parts, thread.ReportedHistoryFor(this.Provider.Model), this.countToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// The component went away.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets about how many tokens the conversation carries, or null when nothing could be counted.
|
||||
/// </summary>
|
||||
private string? TokenText => this.tokens.IsKnown
|
||||
? string.Format(T("approx. {0} tokens"), TokenAmount.Format(this.tokens.Tokens, Tools.PluginSystem.I18N.I.Culture))
|
||||
: null;
|
||||
|
||||
private Task AttachmentsChanged(HashSet<FileAttachment> changedAttachments)
|
||||
{
|
||||
this.attachments = changedAttachments;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the message of the user, with its attachments.
|
||||
/// </summary>
|
||||
private async Task SendAsync()
|
||||
{
|
||||
if (!this.CanSendMessage)
|
||||
return;
|
||||
|
||||
var message = new ContentText { Text = this.userInput.Trim(), FileAttachments = [.. this.attachments] };
|
||||
this.userInput = string.Empty;
|
||||
this.attachments = [];
|
||||
await this.ChatService.SendAsync(this.ChatId, this.RequestOf(message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the opening message. When it cannot be sent, e.g., because the provider may not answer, the message waits
|
||||
/// in the field instead, so that the user can send it once the reason is gone.
|
||||
/// </summary>
|
||||
/// <param name="message">The opening message.</param>
|
||||
private async Task SendOpeningMessageAsync(ContentText message)
|
||||
{
|
||||
if (this.CanSend && await this.ChatService.SendAsync(this.ChatId, this.RequestOf(message)))
|
||||
return;
|
||||
|
||||
await this.InvokeAsync(() =>
|
||||
{
|
||||
this.userInput = message.Text;
|
||||
this.attachments = [.. message.FileAttachments];
|
||||
this.StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the request which sends a message with the provider and the preparation of this chat.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
private EmbeddedChatRequest RequestOf(ContentText message) => new()
|
||||
{
|
||||
Provider = this.Provider,
|
||||
Message = message,
|
||||
HidesModelIdentity = this.HidesModelIdentity,
|
||||
PrepareAsync = this.PrepareAsync,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Sends the message when the user releases the shortcut they chose for the chat.
|
||||
/// </summary>
|
||||
/// <param name="keyEvent">The key released.</param>
|
||||
private async Task InputKeyEventAsync(KeyboardEventArgs keyEvent)
|
||||
{
|
||||
if (this.SettingsManager.ConfigurationData.Chat.ShortcutSendBehavior.IsSendShortcut(keyEvent))
|
||||
await this.SendAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the answer the model writes.
|
||||
/// </summary>
|
||||
private Task CancelAsync() => this.ChatService.CancelAsync(this.ChatId);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Wrap="Wrap.NoWrap">
|
||||
@* ProviderSelection marks its select as flex-grow-0, and that utility is declared !important, so StretchItems
|
||||
cannot widen it. The width has to come from here. *@
|
||||
<div class="flex-grow-1">
|
||||
<ProviderSelection ProviderSettings="@this.ProviderSettings" ProviderSettingsChanged="@this.ProviderSettingsChanged" ValidateProvider="@this.ValidateProvider"/>
|
||||
</div>
|
||||
@if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)
|
||||
{
|
||||
<ConfidenceInfo LLMProvider="@this.ProviderSettings.UsedLLMProvider"/>
|
||||
}
|
||||
</MudStack>
|
||||
@@ -0,0 +1,32 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A provider selection with the confidence of the selected provider next to it, for a page which lets the user choose
|
||||
/// several providers: the confidence card in the footer of an assistant knows only one of them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The confidence shows only when the user wants to see it, see DataConfidence.ShowProviderConfidence. Like
|
||||
/// ProviderSelection, this takes the component from the cascading parameter of its page.
|
||||
/// </remarks>
|
||||
public partial class ProviderSelectionWithConfidence : MSGComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the selected provider.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public AIStudio.Settings.Provider ProviderSettings { get; set; } = AIStudio.Settings.Provider.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets what happens when the user selects another provider.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<AIStudio.Settings.Provider> ProviderSettingsChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the validation of the selected provider, which returns an issue or null.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<AIStudio.Settings.Provider, string?> ValidateProvider { get; set; } = _ => null;
|
||||
}
|
||||
@@ -15,6 +15,17 @@ public partial class ToolSelection : MSGComponentBase
|
||||
[Parameter]
|
||||
public required AIStudio.Settings.Provider LLMProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Further models which work with the same tools, e.g., the second model of a comparison series.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The selection then offers only what every model may use: tools only when each of them can call tools, and each
|
||||
/// tool by the lowest confidence among them, see SharedSelectionRules. Without further models, it offers what
|
||||
/// LLMProvider alone may use.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public IReadOnlyList<AIStudio.Settings.Provider> AdditionalProviders { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Where the chat of this selection may still send data, because of the mailboxes it read from.
|
||||
/// </summary>
|
||||
@@ -60,7 +71,12 @@ public partial class ToolSelection : MSGComponentBase
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private ToolCallingAvailability ToolCallingAvailability => this.LLMProvider.GetToolCallingAvailability();
|
||||
/// <summary>
|
||||
/// Every model which works with the selected tools.
|
||||
/// </summary>
|
||||
private IEnumerable<AIStudio.Settings.Provider> AllProviders => [this.LLMProvider, .. this.AdditionalProviders];
|
||||
|
||||
private ToolCallingAvailability ToolCallingAvailability => SharedSelectionRules.ToolCallingOfAll(this.AllProviders.Select(provider => provider.GetToolCallingAvailability()));
|
||||
|
||||
private bool SupportsTools => this.ToolCallingAvailability.IsAvailable;
|
||||
|
||||
@@ -70,9 +86,12 @@ public partial class ToolSelection : MSGComponentBase
|
||||
|
||||
private string UnsupportedToolsMessage => this.ToolCallingAvailability.Message;
|
||||
|
||||
private ConfidenceLevel ProviderConfidence => this.LLMProvider == AIStudio.Settings.Provider.NONE
|
||||
/// <summary>
|
||||
/// The confidence which counts for the tools: the lowest of all models, see SharedSelectionRules.LowestConfidence.
|
||||
/// </summary>
|
||||
private ConfidenceLevel ProviderConfidence => SharedSelectionRules.LowestConfidence(this.AllProviders.Select(provider => provider == AIStudio.Settings.Provider.NONE
|
||||
? ConfidenceLevel.NONE
|
||||
: this.LLMProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level;
|
||||
: provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level));
|
||||
|
||||
private async Task ToggleSelection()
|
||||
{
|
||||
@@ -193,7 +212,9 @@ public partial class ToolSelection : MSGComponentBase
|
||||
return null;
|
||||
|
||||
return string.Format(
|
||||
this.T("This tool requires provider confidence {0}. The selected provider has {1}."),
|
||||
this.AdditionalProviders.Count == 0
|
||||
? this.T("This tool requires provider confidence {0}. The selected provider has {1}.")
|
||||
: this.T("This tool requires provider confidence {0}. The selected provider with the lowest confidence has {1}."),
|
||||
GetMinimumProviderConfidence(item).Name,
|
||||
this.ProviderConfidence.Name);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user