mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-29 10:43:37 +00:00
Resolved 29 conflicting files. The notable decisions: Confidence: main's tool-calling gate (RequiredProviderConfidence) and this branch's local-RAG gate (DataConfidenceLevel) turned out to be the same rule on the same axis, so they are now one field. Both tool results and data sources raise it through RequireProviderConfidence(). The gate checks the level strictly and no longer exempts providers trusted by configuration: TrustedProviderIds is documented as applying to data-source security checks only, and organizations set confidence through DataConfidence .CustomConfidenceScheme instead. The security axis (DataSecurity, ERI, IsTrustedForDataSourceSecurityChecks) is unchanged. Provider creation: main's CreateProvider signature won (hfEndpointKind, capabilityOverrides, no model parameter); tokenizerPath was added to it and is set for every provider, including the new Hetzner, IONOS and LiteLLM. Provider and EmbeddingProvider combine the record parameters, Lua parsing and Lua serialization of both sides. File types: main's hierarchy (ODT leaf, WORD parent, PowerPoint without the legacy .ppt, TABULAR instead of DELIMITED_TABLE) plus this branch's SPREADSHEET parent with ODS and the xlsm/xlsb/xla/xlam extensions, which the runtime already reads. Both sides had added a conflicting HTML filter; the reading family keeps the name, and the export path uses a narrow HTML_DOCUMENT, following the existing LATEX/TEX split. Runtime: main's file_data.rs is the base, including the prompt-injection sanitizer and the extraction routes. Token counting and chunk segmentation moved into take_released, so they act on the text the filter has released rather than on text it is still holding. A failed count is logged and left out instead of ending the extraction, because the app counts such a segment itself. Data sources: the participating-provider checks of this branch are kept, and main's GetAllowedDataSources overload now builds on them. DirectChatService resolves the launched chat's data source options before the check, so filter and chat see the same options. .NET and Rust both build clean; I18N regenerated to 4060 keys.
306 lines
16 KiB
C#
306 lines
16 KiB
C#
using AIStudio.Assistants.ERI;
|
|
using AIStudio.Provider;
|
|
using AIStudio.Settings;
|
|
using AIStudio.Settings.DataModel;
|
|
using AIStudio.Tools.ERIClient;
|
|
using AIStudio.Tools.ERIClient.DataModel;
|
|
|
|
namespace AIStudio.Tools.Services;
|
|
|
|
public sealed class DataSourceService
|
|
{
|
|
private readonly record struct ParticipatingProvider(string Role, bool IsTrusted, ConfidenceLevel ConfidenceLevel);
|
|
|
|
private readonly RustService rustService;
|
|
private readonly SettingsManager settingsManager;
|
|
private readonly ILogger<DataSourceService> logger;
|
|
|
|
public DataSourceService(SettingsManager settingsManager, ILogger<DataSourceService> logger, RustService rustService)
|
|
{
|
|
this.logger = logger;
|
|
this.rustService = rustService;
|
|
this.settingsManager = settingsManager;
|
|
|
|
this.logger.LogInformation("The data source service has been initialized.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a list of data sources that are allowed for the selected LLM provider.
|
|
/// It also returns the data sources selected before when they are still allowed.
|
|
/// </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="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)
|
|
{
|
|
//
|
|
// Case: Somehow the selected LLM provider was not set. The default provider
|
|
// does not mean anything. We cannot filter the data sources by any means.
|
|
// We return an empty list. Better safe than sorry.
|
|
//
|
|
if (selectedLLMProvider == Settings.Provider.NONE)
|
|
{
|
|
this.logger.LogWarning("The selected LLM provider is not set. We cannot filter the data sources by any means.");
|
|
return new([], []);
|
|
}
|
|
|
|
var usingTrustedProvider = selectedLLMProvider.IsTrustedForDataSourceSecurityChecks(this.settingsManager);
|
|
var participatingProviders = this.GetParticipatingProviders(selectedLLMProvider.Id, dataSourceOptions,
|
|
new("chat provider", usingTrustedProvider, selectedLLMProvider.GetConfidenceLevel(this.settingsManager)));
|
|
return await this.GetDataSources(usingTrustedProvider, participatingProviders, previousSelectedDataSources);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the requested data sources that are allowed for the selected LLM provider.
|
|
/// Unlike see GetDataSources(AIStudio.Settings.Provider, IReadOnlyCollection{IDataSource}),
|
|
/// this method checks only the supplied data sources.
|
|
/// </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="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)
|
|
{
|
|
if (selectedLLMProvider == Settings.Provider.NONE)
|
|
{
|
|
this.logger.LogWarning("The selected LLM provider is not set. We cannot filter the data sources by any means.");
|
|
return [];
|
|
}
|
|
|
|
var usingTrustedProvider = selectedLLMProvider.IsTrustedForDataSourceSecurityChecks(this.settingsManager);
|
|
var participatingProviders = this.GetParticipatingProviders(selectedLLMProvider.Id, dataSourceOptions,
|
|
new("chat provider", usingTrustedProvider, selectedLLMProvider.GetConfidenceLevel(this.settingsManager)));
|
|
return await this.GetAllowedDataSources(usingTrustedProvider, participatingProviders, requestedDataSources);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a list of data sources that are allowed for the selected LLM provider.
|
|
/// It also returns the data sources selected before when they are still allowed.
|
|
/// </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="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)
|
|
{
|
|
//
|
|
// Case: Somehow the selected LLM provider was not set. The default provider
|
|
// does not mean anything. We cannot filter the data sources by any means.
|
|
// We return an empty list. Better safe than sorry.
|
|
//
|
|
if (selectedLLMProvider is NoProvider)
|
|
{
|
|
this.logger.LogWarning("The selected LLM provider is the default provider. We cannot filter the data sources by any means.");
|
|
return new([], []);
|
|
}
|
|
|
|
var usingTrustedProvider = selectedLLMProvider.IsTrustedForDataSourceSecurityChecks(this.settingsManager);
|
|
var participatingProviders = this.GetParticipatingProviders(selectedLLMProvider.ConfiguredProviderId, dataSourceOptions,
|
|
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)
|
|
{
|
|
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");
|
|
|
|
return providers;
|
|
}
|
|
|
|
private void AddAgentProvider(List<ParticipatingProvider> providers, Components component, string currentProviderId, string role)
|
|
{
|
|
var provider = this.settingsManager.GetPreselectedProvider(component, currentProviderId, true);
|
|
if (provider == Settings.Provider.NONE)
|
|
{
|
|
this.logger.LogWarning($"No provider is available for the {role}. Data sources cannot be made available while this agent is enabled.");
|
|
providers.Add(new(role, false, ConfidenceLevel.NONE));
|
|
return;
|
|
}
|
|
|
|
providers.Add(new(
|
|
role,
|
|
provider.IsTrustedForDataSourceSecurityChecks(this.settingsManager),
|
|
provider.GetConfidenceLevel(this.settingsManager)));
|
|
}
|
|
|
|
private async Task<AllowedSelectedDataSources> GetDataSources(bool usingTrustedProvider, IReadOnlyList<ParticipatingProvider> participatingProviders, IReadOnlyCollection<IDataSource>? previousSelectedDataSources = null)
|
|
{
|
|
var allDataSources = this.settingsManager.ConfigurationData.DataSources.ToList();
|
|
var previousSelectedDataSourceIds = previousSelectedDataSources?.Select(source => source.Id).ToHashSet(StringComparer.Ordinal) ?? [];
|
|
var filteredDataSources = await this.GetAllowedDataSources(usingTrustedProvider, participatingProviders, allDataSources);
|
|
var filteredSelectedDataSources = filteredDataSources.Where(source => previousSelectedDataSourceIds.Contains(source.Id)).ToList();
|
|
|
|
return new(filteredDataSources, filteredSelectedDataSources);
|
|
}
|
|
|
|
private async Task<IReadOnlyList<IDataSource>> GetAllowedDataSources(bool usingTrustedProvider, IReadOnlyList<ParticipatingProvider> participatingProviders, IReadOnlyCollection<IDataSource> requestedDataSources)
|
|
{
|
|
var filteredDataSources = new List<IDataSource>(requestedDataSources.Count);
|
|
var tasks = new List<Task<IDataSource?>>(requestedDataSources.Count);
|
|
|
|
// Start all checks in parallel:
|
|
foreach (var source in requestedDataSources)
|
|
tasks.Add(this.CheckOneDataSource(source, usingTrustedProvider, participatingProviders));
|
|
|
|
|
|
// Wait for all checks and collect the results:
|
|
foreach (var task in tasks)
|
|
{
|
|
var source = await task;
|
|
if (source is not null)
|
|
filteredDataSources.Add(source);
|
|
}
|
|
|
|
return filteredDataSources;
|
|
}
|
|
|
|
private async Task<IDataSource?> CheckOneDataSource(IDataSource source, bool usingTrustedProvider, IReadOnlyList<ParticipatingProvider> participatingProviders)
|
|
{
|
|
if (source is IInternalDataSource internalSource)
|
|
{
|
|
foreach (var provider in participatingProviders)
|
|
{
|
|
if (!provider.ConfidenceLevel.AllowsDataSourceConfidenceLevel(internalSource.ConfidenceLevel))
|
|
{
|
|
this.logger.LogWarning($"The internal data source '{source.Name}' (id={source.Id}) requires provider confidence '{internalSource.ConfidenceLevel.GetName()}'. The {provider.Role} only has confidence '{provider.ConfidenceLevel.GetName()}'. We skip this source.");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (!DataSourceEmbeddingProviders.TryResolve(this.settingsManager, source, out var embeddingProvider))
|
|
{
|
|
this.logger.LogWarning($"The internal data source '{source.Name}' (id={source.Id}) has no usable embedding provider. We skip this source.");
|
|
return null;
|
|
}
|
|
|
|
var embeddingProviderConfidence = embeddingProvider.GetConfidenceLevel(this.settingsManager);
|
|
if (!embeddingProviderConfidence.AllowsDataSourceConfidenceLevel(internalSource.ConfidenceLevel))
|
|
{
|
|
this.logger.LogWarning($"The internal data source '{source.Name}' (id={source.Id}) requires provider confidence '{internalSource.ConfidenceLevel.GetName()}'. Its embedding provider '{embeddingProvider.Name}' only has confidence '{embeddingProviderConfidence.GetName()}'. We skip this source.");
|
|
return null;
|
|
}
|
|
|
|
return source;
|
|
}
|
|
|
|
//
|
|
// Unfortunately, we have to live-check any ERI source for its security requirements.
|
|
// Because the ERI server operator might change the security requirements at any time.
|
|
//
|
|
SecurityRequirements? eriSourceRequirements = null;
|
|
if (source is DataSourceERI_V1 eriSource)
|
|
{
|
|
using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(6));
|
|
using var client = ERIClientFactory.Get(ERIVersion.V1, eriSource);
|
|
if(client is null)
|
|
{
|
|
this.logger.LogError($"Could not create ERI client for source '{source.Name}' (id={source.Id}). We skip this source.");
|
|
return null;
|
|
}
|
|
|
|
this.logger.LogInformation($"Authenticating with ERI source '{source.Name}' (id={source.Id})...");
|
|
var loginResult = await client.AuthenticateAsync(this.rustService, cancellationToken: cancellationTokenSource.Token);
|
|
if (!loginResult.Successful)
|
|
{
|
|
this.logger.LogWarning($"Authentication with ERI source '{source.Name}' (id={source.Id}) failed. We skip this source. Reason: {loginResult.Message}");
|
|
return null;
|
|
}
|
|
|
|
this.logger.LogInformation($"Checking security requirements for ERI source '{source.Name}' (id={source.Id})...");
|
|
var securityRequest = await client.GetSecurityRequirementsAsync(cancellationTokenSource.Token);
|
|
if (!securityRequest.Successful)
|
|
{
|
|
this.logger.LogWarning($"Could not retrieve security requirements for ERI source '{source.Name}' (id={source.Id}). We skip this source. Reason: {loginResult.Message}");
|
|
return null;
|
|
}
|
|
|
|
eriSourceRequirements = securityRequest.Data;
|
|
this.logger.LogInformation($"Security requirements for ERI source '{source.Name}' (id={source.Id}) retrieved successfully.");
|
|
}
|
|
|
|
if (source is not IExternalDataSource externalSource)
|
|
return source;
|
|
|
|
switch (externalSource.SecurityPolicy)
|
|
{
|
|
case DataSourceSecurity.ALLOW_ANY:
|
|
|
|
//
|
|
// Case: The data source allows any provider type. We want to use a trusted provider.
|
|
// There is no issue with this source. Accept it.
|
|
//
|
|
if(usingTrustedProvider)
|
|
return source;
|
|
|
|
//
|
|
// Case: This is a local data source. When the source allows any provider type, we can use it.
|
|
// Accept it.
|
|
//
|
|
if(eriSourceRequirements is null)
|
|
return source;
|
|
|
|
//
|
|
// Case: The ERI source requires a self-hosted or organization-trusted provider. This misconfiguration happens
|
|
// when the ERI server operator changes the security requirements. The ERI server
|
|
// operator owns the data -- we have to respect their rules. We skip this source.
|
|
//
|
|
if (eriSourceRequirements is { AllowedProviderType: ProviderType.SELF_HOSTED })
|
|
{
|
|
this.logger.LogWarning($"The ERI source '{source.Name}' (id={source.Id}) requires a self-hosted or organization-trusted provider. We skip this source.");
|
|
return null;
|
|
}
|
|
|
|
//
|
|
// Case: The ERI source allows any provider type. The data source configuration is correct.
|
|
// Accept it.
|
|
//
|
|
if(eriSourceRequirements is { AllowedProviderType: ProviderType.ANY })
|
|
return source;
|
|
|
|
//
|
|
// Case: Missing rules. We skip this source. Better safe than sorry.
|
|
//
|
|
this.logger.LogWarning($"The ERI source '{source.Name}' (id={source.Id}) was filtered out due to missing rules.");
|
|
return null;
|
|
|
|
//
|
|
// Case: The data source requires a trusted provider. We want to use a trusted provider.
|
|
// There is no issue with this source. Accept it.
|
|
//
|
|
case DataSourceSecurity.SELF_HOSTED when usingTrustedProvider:
|
|
return source;
|
|
|
|
//
|
|
// Case: The data source requires a trusted provider. We want to use an untrusted provider.
|
|
// We skip this source.
|
|
//
|
|
case DataSourceSecurity.SELF_HOSTED when !usingTrustedProvider:
|
|
this.logger.LogWarning($"The data source '{source.Name}' (id={source.Id}) requires a self-hosted or organization-trusted provider. We skip this source.");
|
|
return null;
|
|
|
|
//
|
|
// Case: The data source did not specify a security policy. We skip this source.
|
|
// Better safe than sorry.
|
|
//
|
|
case DataSourceSecurity.NOT_SPECIFIED:
|
|
this.logger.LogWarning($"The data source '{source.Name}' (id={source.Id}) has no security policy. We skip this source.");
|
|
return null;
|
|
|
|
//
|
|
// Case: Some developer forgot to implement a security policy. We skip this source.
|
|
// Better safe than sorry.
|
|
//
|
|
default:
|
|
this.logger.LogWarning($"The data source '{source.Name}' (id={source.Id}) was filtered out due unknown security policy.");
|
|
return null;
|
|
}
|
|
}
|
|
}
|