Integrated data sources into any chat (#282)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2025-02-15 15:41:12 +01:00
1 parent 954cf44939
commit f4780939fc
70 files changed
+1506 -250

No files matched your search

@@ -0,0 +1,13 @@
using AIStudio.Settings;
namespace AIStudio.Tools;
/// <summary>
/// Contains both the allowed and selected data sources.
/// </summary>
/// <remarks>
/// The selected data sources are a subset of the allowed data sources.
/// </remarks>
/// <param name="AllowedDataSources">The allowed data sources.</param>
/// <param name="SelectedDataSources">The selected data sources, which are a subset of the allowed data sources.</param>
public readonly record struct AllowedSelectedDataSources(IReadOnlyList<IDataSource> AllowedDataSources, IReadOnlyList<IDataSource> SelectedDataSources);
@@ -3,6 +3,7 @@ using System.Text.Json;
using AIStudio.Settings;
using AIStudio.Tools.ERIClient.DataModel;
using AIStudio.Tools.Services;
namespace AIStudio.Tools.ERIClient;
@@ -12,332 +13,465 @@ public class ERIClientV1(string baseAddress) : ERIClientBase(baseAddress), IERIC
public async Task<APIResponse<List<AuthScheme>>> GetAuthMethodsAsync(CancellationToken cancellationToken = default)
{
using var response = await this.httpClient.GetAsync("/auth/methods", cancellationToken);
if(!response.IsSuccessStatusCode)
try
{
using var response = await this.httpClient.GetAsync("/auth/methods", cancellationToken);
if (!response.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the authentication methods: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
};
}
var authMethods = await response.Content.ReadFromJsonAsync<List<AuthScheme>>(JSON_OPTIONS, cancellationToken);
if (authMethods is null)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
};
}
return new()
{
Successful = true,
Data = authMethods
};
}
catch (TaskCanceledException)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the authentication methods: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
Message = "Failed to retrieve the authentication methods: the request was canceled either by the user or due to a timeout."
};
}
var authMethods = await response.Content.ReadFromJsonAsync<List<AuthScheme>>(JSON_OPTIONS, cancellationToken);
if(authMethods is null)
catch (Exception e)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
Message = $"Failed to retrieve the authentication methods due to an exception: {e.Message}"
};
}
return new()
{
Successful = true,
Data = authMethods
};
}
public async Task<APIResponse<AuthResponse>> AuthenticateAsync(IERIDataSource dataSource, RustService rustService, CancellationToken cancellationToken = default)
{
var authMethod = dataSource.AuthMethod;
var username = dataSource.Username;
switch (dataSource.AuthMethod)
try
{
case AuthMethod.NONE:
using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
{
using var noneAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
if(!noneAuthResponse.IsSuccessStatusCode)
var authMethod = dataSource.AuthMethod;
var username = dataSource.Username;
switch (dataSource.AuthMethod)
{
case AuthMethod.NONE:
using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
{
using var noneAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
if(!noneAuthResponse.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to authenticate with the ERI server. Code: {noneAuthResponse.StatusCode}, Reason: {noneAuthResponse.ReasonPhrase}"
};
}
var noneAuthResult = await noneAuthResponse.Content.ReadFromJsonAsync<AuthResponse>(JSON_OPTIONS, cancellationToken);
if(noneAuthResult == default)
{
return new()
{
Successful = false,
Message = "Failed to authenticate with the ERI server: the response was invalid."
};
}
this.securityToken = noneAuthResult.Token ?? string.Empty;
return new()
{
Successful = false,
Message = $"Failed to authenticate with the ERI server. Code: {noneAuthResponse.StatusCode}, Reason: {noneAuthResponse.ReasonPhrase}"
Successful = true,
Data = noneAuthResult
};
}
var noneAuthResult = await noneAuthResponse.Content.ReadFromJsonAsync<AuthResponse>(JSON_OPTIONS, cancellationToken);
if(noneAuthResult == default)
{
return new()
{
Successful = false,
Message = "Failed to authenticate with the ERI server: the response was invalid."
};
}
this.securityToken = noneAuthResult.Token ?? string.Empty;
return new()
{
Successful = true,
Data = noneAuthResult
};
}
case AuthMethod.USERNAME_PASSWORD:
var passwordResponse = await rustService.GetSecret(dataSource);
if (!passwordResponse.Success)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the password."
};
}
var password = await passwordResponse.Secret.Decrypt(Program.ENCRYPTION);
using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
{
// We must send both values inside the header. The username field is named 'user'.
// The password field is named 'password'.
request.Headers.Add("user", username);
request.Headers.Add("password", password);
using var usernamePasswordAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
if(!usernamePasswordAuthResponse.IsSuccessStatusCode)
case AuthMethod.USERNAME_PASSWORD:
var passwordResponse = await rustService.GetSecret(dataSource);
if (!passwordResponse.Success)
{
return new()
{
Successful = false,
Message = $"Failed to authenticate with the ERI server. Code: {usernamePasswordAuthResponse.StatusCode}, Reason: {usernamePasswordAuthResponse.ReasonPhrase}"
Message = "Failed to retrieve the password."
};
}
var usernamePasswordAuthResult = await usernamePasswordAuthResponse.Content.ReadFromJsonAsync<AuthResponse>(JSON_OPTIONS, cancellationToken);
if(usernamePasswordAuthResult == default)
{
return new()
{
Successful = false,
Message = "Failed to authenticate with the server: the response was invalid."
};
}
this.securityToken = usernamePasswordAuthResult.Token ?? string.Empty;
return new()
{
Successful = true,
Data = usernamePasswordAuthResult
};
}
case AuthMethod.TOKEN:
var tokenResponse = await rustService.GetSecret(dataSource);
if (!tokenResponse.Success)
{
return new()
var password = await passwordResponse.Secret.Decrypt(Program.ENCRYPTION);
using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
{
Successful = false,
Message = "Failed to retrieve the access token."
};
}
// We must send both values inside the header. The username field is named 'user'.
// The password field is named 'password'.
request.Headers.Add("user", username);
request.Headers.Add("password", password);
var token = await tokenResponse.Secret.Decrypt(Program.ENCRYPTION);
using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
{
request.Headers.Add("Authorization", $"Bearer {token}");
using var usernamePasswordAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
if(!usernamePasswordAuthResponse.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to authenticate with the ERI server. Code: {usernamePasswordAuthResponse.StatusCode}, Reason: {usernamePasswordAuthResponse.ReasonPhrase}"
};
}
using var tokenAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
if(!tokenAuthResponse.IsSuccessStatusCode)
var usernamePasswordAuthResult = await usernamePasswordAuthResponse.Content.ReadFromJsonAsync<AuthResponse>(JSON_OPTIONS, cancellationToken);
if(usernamePasswordAuthResult == default)
{
return new()
{
Successful = false,
Message = "Failed to authenticate with the server: the response was invalid."
};
}
this.securityToken = usernamePasswordAuthResult.Token ?? string.Empty;
return new()
{
Successful = true,
Data = usernamePasswordAuthResult
};
}
case AuthMethod.TOKEN:
var tokenResponse = await rustService.GetSecret(dataSource);
if (!tokenResponse.Success)
{
return new()
{
Successful = false,
Message = $"Failed to authenticate with the ERI server. Code: {tokenAuthResponse.StatusCode}, Reason: {tokenAuthResponse.ReasonPhrase}"
Message = "Failed to retrieve the access token."
};
}
var tokenAuthResult = await tokenAuthResponse.Content.ReadFromJsonAsync<AuthResponse>(JSON_OPTIONS, cancellationToken);
if(tokenAuthResult == default)
var token = await tokenResponse.Secret.Decrypt(Program.ENCRYPTION);
using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
{
request.Headers.Add("Authorization", $"Bearer {token}");
using var tokenAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
if(!tokenAuthResponse.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to authenticate with the ERI server. Code: {tokenAuthResponse.StatusCode}, Reason: {tokenAuthResponse.ReasonPhrase}"
};
}
var tokenAuthResult = await tokenAuthResponse.Content.ReadFromJsonAsync<AuthResponse>(JSON_OPTIONS, cancellationToken);
if(tokenAuthResult == default)
{
return new()
{
Successful = false,
Message = "Failed to authenticate with the ERI server: the response was invalid."
};
}
this.securityToken = tokenAuthResult.Token ?? string.Empty;
return new()
{
Successful = false,
Message = "Failed to authenticate with the ERI server: the response was invalid."
Successful = true,
Data = tokenAuthResult
};
}
this.securityToken = tokenAuthResult.Token ?? string.Empty;
return new()
{
Successful = true,
Data = tokenAuthResult
};
}
default:
this.securityToken = string.Empty;
return new()
{
Successful = false,
Message = "The authentication method is not supported yet."
};
default:
this.securityToken = string.Empty;
return new()
{
Successful = false,
Message = "The authentication method is not supported yet."
};
}
}
catch(TaskCanceledException)
{
return new()
{
Successful = false,
Message = "Failed to authenticate with the ERI server: the request was canceled either by the user or due to a timeout."
};
}
catch (Exception e)
{
return new()
{
Successful = false,
Message = $"Failed to authenticate with the ERI server due to an exception: {e.Message}"
};
}
}
public async Task<APIResponse<DataSourceInfo>> GetDataSourceInfoAsync(CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/dataSource");
request.Headers.Add("token", this.securityToken);
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/dataSource");
request.Headers.Add("token", this.securityToken);
using var response = await this.httpClient.SendAsync(request, cancellationToken);
if(!response.IsSuccessStatusCode)
using var response = await this.httpClient.SendAsync(request, cancellationToken);
if(!response.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the data source information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
};
}
var dataSourceInfo = await response.Content.ReadFromJsonAsync<DataSourceInfo>(JSON_OPTIONS, cancellationToken);
if(dataSourceInfo == default)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the data source information: the ERI server did not return a valid response."
};
}
return new()
{
Successful = true,
Data = dataSourceInfo
};
}
catch(TaskCanceledException)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the data source information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
Message = "Failed to retrieve the data source information: the request was canceled either by the user or due to a timeout."
};
}
var dataSourceInfo = await response.Content.ReadFromJsonAsync<DataSourceInfo>(JSON_OPTIONS, cancellationToken);
if(dataSourceInfo == default)
catch (Exception e)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the data source information: the ERI server did not return a valid response."
Message = $"Failed to retrieve the data source information due to an exception: {e.Message}"
};
}
return new()
{
Successful = true,
Data = dataSourceInfo
};
}
public async Task<APIResponse<List<EmbeddingInfo>>> GetEmbeddingInfoAsync(CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/embedding/info");
request.Headers.Add("token", this.securityToken);
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/embedding/info");
request.Headers.Add("token", this.securityToken);
using var response = await this.httpClient.SendAsync(request, cancellationToken);
if(!response.IsSuccessStatusCode)
using var response = await this.httpClient.SendAsync(request, cancellationToken);
if(!response.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the embedding information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
};
}
var embeddingInfo = await response.Content.ReadFromJsonAsync<List<EmbeddingInfo>>(JSON_OPTIONS, cancellationToken);
if(embeddingInfo is null)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the embedding information: the ERI server did not return a valid response."
};
}
return new()
{
Successful = true,
Data = embeddingInfo
};
}
catch(TaskCanceledException)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the embedding information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
Message = "Failed to retrieve the embedding information: the request was canceled either by the user or due to a timeout."
};
}
var embeddingInfo = await response.Content.ReadFromJsonAsync<List<EmbeddingInfo>>(JSON_OPTIONS, cancellationToken);
if(embeddingInfo is null)
catch (Exception e)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the embedding information: the ERI server did not return a valid response."
Message = $"Failed to retrieve the embedding information due to an exception: {e.Message}"
};
}
return new()
{
Successful = true,
Data = embeddingInfo
};
}
public async Task<APIResponse<List<RetrievalInfo>>> GetRetrievalInfoAsync(CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/retrieval/info");
request.Headers.Add("token", this.securityToken);
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/retrieval/info");
request.Headers.Add("token", this.securityToken);
using var response = await this.httpClient.SendAsync(request, cancellationToken);
if(!response.IsSuccessStatusCode)
using var response = await this.httpClient.SendAsync(request, cancellationToken);
if(!response.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the retrieval information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
};
}
var retrievalInfo = await response.Content.ReadFromJsonAsync<List<RetrievalInfo>>(JSON_OPTIONS, cancellationToken);
if(retrievalInfo is null)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the retrieval information: the ERI server did not return a valid response."
};
}
return new()
{
Successful = true,
Data = retrievalInfo
};
}
catch(TaskCanceledException)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the retrieval information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
Message = "Failed to retrieve the retrieval information: the request was canceled either by the user or due to a timeout."
};
}
var retrievalInfo = await response.Content.ReadFromJsonAsync<List<RetrievalInfo>>(JSON_OPTIONS, cancellationToken);
if(retrievalInfo is null)
catch (Exception e)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the retrieval information: the ERI server did not return a valid response."
Message = $"Failed to retrieve the retrieval information due to an exception: {e.Message}"
};
}
return new()
{
Successful = true,
Data = retrievalInfo
};
}
public async Task<APIResponse<List<Context>>> ExecuteRetrievalAsync(RetrievalRequest request, CancellationToken cancellationToken = default)
{
using var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/retrieval");
requestMessage.Headers.Add("token", this.securityToken);
try
{
using var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/retrieval");
requestMessage.Headers.Add("token", this.securityToken);
using var content = new StringContent(JsonSerializer.Serialize(request, JSON_OPTIONS), Encoding.UTF8, "application/json");
requestMessage.Content = content;
using var content = new StringContent(JsonSerializer.Serialize(request, JSON_OPTIONS), Encoding.UTF8, "application/json");
requestMessage.Content = content;
using var response = await this.httpClient.SendAsync(requestMessage, cancellationToken);
if(!response.IsSuccessStatusCode)
using var response = await this.httpClient.SendAsync(requestMessage, cancellationToken);
if(!response.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to execute the retrieval request: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
};
}
var contexts = await response.Content.ReadFromJsonAsync<List<Context>>(JSON_OPTIONS, cancellationToken);
if(contexts is null)
{
return new()
{
Successful = false,
Message = "Failed to execute the retrieval request: the ERI server did not return a valid response."
};
}
return new()
{
Successful = true,
Data = contexts
};
}
catch(TaskCanceledException)
{
return new()
{
Successful = false,
Message = $"Failed to execute the retrieval request: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
Message = "Failed to execute the retrieval request: the request was canceled either by the user or due to a timeout."
};
}
var contexts = await response.Content.ReadFromJsonAsync<List<Context>>(JSON_OPTIONS, cancellationToken);
if(contexts is null)
catch (Exception e)
{
return new()
{
Successful = false,
Message = "Failed to execute the retrieval request: the ERI server did not return a valid response."
Message = $"Failed to execute the retrieval request due to an exception: {e.Message}"
};
}
return new()
{
Successful = true,
Data = contexts
};
}
public async Task<APIResponse<SecurityRequirements>> GetSecurityRequirementsAsync(CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/security/requirements");
request.Headers.Add("token", this.securityToken);
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/security/requirements");
request.Headers.Add("token", this.securityToken);
using var response = await this.httpClient.SendAsync(request, cancellationToken);
if(!response.IsSuccessStatusCode)
using var response = await this.httpClient.SendAsync(request, cancellationToken);
if(!response.IsSuccessStatusCode)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the security requirements: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
};
}
var securityRequirements = await response.Content.ReadFromJsonAsync<SecurityRequirements>(JSON_OPTIONS, cancellationToken);
if(securityRequirements == default)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the security requirements: the ERI server did not return a valid response."
};
}
return new()
{
Successful = true,
Data = securityRequirements
};
}
catch(TaskCanceledException)
{
return new()
{
Successful = false,
Message = $"Failed to retrieve the security requirements: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
Message = "Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout."
};
}
var securityRequirements = await response.Content.ReadFromJsonAsync<SecurityRequirements>(JSON_OPTIONS, cancellationToken);
if(securityRequirements == default)
catch (Exception e)
{
return new()
{
Successful = false,
Message = "Failed to retrieve the security requirements: the ERI server did not return a valid response."
Message = $"Failed to retrieve the security requirements due to an exception: {e.Message}"
};
}
return new()
{
Successful = true,
Data = securityRequirements
};
}
#endregion
@@ -1,5 +1,6 @@
using AIStudio.Settings;
using AIStudio.Tools.ERIClient.DataModel;
using AIStudio.Tools.Services;
namespace AIStudio.Tools.ERIClient;
@@ -0,0 +1,209 @@
using AIStudio.Assistants.ERI;
using AIStudio.Provider;
using AIStudio.Provider.SelfHosted;
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 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="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, 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 == default)
{
this.logger.LogWarning("The selected LLM provider is not set. We cannot filter the data sources by any means.");
return new([], []);
}
return await this.GetDataSources(selectedLLMProvider.IsSelfHosted, previousSelectedDataSources);
}
/// <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="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, 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([], []);
}
return await this.GetDataSources(selectedLLMProvider is ProviderSelfHosted, previousSelectedDataSources);
}
private async Task<AllowedSelectedDataSources> GetDataSources(bool usingSelfHostedProvider, IReadOnlyCollection<IDataSource>? previousSelectedDataSources = null)
{
var allDataSources = this.settingsManager.ConfigurationData.DataSources;
var filteredDataSources = new List<IDataSource>(allDataSources.Count);
var filteredSelectedDataSources = new List<IDataSource>(previousSelectedDataSources?.Count ?? 0);
var tasks = new List<Task<IDataSource?>>(allDataSources.Count);
// Start all checks in parallel:
foreach (var source in allDataSources)
tasks.Add(this.CheckOneDataSource(source, usingSelfHostedProvider));
// 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);
if (previousSelectedDataSources is not null && previousSelectedDataSources.Contains(source))
filteredSelectedDataSources.Add(source);
}
}
return new(filteredDataSources, filteredSelectedDataSources);
}
private async Task<IDataSource?> CheckOneDataSource(IDataSource source, bool usingSelfHostedProvider)
{
//
// 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(eriSource, this.rustService, 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.");
}
switch (source.SecurityPolicy)
{
case DataSourceSecurity.ALLOW_ANY:
//
// Case: The data source allows any provider type. We want to use a self-hosted provider.
// There is no issue with this source. Accept it.
//
if(usingSelfHostedProvider)
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 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 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.LogDebug($"The ERI source '{source.Name}' (id={source.Id}) was filtered out due to missing rules.");
return null;
//
// Case: The data source requires a self-hosted provider. We want to use a self-hosted provider.
// There is no issue with this source. Accept it.
//
case DataSourceSecurity.SELF_HOSTED when usingSelfHostedProvider:
return source;
//
// Case: The data source requires a self-hosted provider. We want to use a cloud provider.
// We skip this source.
//
case DataSourceSecurity.SELF_HOSTED when !usingSelfHostedProvider:
this.logger.LogWarning($"The data source '{source.Name}' (id={source.Id}) requires a self-hosted 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;
}
}
}
@@ -1,6 +1,6 @@
using AIStudio.Tools.Rust;
namespace AIStudio.Tools;
namespace AIStudio.Tools.Services;
public sealed partial class RustService
{
@@ -1,6 +1,6 @@
using System.Security.Cryptography;
namespace AIStudio.Tools;
namespace AIStudio.Tools.Services;
public sealed partial class RustService
{
@@ -1,6 +1,6 @@
using AIStudio.Tools.Rust;
namespace AIStudio.Tools;
namespace AIStudio.Tools.Services;
public sealed partial class RustService
{
@@ -1,6 +1,6 @@
using AIStudio.Tools.Rust;
namespace AIStudio.Tools;
namespace AIStudio.Tools.Services;
public sealed partial class RustService
{
@@ -1,6 +1,6 @@
using AIStudio.Tools.Rust;
namespace AIStudio.Tools;
namespace AIStudio.Tools.Services;
public sealed partial class RustService
{
@@ -1,6 +1,6 @@
using AIStudio.Tools.Rust;
namespace AIStudio.Tools;
namespace AIStudio.Tools.Services;
public sealed partial class RustService
{
@@ -3,7 +3,7 @@ using System.Text.Json;
// ReSharper disable NotAccessedPositionalProperty.Local
namespace AIStudio.Tools;
namespace AIStudio.Tools.Services;
/// <summary>
/// Calling Rust functions.
@@ -30,9 +30,15 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
//
// Wait until the app is fully initialized.
//
while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED)
await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
//
// Set the update interval based on the user's settings.
//
this.updateInterval = this.settingsManager.ConfigurationData.App.UpdateBehavior switch
{
UpdateBehavior.NO_CHECK => Timeout.InfiniteTimeSpan,
@@ -45,16 +51,35 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
_ => TimeSpan.FromHours(1)
};
//
// When the user doesn't want to check for updates, we can
// return early.
//
if(this.settingsManager.ConfigurationData.App.UpdateBehavior is UpdateBehavior.NO_CHECK)
return;
//
// Check for updates at the beginning. The user aspects this when the app
// is started.
//
await this.CheckForUpdate();
//
// Start the update loop. This will check for updates based on the
// user's settings.
//
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(this.updateInterval, stoppingToken);
await this.CheckForUpdate();
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
this.messageBus.Unregister(this);
await base.StopAsync(cancellationToken);
}
#endregion
@@ -79,16 +104,6 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
#endregion
#region Overrides of BackgroundService
public override async Task StopAsync(CancellationToken cancellationToken)
{
this.messageBus.Unregister(this);
await base.StopAsync(cancellationToken);
}
#endregion
private async Task CheckForUpdate(bool notifyUserWhenNoUpdate = false)
{
if(!IS_INITIALIZED)
+11
View File
@@ -0,0 +1,11 @@
namespace AIStudio.Tools;
public enum TextColor
{
DEFAULT,
WARN,
ERROR,
SUCCESS,
INFO,
}
@@ -0,0 +1,18 @@
using AIStudio.Settings;
namespace AIStudio.Tools;
public static class TextColorExtensions
{
public static string GetHTMLColor(this TextColor color, SettingsManager settingsManager) => color switch
{
TextColor.DEFAULT => string.Empty,
TextColor.ERROR => settingsManager.IsDarkMode ? "#ff6c6c" : "#ff0000",
TextColor.WARN => settingsManager.IsDarkMode ? "#c7a009" : "#c7c000",
TextColor.SUCCESS => settingsManager.IsDarkMode ? "#08b342" : "#009933",
TextColor.INFO => settingsManager.IsDarkMode ? "#5279b8" : "#2d67c4",
_ => string.Empty,
};
}
@@ -1,3 +1,4 @@
using AIStudio.Settings.DataModel;
using AIStudio.Tools.ERIClient.DataModel;
namespace AIStudio.Tools.Validation;
@@ -11,6 +12,8 @@ public sealed class DataSourceValidation
public Func<IEnumerable<string>> GetUsedDataSourceNames { get; init; } = () => [];
public Func<AuthMethod> GetAuthMethod { get; init; } = () => AuthMethod.NONE;
public Func<SecurityRequirements?> GetSecurityRequirements { get; init; } = () => null;
public Func<bool> GetSelectedCloudEmbedding { get; init; } = () => false;
@@ -42,6 +45,21 @@ public sealed class DataSourceValidation
return null;
}
public string? ValidateSecurityPolicy(DataSourceSecurity securityPolicy)
{
if(securityPolicy is DataSourceSecurity.NOT_SPECIFIED)
return "Please select your security policy.";
var dataSourceSecurity = this.GetSecurityRequirements();
if (dataSourceSecurity is null)
return null;
if(dataSourceSecurity.Value.AllowedProviderType is ProviderType.SELF_HOSTED && securityPolicy is not DataSourceSecurity.SELF_HOSTED)
return "This data source can only be used with a self-hosted LLM provider. Please change the security policy.";
return null;
}
public string? ValidateUsername(string username)
{
if(this.GetAuthMethod() is not AuthMethod.USERNAME_PASSWORD)