diff --git a/app/MindWork AI Studio/Chat/ChatThread.cs b/app/MindWork AI Studio/Chat/ChatThread.cs index 3e41161c..7709db7e 100644 --- a/app/MindWork AI Studio/Chat/ChatThread.cs +++ b/app/MindWork AI Studio/Chat/ChatThread.cs @@ -1,3 +1,4 @@ +using AIStudio.Components; using AIStudio.Settings; using AIStudio.Settings.DataModel; @@ -33,6 +34,11 @@ public sealed record ChatThread /// public DataSourceOptions DataSourceOptions { get; set; } = new(); + /// + /// The AI-selected data sources for this chat thread. + /// + public IReadOnlyList AISelectedDataSources { get; set; } = []; + /// /// The name of the chat thread. Usually generated by an AI model or manually edited by the user. /// diff --git a/app/MindWork AI Studio/Chat/ContentText.cs b/app/MindWork AI Studio/Chat/ContentText.cs index 8275c245..c3b4625a 100644 --- a/app/MindWork AI Studio/Chat/ContentText.cs +++ b/app/MindWork AI Studio/Chat/ContentText.cs @@ -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()!; // Let the AI agent do its work: + IReadOnlyList 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 diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor index 2ae6eb2a..06dd07b2 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor +++ b/app/MindWork AI Studio/Components/ChatComponent.razor @@ -111,7 +111,7 @@ @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) { - + } diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index 78721864..7a25680b 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -305,6 +305,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable await this.ChatThreadChanged.InvokeAsync(this.ChatThread); } + private IReadOnlyList 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 { diff --git a/app/MindWork AI Studio/Components/DataSourceAgentSelected.cs b/app/MindWork AI Studio/Components/DataSourceAgentSelected.cs new file mode 100644 index 00000000..5108596b --- /dev/null +++ b/app/MindWork AI Studio/Components/DataSourceAgentSelected.cs @@ -0,0 +1,25 @@ +using AIStudio.Agents; +using AIStudio.Settings; + +namespace AIStudio.Components; + +/// +/// A data structure to combine the data source and the underlying AI decision. +/// +public sealed class DataSourceAgentSelected +{ + /// + /// The data source. + /// + public required IDataSource DataSource { get; set; } + + /// + /// The AI decision, which led to the selection of the data source. + /// + public required SelectedDataSource AIDecision { get; set; } + + /// + /// Indicates whether the data source is part of the final selection for the RAG process. + /// + public bool Selected { get; set; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/DataSourceSelection.razor b/app/MindWork AI Studio/Components/DataSourceSelection.razor index f9dff17b..08160443 100644 --- a/app/MindWork AI Studio/Components/DataSourceSelection.razor +++ b/app/MindWork AI Studio/Components/DataSourceSelection.razor @@ -24,7 +24,7 @@ Data Source Selection - + @if (this.waitingForDataSources) { @@ -38,16 +38,54 @@ { - - - @foreach (var source in this.availableDataSources) - { - - @source.Name - - } - - + + @if (this.aiBasedSourceSelection is false || this.DataSourcesAISelected.Count == 0) + { + + + @foreach (var source in this.availableDataSources) + { + + @source.Name + + } + + + } + else + { + + + + @foreach (var source in this.availableDataSources) + { + + @source.Name + + } + + + + + @foreach (var source in this.DataSourcesAISelected) + { + + + + @source.DataSource.Name + + + + + @this.GetAIReasoning(source) + + + + } + + + + } } } @@ -79,7 +117,7 @@ else if (this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE) - + @foreach (var source in this.availableDataSources) { diff --git a/app/MindWork AI Studio/Components/DataSourceSelection.razor.cs b/app/MindWork AI Studio/Components/DataSourceSelection.razor.cs index 7b75bbd4..73796d6e 100644 --- a/app/MindWork AI Studio/Components/DataSourceSelection.razor.cs +++ b/app/MindWork AI Studio/Components/DataSourceSelection.razor.cs @@ -25,6 +25,9 @@ public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, I [Parameter] public EventCallback DataSourceOptionsChanged { get; set; } + + [Parameter] + public IReadOnlyList 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 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? 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 aiSelectedDataSources) + this.DataSourcesAISelected = aiSelectedDataSources; + + this.StateHasChanged(); + break; } return Task.CompletedTask; diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs index 37a855fd..45005fd1 100644 --- a/app/MindWork AI Studio/Tools/Event.cs +++ b/app/MindWork AI Studio/Tools/Event.cs @@ -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,