Store the AI-selected data sources to the chat thread so that the user can see the selection

This commit is contained in:
Thorsten Sommer 2025-02-17 12:31:55 +01:00
parent 0887ae8976
commit 5d17068e66
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
8 changed files with 137 additions and 25 deletions

View File

@ -1,3 +1,4 @@
using AIStudio.Components;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
@ -33,6 +34,11 @@ public sealed record ChatThread
/// </summary>
public DataSourceOptions DataSourceOptions { get; set; } = new();
/// <summary>
/// The AI-selected data sources for this chat thread.
/// </summary>
public IReadOnlyList<DataSourceAgentSelected> AISelectedDataSources { get; set; } = [];
/// <summary>
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
/// </summary>

View File

@ -1,6 +1,7 @@
using System.Text.Json.Serialization;
using AIStudio.Agents;
using AIStudio.Components;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools.Services;
@ -74,6 +75,7 @@ public sealed class ContentText : IContent
var selectionAgent = Program.SERVICE_PROVIDER.GetService<AgentDataSourceSelection>()!;
// Let the AI agent do its work:
IReadOnlyList<DataSourceAgentSelected> finalAISelection = [];
var aiSelectedDataSources = await selectionAgent.PerformSelectionAsync(provider, lastPrompt, chatThread, dataSources, token);
// Check if the AI selected any data sources:
@ -81,6 +83,11 @@ public sealed class ContentText : IContent
{
logger.LogWarning("The AI did not select any data sources. The RAG process is skipped.");
proceedWithRAG = false;
// Send the selected data sources to the data source selection component.
// Then, the user can see which data sources were selected by the AI.
await MessageBus.INSTANCE.SendMessage(null, Event.RAG_AUTO_DATA_SOURCES_SELECTED, finalAISelection);
chatThread.AISelectedDataSources = finalAISelection;
}
else
{
@ -96,6 +103,9 @@ public sealed class ContentText : IContent
// Filter out the data sources that are not available:
aiSelectedDataSources = aiSelectedDataSources.Where(x => settings.ConfigurationData.DataSources.FirstOrDefault(ds => ds.Id == x.Id) is not null).ToList();
// Store the real AI-selected data sources:
finalAISelection = aiSelectedDataSources.Select(x => new DataSourceAgentSelected { DataSource = settings.ConfigurationData.DataSources.First(ds => ds.Id == x.Id), AIDecision = x, Selected = false }).ToList();
var numHallucinatedSources = totalAISelectedDataSources - aiSelectedDataSources.Count;
if(numHallucinatedSources > 0)
logger.LogWarning($"The AI hallucinated {numHallucinatedSources} data source(s). We ignore them.");
@ -142,6 +152,10 @@ public sealed class ContentText : IContent
// Filter the data sources by the threshold:
//
aiSelectedDataSources = aiSelectedDataSources.Where(x => x.Confidence >= threshold).ToList();
foreach (var dataSource in finalAISelection)
if(aiSelectedDataSources.Any(x => x.Id == dataSource.DataSource.Id))
dataSource.Selected = true;
logger.LogInformation($"The AI selected {aiSelectedDataSources.Count} data source(s) with a confidence of at least {threshold}.");
// Transform the final data sources to the actual data sources:
@ -151,12 +165,18 @@ public sealed class ContentText : IContent
// We have max. 3 data sources. We take all of them:
else
{
// Transform the selected data sources to the actual data sources.
// Transform the selected data sources to the actual data sources:
selectedDataSources = aiSelectedDataSources.Select(x => settings.ConfigurationData.DataSources.FirstOrDefault(ds => ds.Id == x.Id)).Where(ds => ds is not null).ToList()!;
// Mark the data sources as selected:
foreach (var dataSource in finalAISelection)
dataSource.Selected = true;
}
// Store the changes in the chat thread:
chatThread.DataSourceOptions.PreselectedDataSourceIds = selectedDataSources.Select(ds => ds.Id).ToList();
// Send the selected data sources to the data source selection component.
// Then, the user can see which data sources were selected by the AI.
await MessageBus.INSTANCE.SendMessage(null, Event.RAG_AUTO_DATA_SOURCES_SELECTED, finalAISelection);
chatThread.AISelectedDataSources = finalAISelection;
}
}
else

