Count the agents only where the classic RAG process runs them

This commit is contained in:
Thorsten Sommer 2026-09-24 16:32:01 +02:00
parent abb4a41a71
commit a75ffc54a6
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
6 changed files with 116 additions and 16 deletions

View File

@ -246,7 +246,7 @@ 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());
var sources = await this.DataSourceService.GetDataSources(this.LLMProvider, this.DataSourceOptions, DataSourceRetrievalMode.EVERY_MESSAGE, this.GetDataSourcesFromConfiguredIds());
if (generation != this.loadAndApplyFiltersGeneration)
return;

View File

@ -0,0 +1,21 @@
namespace AIStudio.Settings.DataModel;
/// <summary>
/// How the data sources of a chat are searched.
/// </summary>
public enum DataSourceRetrievalMode
{
/// <summary>
/// The model searches the data sources itself, through the tool semantic_search, whenever a
/// question calls for it. No agent takes part: the chat model picks the data sources and
/// judges what it found.
/// </summary>
SEMANTIC_SEARCH,
/// <summary>
/// AI Studio searches the data sources with every message, before the model answers. This is
/// the classic RAG process, with its agents for selecting data sources and for validating what
/// was found.
/// </summary>
EVERY_MESSAGE,
}

View File

@ -94,7 +94,7 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
// data sources changed its security requirements.
//
List<IDataSource> preselectedDataSources = chatThread.DataSourceOptions.PreselectedDataSourceIds.Select(id => settings.ConfigurationData.DataSources.FirstOrDefault(ds => ds.Id == id)).Where(ds => ds is not null).ToList()!;
var dataSources = await dataSourceService.GetDataSources(provider, chatThread.DataSourceOptions, preselectedDataSources);
var dataSources = await dataSourceService.GetDataSources(provider, chatThread.DataSourceOptions, DataSourceRetrievalMode.EVERY_MESSAGE, preselectedDataSources);
var selectedDataSources = dataSources.SelectedDataSources;
//

View File

