mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-17 05:17:59 +00:00
Integrated data sources into any chat (#282)
This commit is contained in:
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
-1
@@ -1,6 +1,6 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
+1
-1
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user