Added a semantic search tool (#1005)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-09-27 16:26:52 +02:00
1 parent 4baf21656a
commit 1eaca9b12f
91 files changed
+4248 -487

No files matched your search

@@ -794,6 +794,7 @@ public partial class ChatComponent : MSGComponentBase
return left.DisableDataSources == right.DisableDataSources
&& left.AutomaticDataSourceSelection == right.AutomaticDataSourceSelection
&& left.AutomaticValidation == right.AutomaticValidation
&& left.RetrievalMode == right.RetrievalMode
&& left.PreselectedDataSourceIds.ToHashSet(StringComparer.Ordinal).SetEquals(right.PreselectedDataSourceIds);
}
@@ -1607,6 +1608,13 @@ public partial class ChatComponent : MSGComponentBase
var parts = ConversationParts.NOTHING;
var reported = ReportedHistory.UNKNOWN;
//
// Semantic Search offers itself rather than being selected, and the registry answers
// whether it can be offered asynchronously. So this is asked before collecting, which only
// reads the provider and the choice of the chat, nothing a background job appends to:
//
var offersSemanticSearch = await this.OffersSemanticSearchAsync();
//
// Collected on the render thread, counted off it. Counting may take an IPC call per text,
// and while it runs, the background job which writes the answer appends to the very list
@@ -1621,7 +1629,7 @@ public partial class ChatComponent : MSGComponentBase
// of it would tell a person their window is empty while their first message is not.
//
var thread = this.ChatThread ?? this.NewChatThread(string.Empty);
var toolDefinitions = this.GetRunnableToolDefinitions();
var toolDefinitions = this.GetRunnableToolDefinitions(offersSemanticSearch);
provider = this.Provider;
parts = ConversationParts.Of(thread, this.BuildSystemPromptFor(thread, toolDefinitions), this.UserInput, this.ComposerState.FileAttachments, provider.SupportsImageInput(), toolDefinitions);
reported = thread.ReportedHistoryFor(provider.Model);
@@ -1665,13 +1673,45 @@ public partial class ChatComponent : MSGComponentBase
/// Asked for once and used twice: their policy goes into the system prompt, and their schemas
/// travel next to it in the request body. Both cost tokens, and both change the moment somebody
/// switches a tool on.
///
/// Semantic Search is no selected tool, so it comes on top when it is offered. It counts with
/// its static definition: the one a request offers lists the data sources as well, which only
/// the request asks for.
/// </remarks>
/// <returns>The definitions of the selected tools.</returns>
private IReadOnlyList<ToolDefinition> GetRunnableToolDefinitions() => this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds)
.Select(this.ToolRegistry.GetDefinition)
.Where(definition => definition is not null)
.Select(definition => definition!)
.ToList();
/// <param name="offersSemanticSearch">Whether the next request offers Semantic Search, see OffersSemanticSearchAsync.</param>
/// <returns>The definitions of the selected tools, and of Semantic Search when it is offered.</returns>
private IReadOnlyList<ToolDefinition> GetRunnableToolDefinitions(bool offersSemanticSearch)
{
var definitions = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds)
.Select(this.ToolRegistry.GetDefinition)
.Where(definition => definition is not null)
.Select(definition => definition!)
.ToList();
if (offersSemanticSearch && this.ToolRegistry.GetDefinition(ToolSelectionRules.SEMANTIC_SEARCH_TOOL_ID) is { } semanticSearch)
definitions.Add(semanticSearch);
return definitions;
}
/// <summary>
/// Whether the next request offers the model Semantic Search, as far as this can be told without asking the data sources.
/// </summary>
/// <remarks>
/// An estimate on purpose. Whether a data source can be searched right now would mean asking
/// every ERI server with every count, and the count runs all the time. Once the first answer is
/// there, the number the provider reported takes over anyway, see ChatThread.ReportedHistoryFor.
/// </remarks>
/// <returns>True when the next request offers Semantic Search, as far as can be told.</returns>
private async Task<bool> OffersSemanticSearchAsync()
{
var options = this.GetCurrentDataSourceOptions();
if (!PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager) || !options.IsEnabled())
return false;
var retrievalMode = await this.ToolRegistry.GetEffectiveRetrievalModeAsync(options, this.Provider, Tools.Components.CHAT);
return retrievalMode.Mode is DataSourceRetrievalMode.SEMANTIC_SEARCH;
}
/// <summary>
/// The thread a new chat starts with, as the selections made so far decide it.
@@ -60,9 +60,22 @@
<MudTextSwitch Label="@T("Are data sources enabled?")" Value="@this.areDataSourcesEnabled" LabelOn="@T("Yes, I want to use data sources.")" LabelOff="@T("No, I don't want to use data sources.")" ValueChanged="@this.EnabledChanged" Dense="@true"/>
@if (this.areDataSourcesEnabled)
{
@* Where Semantic Search cannot be used, there is no choice to make, only the reason to tell: *@
@if (this.IsSemanticSearchUnavailable)
{
<MudJustifiedText Typo="Typo.body2" Color="Color.Info" Class="mb-2">
@this.GetSemanticSearchUnavailableMessage()
</MudJustifiedText>
}
else
{
<MudTextSwitch Label="@T("Semantic Search")" Value="@this.IsSemanticSearchPreferred" LabelOn="@T("Yes, the AI searches your data sources itself, whenever a question calls for it (Semantic Search).")" LabelOff="@T("No, AI Studio searches your data sources with every message, before the AI answers (classic RAG).")" ValueChanged="@this.SemanticSearchChanged" Dense="@true"/>
}
<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"/>
@if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
@* With Semantic Search, no agent takes part: nothing would validate the retrieved data. *@
@if (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"/>
}
@@ -75,12 +88,6 @@
</MudText>
break;
case true when this.DataSourcesAISelected.Count == 0:
<MudText Typo="Typo.body2" Class="mb-2">
@T("The AI evaluates each of your inputs to determine whether and which data sources are necessary. Currently, the AI has not selected any source.")
</MudText>
break;
case false when this.GetListedDataSources().Count == 0:
<MudText Typo="Typo.body2" Class="mb-2">
@T("Your data sources cannot be used with the selected providers due to data privacy or confidence-level requirements, or they are currently unavailable.")
@@ -99,6 +106,19 @@
break;
case true:
@if (this.IsSemanticSearchEffective)
{
<MudText Typo="Typo.body2" Class="mb-2">
@T("Whenever a question calls for it, the AI picks the fitting ones among these data sources itself.")
</MudText>
}
else if (this.DataSourcesAISelected.Count == 0)
{
<MudText Typo="Typo.body2" Class="mb-2">
@T("The AI evaluates each of your inputs to determine whether and which data sources are necessary. Currently, the AI has not selected any source.")
</MudText>
}
<MudExpansionPanels MultiExpansion="@false" Class="mt-3" Style="max-height: 14em;">
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TouchApp" IconSize="Size.Small" HeaderTypo="Typo.subtitle1" HeaderClass="expansion-panel-header-compact" HeaderText="@T("Available Data Sources")">
<MudList T="IDataSource" Dense="@true" Class="data-source-rows" SelectionMode="MudBlazor.SelectionMode.SingleSelection" SelectedValues="@this.selectedDataSources" Style="max-height: 14em;">
@@ -108,34 +128,42 @@
}
</MudList>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Filter" IconSize="Size.Small" HeaderTypo="Typo.subtitle1" HeaderClass="expansion-panel-header-compact" HeaderText="@T("AI-Selected Data Sources")">
<MudList T="DataSourceAgentSelected" Dense="@true" Class="data-source-rows" SelectionMode="MudBlazor.SelectionMode.MultiSelection" ReadOnly="@true" SelectedValues="@this.GetSelectedDataSourcesWithAI()" Style="max-height: 14em;">
@foreach (var source in this.DataSourcesAISelected)
{
<MudListItem Value="@source">
<ChildContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Style="min-width: 0; width: 100%;">
<MudText Typo="Typo.body2" Style="min-width: 0; white-space: normal; overflow-wrap: anywhere;">
@source.DataSource.Name
</MudText>
@if (source.DataSource is IInternalDataSource internalSource)
{
<MudSpacer/>
<MudTooltip Text="@internalSource.ConfidenceLevel.GetName()">
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Small" Class="confidence-icon" Style="@this.GetConfidenceIconStyle(internalSource)"/>
</MudTooltip>
}
</MudStack>
@*
With Semantic Search, no agent selects: the chat model picks with each search, and its
searches show in the answer. With classic RAG, there is something to show once the agent
picked for a message.
*@
@if (!this.IsSemanticSearchEffective && this.DataSourcesAISelected.Count > 0)
{
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Filter" IconSize="Size.Small" HeaderTypo="Typo.subtitle1" HeaderClass="expansion-panel-header-compact" HeaderText="@T("AI-Selected Data Sources")">
<MudList T="DataSourceAgentSelected" Dense="@true" Class="data-source-rows" SelectionMode="MudBlazor.SelectionMode.MultiSelection" ReadOnly="@true" SelectedValues="@this.GetSelectedDataSourcesWithAI()" Style="max-height: 14em;">
@foreach (var source in this.DataSourcesAISelected)
{
<MudListItem Value="@source">
<ChildContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Style="min-width: 0; width: 100%;">
<MudText Typo="Typo.body2" Style="min-width: 0; white-space: normal; overflow-wrap: anywhere;">
@source.DataSource.Name
</MudText>
@if (source.DataSource is IInternalDataSource internalSource)
{
<MudSpacer/>
<MudTooltip Text="@internalSource.ConfidenceLevel.GetName()">
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Small" Class="confidence-icon" Style="@this.GetConfidenceIconStyle(internalSource)"/>
</MudTooltip>
}
</MudStack>
<MudProgressLinear Color="Color.Info" Min="0" Max="1" Value="@source.AIDecision.Confidence"/>
<MudJustifiedText Typo="Typo.body2">
@(this.GetAIReasoning(source))
</MudJustifiedText>
</ChildContent>
</MudListItem>
}
</MudList>
</ExpansionPanel>
<MudProgressLinear Color="Color.Info" Min="0" Max="1" Value="@source.AIDecision.Confidence"/>
<MudJustifiedText Typo="Typo.body2">
@(this.GetAIReasoning(source))
</MudJustifiedText>
</ChildContent>
</MudListItem>
}
</MudList>
</ExpansionPanel>
}
</MudExpansionPanels>
break;
}
@@ -182,8 +210,24 @@ else if (this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE)
<MudTextSwitch Label="@T("Are data sources enabled?")" Value="@this.areDataSourcesEnabled" LabelOn="@T("Yes, I want to use data sources.")" LabelOff="@T("No, I don't want to use data sources.")" ValueChanged="@this.EnabledChanged" Disabled="@(this.ReadOnly || this.IsPreselectedDataSourcesDisabledLocked())"/>
@if (this.areDataSourcesEnabled)
{
<MudTextSwitch Label="@T("Semantic Search")" Value="@this.IsSemanticSearchPreferred" LabelOn="@T("Yes, the AI searches your data sources itself, whenever a question calls for it (Semantic Search).")" LabelOff="@T("No, AI Studio searches your data sources with every message, before the AI answers (classic RAG).")" ValueChanged="@this.SemanticSearchChanged" Disabled="@(this.ReadOnly || this.IsPreselectedDataSourcesRetrievalModeLocked())"/>
@* No provider is known here, so this says in general what the chat itself tells about its model: *@
@if (this.IsSemanticSearchPreferred)
{
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
@T("When the model of a chat cannot use tools, AI Studio uses classic RAG instead and searches your data sources with every message.")
</MudJustifiedText>
}
<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" Disabled="@(this.ReadOnly || this.IsPreselectedDataSourcesAutomaticSelectionLocked())"/>
<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" Disabled="@(this.ReadOnly || this.IsPreselectedDataSourcesAutomaticValidationLocked())"/>
@* Semantic Search may fall back to searching with every message, so the validation is kept, marked as belonging to that way only: *@
<MudPaper Class="pa-3 mb-3 border-dashed border rounded-lg">
<MudJustifiedText Typo="Typo.body2" Class="mb-2">
@T("Only used with classic RAG, which searches your data sources with every message:")
</MudJustifiedText>
<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" Disabled="@(this.ReadOnly || this.IsPreselectedDataSourcesAutomaticValidationLocked())"/>
</MudPaper>
<MudField Label="@T("Available Data Sources")" Variant="Variant.Outlined" Class="mb-3" Disabled="@(this.ReadOnly || this.aiBasedSourceSelection || this.IsPreselectedDataSourceIdsLocked())">
<MudList T="IDataSource" Dense="@true" Class="data-source-rows" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" ReadOnly="@(this.ReadOnly || this.IsPreselectedDataSourceIdsLocked())">
@*
@@ -3,6 +3,7 @@ using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
using AIStudio.Tools.ToolCallingSystem;
using Microsoft.AspNetCore.Components;
@@ -62,7 +63,10 @@ public partial class DataSourceSelection : MSGComponentBase
[Inject]
private DataSourceService DataSourceService { get; init; } = null!;
[Inject]
private ToolRegistry ToolRegistry { get; init; } = null!;
[Inject]
private IDialogService DialogService { get; init; } = null!;
@@ -78,6 +82,8 @@ public partial class DataSourceSelection : MSGComponentBase
private bool aiBasedSourceSelection;
private bool aiBasedValidation;
private bool areDataSourcesEnabled;
private DataSourceRetrievalMode retrievalMode;
private EffectiveRetrievalMode effectiveRetrievalMode;
private uint loadAndApplyFiltersGeneration;
#region Overrides of ComponentBase
@@ -92,6 +98,7 @@ public partial class DataSourceSelection : MSGComponentBase
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
this.retrievalMode = this.DataSourceOptions.RetrievalMode;
this.waitingForDataSources = this.areDataSourcesEnabled && this.SelectionMode is not DataSourceSelectionMode.CONFIGURATION_MODE;
//
@@ -113,6 +120,7 @@ public partial class DataSourceSelection : MSGComponentBase
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
this.retrievalMode = this.DataSourceOptions.RetrievalMode;
this.selectedDataSources = this.GetDataSourcesFromConfiguredIds();
}
@@ -178,6 +186,7 @@ public partial class DataSourceSelection : MSGComponentBase
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
this.retrievalMode = this.DataSourceOptions.RetrievalMode;
this.selectedDataSources = this.GetDataSourcesFromConfiguredIds();
this.waitingForDataSources = false;
@@ -246,10 +255,16 @@ public partial class DataSourceSelection : MSGComponentBase
// that field holds what was usable the last time we looked, so a source filtered out once
// would never come back, while the RAG process keeps reading it from the preselection.
//
var sources = await this.DataSourceService.GetDataSources(this.LLMProvider, this.DataSourceOptions, this.GetDataSourcesFromConfiguredIds());
// 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:
//
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());
if (generation != this.loadAndApplyFiltersGeneration)
return;
this.effectiveRetrievalMode = effectiveMode;
this.availableDataSources = sources.AllowedDataSources;
this.dataSourcesAwaitingReindex = sources.DataSourcesAwaitingReindex;
this.dataSourceIdsAwaitingReindex = sources.DataSourcesAwaitingReindex.Select(source => source.Id).ToHashSet(StringComparer.Ordinal);
@@ -326,6 +341,47 @@ public partial class DataSourceSelection : MSGComponentBase
await this.OptionsChanged();
}
private async Task SemanticSearchChanged(bool state)
{
this.retrievalMode = state ? DataSourceRetrievalMode.SEMANTIC_SEARCH : DataSourceRetrievalMode.EVERY_MESSAGE;
this.DataSourceOptions.RetrievalMode = this.retrievalMode;
// Which data sources the provider may use depends on it, see LoadAndApplyFilters:
await this.LoadAndApplyFilters();
await this.OptionsChanged();
}
private bool IsSemanticSearchPreferred => this.retrievalMode is DataSourceRetrievalMode.SEMANTIC_SEARCH;
/// <summary>
/// Whether the model of the chat searches the data sources itself.
/// </summary>
/// <remarks>
/// Only the selection mode can tell, since only it knows the provider of a chat. The effective
/// mode is found out while loading the data sources; asking the preference as well keeps a
/// switch to every message from showing the other way until that loading is done.
/// </remarks>
private bool IsSemanticSearchEffective => this.IsSemanticSearchPreferred && this.effectiveRetrievalMode.Mode is DataSourceRetrievalMode.SEMANTIC_SEARCH;
/// <summary>
/// Whether Semantic Search cannot be used in this chat, whatever the user prefers.
/// </summary>
/// <remarks>
/// Then there is nothing to choose: classic RAG searches the data sources. The choice is left
/// out rather than shown as one which changes nothing, and the preference stays as it is, so it
/// is back the moment the chat gets a model which can search itself.
/// </remarks>
private bool IsSemanticSearchUnavailable => this.effectiveRetrievalMode.SemanticSearchBlockReason is not ToolOfferBlockReason.NONE;
private string GetSemanticSearchUnavailableMessage() => this.effectiveRetrievalMode.SemanticSearchBlockReason switch
{
ToolOfferBlockReason.TOOLS_SWITCHED_OFF => T("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 => T("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 => T("Your organization has switched Semantic Search off, so AI Studio uses classic RAG instead and searches your data sources with every message."),
ToolOfferBlockReason.PROVIDER_CONFIDENCE_TOO_LOW => T("The selected provider is not trusted enough for Semantic Search, so AI Studio uses classic RAG instead and searches your data sources with every message."),
_ => T("Semantic Search cannot be used here, so AI Studio uses classic RAG instead and searches your data sources with every message."),
};
private async Task ValidationModeChanged(bool state)
{
this.aiBasedValidation = state;
@@ -360,6 +416,14 @@ public partial class DataSourceSelection : MSGComponentBase
&& meta.IsLocked;
}
private bool IsPreselectedDataSourcesRetrievalModeLocked()
{
return this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE
&& this.ConfiguresChatDefaults
&& ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedDataSourcesRetrievalMode, out var meta)
&& meta.IsLocked;
}
private bool IsPreselectedDataSourcesAutomaticSelectionLocked()
{
return this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE
@@ -1,5 +1,6 @@
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools.ToolCallingSystem;
using Microsoft.AspNetCore.Components;
@@ -100,6 +101,10 @@ public partial class ProviderSelection : MSGComponentBase
if (profile.Has(Capability.SPEECH_INPUT))
capabilityIcons.Add(new(Icons.Material.Filled.Mic, this.T("Speech input possible")));
// The same check which decides whether a request offers tools, Semantic Search among them:
if (provider.GetToolCallingAvailability().IsAvailable)
capabilityIcons.Add(new(Icons.Material.Filled.Build, this.T("Tool calling possible")));
var reasoningIndicatorState = provider.GetReasoningIndicatorState();
if (reasoningIndicatorState is not ReasoningIndicatorState.NONE)
capabilityIcons.Add(new(Icons.Material.Filled.Psychology, this.GetReasoningTooltip(reasoningIndicatorState)));