@ -39,9 +39,10 @@ public sealed class DataSourceService
/// </summary>
/// <param name="selectedLLMProvider">The selected LLM provider.</param>
/// <param name="dataSourceOptions">The active data source options, which determine which agent providers participate.</param>
/// <param name="retrievalMode">How the data sources are searched in effect, which decides whether any agent participates at all.</param>
/// <param name="previousSelectedDataSources">The data sources selected before.</param>
/// <returns>The allowed data sources and the data sources selected before -- when they are still allowed.</returns>
public async Task<AllowedSelectedDataSources> GetDataSources(AIStudio.Settings.Provider selectedLLMProvider, DataSourceOptions dataSourceOptions, IReadOnlyCollection<IDataSource>? previousSelectedDataSources = null)
public async Task<AllowedSelectedDataSources> GetDataSources(AIStudio.Settings.Provider selectedLLMProvider, DataSourceOptions dataSourceOptions, DataSourceRetrievalMode retrievalMode, IReadOnlyCollection<IDataSource>? previousSelectedDataSources = null)
{
//
// Case: Somehow the selected LLM provider was not set. The default provider
@ -55,7 +56,7 @@ public sealed class DataSourceService
}
var usingTrustedProvider = selectedLLMProvider.IsTrustedForDataSourceSecurityChecks(this.settingsManager);
var participatingProviders = this.GetParticipatingProviders(selectedLLMProvider.Id, dataSourceOptions,
var participatingProviders = this.GetParticipatingProviders(selectedLLMProvider.Id, dataSourceOptions, retrievalMode,
new("chat provider", usingTrustedProvider, selectedLLMProvider.GetConfidenceLevel(this.settingsManager)));
return await this.GetDataSources(usingTrustedProvider, participatingProviders, previousSelectedDataSources);
}
@ -67,9 +68,10 @@ public sealed class DataSourceService
/// </summary>
/// <param name="selectedLLMProvider">The selected LLM provider.</param>
/// <param name="dataSourceOptions">The active data source options, which determine which agent providers participate.</param>
/// <param name="retrievalMode">How the data sources are searched in effect, which decides whether any agent participates at all.</param>
/// <param name="requestedDataSources">The data sources to check.</param>
/// <returns>The requested data sources that are allowed for the provider.</returns>
public async Task<IReadOnlyList<IDataSource>> GetAllowedDataSources(AIStudio.Settings.Provider selectedLLMProvider, DataSourceOptions dataSourceOptions, IReadOnlyCollection<IDataSource> requestedDataSources)
public async Task<IReadOnlyList<IDataSource>> GetAllowedDataSources(AIStudio.Settings.Provider selectedLLMProvider, DataSourceOptions dataSourceOptions, DataSourceRetrievalMode retrievalMode, IReadOnlyCollection<IDataSource> requestedDataSources)
{
if (selectedLLMProvider == Settings.Provider.NONE)
{
@ -78,7 +80,7 @@ public sealed class DataSourceService
}
var usingTrustedProvider = selectedLLMProvider.IsTrustedForDataSourceSecurityChecks(this.settingsManager);
var participatingProviders = this.GetParticipatingProviders(selectedLLMProvider.Id, dataSourceOptions,
var participatingProviders = this.GetParticipatingProviders(selectedLLMProvider.Id, dataSourceOptions, retrievalMode,
new("chat provider", usingTrustedProvider, selectedLLMProvider.GetConfidenceLevel(this.settingsManager)));
var allowedDataSources = await this.GetAllowedDataSources(usingTrustedProvider, participatingProviders, requestedDataSources);
@ -98,9 +100,10 @@ public sealed class DataSourceService
/// </summary>
/// <param name="selectedLLMProvider">The selected LLM provider.</param>
/// <param name="dataSourceOptions">The active data source options, which determine which agent providers participate.</param>
/// <param name="retrievalMode">How the data sources are searched in effect, which decides whether any agent participates at all.</param>
/// <param name="previousSelectedDataSources">The data sources selected before.</param>
/// <returns>The allowed data sources and the data sources selected before -- when they are still allowed.</returns>
public async Task<AllowedSelectedDataSources> GetDataSources(IProvider selectedLLMProvider, DataSourceOptions dataSourceOptions, IReadOnlyCollection<IDataSource>? previousSelectedDataSources = null)
public async Task<AllowedSelectedDataSources> GetDataSources(IProvider selectedLLMProvider, DataSourceOptions dataSourceOptions, DataSourceRetrievalMode retrievalMode, IReadOnlyCollection<IDataSource>? previousSelectedDataSources = null)
{
//
// Case: Somehow the selected LLM provider was not set. The default provider
@ -114,24 +117,49 @@ public sealed class DataSourceService
}
var usingTrustedProvider = selectedLLMProvider.IsTrustedForDataSourceSecurityChecks(this.settingsManager);
var participatingProviders = this.GetParticipatingProviders(selectedLLMProvider.ConfiguredProviderId, dataSourceOptions,
var participatingProviders = this.GetParticipatingProviders(selectedLLMProvider.ConfiguredProviderId, dataSourceOptions, retrievalMode,
new("chat provider", usingTrustedProvider, selectedLLMProvider.GetConfidenceLevel(this.settingsManager)));
return await this.GetDataSources(usingTrustedProvider, participatingProviders, previousSelectedDataSources);
}
private IReadOnlyList<ParticipatingProvider> GetParticipatingProviders(string currentProviderId, DataSourceOptions dataSourceOptions, ParticipatingProvider currentProvider)
private IReadOnlyList<ParticipatingProvider> GetParticipatingProviders(string currentProviderId, DataSourceOptions dataSourceOptions, DataSourceRetrievalMode retrievalMode, ParticipatingProvider currentProvider)
{
var providers = new List<ParticipatingProvider> { currentProvider };
if (dataSourceOptions.AutomaticDataSourceSelection)
this.AddAgentProvider(providers, Components.AGENT_DATA_SOURCE_SELECTION, currentProviderId, "data source selection agent");
if (dataSourceOptions.AutomaticValidation && this.settingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
this.AddAgentProvider(providers, Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION, currentProviderId, "retrieval context validation agent");
var retrievalContextValidationEnabled = this.settingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation;
foreach (var (component, role) in GetParticipatingAgents(dataSourceOptions, retrievalMode, retrievalContextValidationEnabled))
this.AddAgentProvider(providers, component, currentProviderId, role);
return providers;
}
/// <summary>
/// Which agents get to see the data of the data sources, besides the chat provider.
/// </summary>
/// <remarks>
/// Only the classic RAG process runs agents. With Semantic Search, the chat model picks the
/// data sources and judges what it found itself, so the data reaches no other provider.
/// Counting the providers of the agents there would hold back data sources which the chat
/// provider alone may use.
/// </remarks>
/// <param name="dataSourceOptions">The active data source options.</param>
/// <param name="retrievalMode">How the data sources are searched in effect.</param>
/// <param name="retrievalContextValidationEnabled">Whether the validation of retrieval contexts is enabled in the settings.</param>
/// <returns>The component of each participating agent, together with its role for the log.</returns>
internal static IReadOnlyList<(Components Component, string Role)> GetParticipatingAgents(DataSourceOptions dataSourceOptions, DataSourceRetrievalMode retrievalMode, bool retrievalContextValidationEnabled)
{
if (retrievalMode is DataSourceRetrievalMode.SEMANTIC_SEARCH)
return [];
var agents = new List<(Components Component, string Role)>(2);
if (dataSourceOptions.AutomaticDataSourceSelection)
agents.Add((Components.AGENT_DATA_SOURCE_SELECTION, "data source selection agent"));
if (dataSourceOptions.AutomaticValidation && retrievalContextValidationEnabled)
agents.Add((Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION, "retrieval context validation agent"));
return agents;
}
private void AddAgentProvider(List<ParticipatingProvider> providers, Components component, string currentProviderId, string role)
{
var provider = this.settingsManager.GetPreselectedProvider(component, currentProviderId, true);

View File

@ -274,7 +274,7 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
// decide which agent providers take part, and an agent with too little confidence makes
// a data source unavailable.
//
availableDataSources = await dataSourceService.GetAllowedDataSources(provider, chosenOptions, requestedDataSources);
availableDataSources = await dataSourceService.GetAllowedDataSources(provider, chosenOptions, DataSourceRetrievalMode.EVERY_MESSAGE, requestedDataSources);
}
catch (Exception exception)
{

View File

@ -0,0 +1,51 @@
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
namespace AIStudio.Tests.Tools;
/// <summary>
/// Checks which agents count as seeing the data of the data sources.
/// </summary>
/// <remarks>
/// Every provider which sees the data must be trusted enough for every data source it sees. That
/// cuts both ways: leaving out an agent which does run would send data to a provider trusted too
/// little, while counting an agent which does not run holds back data sources for no reason. The
/// classic RAG process runs its agents; Semantic Search runs none, since the chat model picks the
/// data sources and judges the passages itself.
/// </remarks>
[TestFixture]
public sealed class DataSourceParticipatingAgentsTests
{
[Test]
public void SemanticSearchRunsNoAgent()
{
var agents = DataSourceService.GetParticipatingAgents(Options(automaticSelection: true, automaticValidation: true), DataSourceRetrievalMode.SEMANTIC_SEARCH, retrievalContextValidationEnabled: true);
Assert.That(agents, Is.Empty, "The chat provider alone sees the data, so its trust alone decides which data sources it may search.");
}
[Test]
public void TheClassicRAGProcessCountsTheAgentsItRuns()
{
var agents = DataSourceService.GetParticipatingAgents(Options(automaticSelection: true, automaticValidation: true), DataSourceRetrievalMode.EVERY_MESSAGE, retrievalContextValidationEnabled: true);
Assert.That(agents.Select(agent => agent.Component), Is.EqualTo(new[] { AIStudio.Tools.Components.AGENT_DATA_SOURCE_SELECTION, AIStudio.Tools.Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION }));
}
[Test]
public void TheClassicRAGProcessCountsNoAgentItDoesNotRun()
{
Assert.Multiple(() =>
{
Assert.That(DataSourceService.GetParticipatingAgents(Options(automaticSelection: false, automaticValidation: false), DataSourceRetrievalMode.EVERY_MESSAGE, retrievalContextValidationEnabled: true), Is.Empty);
Assert.That(DataSourceService.GetParticipatingAgents(Options(automaticSelection: false, automaticValidation: true), DataSourceRetrievalMode.EVERY_MESSAGE, retrievalContextValidationEnabled: false), Is.Empty, "The validation of this chat is on, but the settings switch it off everywhere.");
});
}
private static DataSourceOptions Options(bool automaticSelection, bool automaticValidation) => new()
{
DisableDataSources = false,
AutomaticDataSourceSelection = automaticSelection,
AutomaticValidation = automaticValidation,
};
}