View File

@ -111,7 +111,7 @@
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
{
<DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.BUTTON" PopoverButtonClasses="ma-3" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@(async options => await this.SetCurrentDataSourceOptions(options))"/>
<DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.BUTTON" PopoverButtonClasses="ma-3" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@(async options => await this.SetCurrentDataSourceOptions(options))" DataSourcesAISelected="@this.GetAgentSelectedDataSources()"/>
}
</MudToolBar>
</FooterContent>

View File

@ -305,6 +305,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
}
private IReadOnlyList<DataSourceAgentSelected> GetAgentSelectedDataSources()
{
if (this.ChatThread is null)
return [];
return this.ChatThread.AISelectedDataSources;
}
private DataSourceOptions GetCurrentDataSourceOptions()
{
if (this.ChatThread is not null)
@ -482,12 +490,6 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Disable the stream state:
this.isStreaming = false;
// Update the data source options. This is useful when
// the AI is responsible for selecting the data source.
// The user can then see the selected data source:
if(this.ChatThread?.DataSourceOptions.AutomaticDataSourceSelection ?? false)
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread!.DataSourceOptions);
// Update the UI:
this.StateHasChanged();
}
@ -682,7 +684,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions);
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
}
else
{

View File

@ -0,0 +1,25 @@
using AIStudio.Agents;
using AIStudio.Settings;
namespace AIStudio.Components;
/// <summary>
/// A data structure to combine the data source and the underlying AI decision.
/// </summary>
public sealed class DataSourceAgentSelected
{
/// <summary>
/// The data source.
/// </summary>
public required IDataSource DataSource { get; set; }
/// <summary>
/// The AI decision, which led to the selection of the data source.
/// </summary>
public required SelectedDataSource AIDecision { get; set; }
/// <summary>
/// Indicates whether the data source is part of the final selection for the RAG process.
/// </summary>
public bool Selected { get; set; }
}

View File

@ -24,7 +24,7 @@
<MudText Typo="Typo.h5">Data Source Selection</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Style="max-height: 60vh; overflow: auto;">
<MudCardContent Style="min-width: 24em; max-height: 60vh; max-width: 45vw; overflow: auto;">
@if (this.waitingForDataSources)
{
<MudSkeleton Width="30%" Height="42px;"/>
@ -38,8 +38,11 @@
{
<MudTextSwitch Label="AI-based data source selection" Value="@this.aiBasedSourceSelection" LabelOn="Yes, let the AI decide which data sources are needed." LabelOff="No, I manually decide which data source to use." ValueChanged="@this.AutoModeChanged"/>
<MudTextSwitch Label="AI-based data validation" Value="@this.aiBasedValidation" LabelOn="Yes, let the AI validate & filter the retrieved data." LabelOff="No, use all data retrieved from the data sources." ValueChanged="@this.ValidationModeChanged"/>
@if (this.aiBasedSourceSelection is false || this.DataSourcesAISelected.Count == 0)
{
<MudField Label="Available Data Sources" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
<MudList T="IDataSource" SelectionMode="MudBlazor.SelectionMode.MultiSelection" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" Style="max-height: 14em;">
<MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" Style="max-height: 14em;">
@foreach (var source in this.availableDataSources)
{
<MudListItem Value="@source">
@ -49,6 +52,41 @@
</MudList>
</MudField>
}
else
{
<MudExpansionPanels MultiExpansion="@false" Class="mt-3" Style="max-height: 14em;">
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TouchApp" HeaderText="Available Data Sources">
<MudList T="IDataSource" SelectionMode="MudBlazor.SelectionMode.SingleSelection" SelectedValues="@this.selectedDataSources" Style="max-height: 14em;">
@foreach (var source in this.availableDataSources)
{
<MudListItem Value="@source">
@source.Name
</MudListItem>
}
</MudList>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Filter" HeaderText="AI-Selected Data Sources">
<MudList T="DataSourceAgentSelected" SelectionMode="MudBlazor.SelectionMode.MultiSelection" ReadOnly="@true" SelectedValues="@this.GetSelectedDataSourcesWithAI()" Style="max-height: 14em;">
@foreach (var source in this.DataSourcesAISelected)
{
<MudListItem Value="@source">
<ChildContent>
<MudText Typo="Typo.body1">
@source.DataSource.Name
</MudText>
<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>
}
}
}
</MudCardContent>
<MudCardActions>
@ -79,7 +117,7 @@ else if (this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE)
<MudTextSwitch Label="AI-based data source selection" Value="@this.aiBasedSourceSelection" LabelOn="Yes, let the AI decide which data sources are needed." LabelOff="No, I manually decide which data source to use." ValueChanged="@this.AutoModeChanged"/>
<MudTextSwitch Label="AI-based data validation" Value="@this.aiBasedValidation" LabelOn="Yes, let the AI validate & filter the retrieved data." LabelOff="No, use all data retrieved from the data sources." ValueChanged="@this.ValidationModeChanged"/>
<MudField Label="Available Data Sources" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
<MudList T="IDataSource" SelectionMode="MudBlazor.SelectionMode.MultiSelection" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))">
<MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))">
@foreach (var source in this.availableDataSources)
{
<MudListItem Value="@source">

View File

@ -26,6 +26,9 @@ public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, I
[Parameter]
public EventCallback<DataSourceOptions> DataSourceOptionsChanged { get; set; }
[Parameter]
public IReadOnlyList<DataSourceAgentSelected> DataSourcesAISelected { get; set; } = [];
[Parameter]
public string ConfigurationHeaderMessage { get; set; } = string.Empty;
@ -58,7 +61,7 @@ public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, I
protected override async Task OnInitializedAsync()
{
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED ]);
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED, Event.RAG_AUTO_DATA_SOURCES_SELECTED ]);
//
// Load the settings:
@ -129,9 +132,17 @@ public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, I
#endregion
public void ChangeOptionWithoutSaving(DataSourceOptions options)
private SelectionMode GetListSelectionMode() => this.aiBasedSourceSelection ? MudBlazor.SelectionMode.SingleSelection : MudBlazor.SelectionMode.MultiSelection;
private IReadOnlyCollection<DataSourceAgentSelected> GetSelectedDataSourcesWithAI() => this.DataSourcesAISelected.Where(n => n.Selected).ToList();
private string GetAIReasoning(DataSourceAgentSelected source) => $"AI reasoning (confidence {source.AIDecision.Confidence:P0}): {source.AIDecision.Reason}";
public void ChangeOptionWithoutSaving(DataSourceOptions options, IReadOnlyList<DataSourceAgentSelected>? aiSelectedDataSources = null)
{
this.DataSourceOptions = options;
this.DataSourcesAISelected = aiSelectedDataSources ?? [];
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
@ -237,6 +248,13 @@ public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, I
this.showDataSourceSelection = false;
this.StateHasChanged();
break;
case Event.RAG_AUTO_DATA_SOURCES_SELECTED:
if(data is IReadOnlyList<DataSourceAgentSelected> aiSelectedDataSources)
this.DataSourcesAISelected = aiSelectedDataSources;
this.StateHasChanged();
break;
}
return Task.CompletedTask;

View File

@ -23,6 +23,9 @@ public enum Event
WORKSPACE_LOADED_CHAT_CHANGED,
WORKSPACE_TOGGLE_OVERLAY,
// RAG events:
RAG_AUTO_DATA_SOURCES_SELECTED,
// Send events:
SEND_TO_GRAMMAR_SPELLING_ASSISTANT,
SEND_TO_ICON_FINDER_ASSISTANT,