From 63be312bb4fe11d1037382ae6a78313df101b51a Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 13 Jan 2025 19:51:26 +0100 Subject: [PATCH] Configure data sources (#259) --- app/ERIClientV1/Client.Generated.cs | 1277 +++++++++++++++++ app/ERIClientV1/ERIClientV1.csproj | 10 + app/MindWork AI Studio.sln | 11 + .../Assistants/ERI/AssistantERI.razor | 4 +- .../Assistants/ERI/AssistantERI.razor.cs | 11 + .../Components/SelectDirectory.razor | 1 + .../Components/SelectDirectory.razor.cs | 3 + .../Components/SelectFile.razor | 17 + .../Components/SelectFile.razor.cs | 63 + .../Settings/SettingsPanelDataSources.razor | 67 + .../SettingsPanelDataSources.razor.cs | 233 +++ .../Settings/SettingsPanelEmbeddings.razor.cs | 12 +- .../Dialogs/DataSourceERI_V1Dialog.razor | 128 ++ .../Dialogs/DataSourceERI_V1Dialog.razor.cs | 277 ++++ .../DataSourceLocalDirectoryDialog.razor | 79 + .../DataSourceLocalDirectoryDialog.razor.cs | 120 ++ .../Dialogs/DataSourceLocalFileDialog.razor | 78 + .../DataSourceLocalFileDialog.razor.cs | 121 ++ .../Dialogs/EmbeddingProviderDialog.razor.cs | 3 +- .../MindWork AI Studio.csproj | 4 + app/MindWork AI Studio/Pages/Settings.razor | 1 + .../Pages/Settings.razor.cs | 1 + .../Settings/DataModel/Data.cs | 10 + .../Settings/DataModel/DataSourceERI_V1.cs | 46 + .../DataModel/DataSourceLocalDirectory.cs | 31 + .../Settings/DataModel/DataSourceLocalFile.cs | 31 + .../Settings/DataModel/DataSourceType.cs | 27 + .../DataModel/DataSourceTypeExtension.cs | 24 + .../Settings/IDataSource.cs | 35 + .../Settings/IERIDataSource.cs | 12 + .../Settings/IExternalDataSource.cs | 16 + .../Settings/IInternalDataSource.cs | 9 + .../Tools/AuthMethodsV1Extensions.cs | 16 + .../Tools/Rust/FileSelectionResponse.cs | 8 + .../Tools/Rust/PreviousFile.cs | 7 + .../Tools/RustService.APIKeys.cs | 76 + .../Tools/RustService.App.cs | 120 ++ .../Tools/RustService.Clipboard.cs | 50 + .../Tools/RustService.FileSystem.cs | 32 + .../Tools/RustService.Secrets.cs | 76 + .../Tools/RustService.Updates.cs | 40 + app/MindWork AI Studio/Tools/RustService.cs | 279 +--- .../Tools/Validation/DataSourceValidation.cs | 149 ++ app/MindWork AI Studio/packages.lock.json | 3 + .../wwwroot/changelog/v0.9.26.md | 3 + runtime/src/app_window.rs | 49 + runtime/src/runtime_api.rs | 1 + 47 files changed, 3388 insertions(+), 283 deletions(-) create mode 100644 app/ERIClientV1/Client.Generated.cs create mode 100644 app/ERIClientV1/ERIClientV1.csproj create mode 100644 app/MindWork AI Studio/Components/SelectFile.razor create mode 100644 app/MindWork AI Studio/Components/SelectFile.razor.cs create mode 100644 app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor create mode 100644 app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor.cs create mode 100644 app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor create mode 100644 app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs create mode 100644 app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor create mode 100644 app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs create mode 100644 app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor create mode 100644 app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/DataSourceLocalDirectory.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/DataSourceLocalFile.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/DataSourceType.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs create mode 100644 app/MindWork AI Studio/Settings/IDataSource.cs create mode 100644 app/MindWork AI Studio/Settings/IERIDataSource.cs create mode 100644 app/MindWork AI Studio/Settings/IExternalDataSource.cs create mode 100644 app/MindWork AI Studio/Settings/IInternalDataSource.cs create mode 100644 app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs create mode 100644 app/MindWork AI Studio/Tools/Rust/FileSelectionResponse.cs create mode 100644 app/MindWork AI Studio/Tools/Rust/PreviousFile.cs create mode 100644 app/MindWork AI Studio/Tools/RustService.APIKeys.cs create mode 100644 app/MindWork AI Studio/Tools/RustService.App.cs create mode 100644 app/MindWork AI Studio/Tools/RustService.Clipboard.cs create mode 100644 app/MindWork AI Studio/Tools/RustService.FileSystem.cs create mode 100644 app/MindWork AI Studio/Tools/RustService.Secrets.cs create mode 100644 app/MindWork AI Studio/Tools/RustService.Updates.cs create mode 100644 app/MindWork AI Studio/Tools/Validation/DataSourceValidation.cs create mode 100644 app/MindWork AI Studio/wwwroot/changelog/v0.9.26.md diff --git a/app/ERIClientV1/Client.Generated.cs b/app/ERIClientV1/Client.Generated.cs new file mode 100644 index 0000000..96698e1 --- /dev/null +++ b/app/ERIClientV1/Client.Generated.cs @@ -0,0 +1,1277 @@ +//---------------------- +// +// Generated using the NSwag toolchain v14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// +//---------------------- + +#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." +#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." +#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' +#pragma warning disable 612 // Disable "CS0612 '...' is obsolete" +#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... +#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." +#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" +#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" +#pragma warning disable 8603 // Disable "CS8603 Possible null reference return" +#pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" +#pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" + +namespace ERI_Client.V1 +{ + using System = global::System; + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Client + { + private System.Net.Http.HttpClient _httpClient; + private static System.Lazy _settings = new System.Lazy(CreateSerializerSettings, true); + + public Client(System.Net.Http.HttpClient httpClient) + { + _httpClient = httpClient; + } + + private static System.Text.Json.JsonSerializerOptions CreateSerializerSettings() + { + var settings = new System.Text.Json.JsonSerializerOptions(); + UpdateJsonSerializerSettings(settings); + return settings; + } + + protected System.Text.Json.JsonSerializerOptions JsonSerializerSettings { get { return _settings.Value; } } + + static partial void UpdateJsonSerializerSettings(System.Text.Json.JsonSerializerOptions settings); + + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url); + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder); + partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response); + + /// + /// Get the available authentication methods. + /// + /// OK + /// A server side error occurred. + public virtual System.Threading.Tasks.Task> GetAuthMethodsAsync() + { + return GetAuthMethodsAsync(System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get the available authentication methods. + /// + /// OK + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task> GetAuthMethodsAsync(System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "auth/methods" + urlBuilder_.Append("auth/methods"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync>(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Authenticate with the data source to get a token for further requests. + /// + /// OK + /// A server side error occurred. + public virtual System.Threading.Tasks.Task AuthenticateAsync(AuthMethod authMethod) + { + return AuthenticateAsync(authMethod, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Authenticate with the data source to get a token for further requests. + /// + /// OK + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task AuthenticateAsync(AuthMethod authMethod, System.Threading.CancellationToken cancellationToken) + { + if (authMethod == null) + throw new System.ArgumentNullException("authMethod"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "auth" + urlBuilder_.Append("auth"); + urlBuilder_.Append('?'); + urlBuilder_.Append(System.Uri.EscapeDataString("authMethod")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(authMethod, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Get information about the data source. + /// + /// OK + /// A server side error occurred. + public virtual System.Threading.Tasks.Task GetDataSourceInfoAsync() + { + return GetDataSourceInfoAsync(System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get information about the data source. + /// + /// OK + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task GetDataSourceInfoAsync(System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "dataSource" + urlBuilder_.Append("dataSource"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Get information about the used embedding(s). + /// + /// OK + /// A server side error occurred. + public virtual System.Threading.Tasks.Task> GetEmbeddingInfoAsync() + { + return GetEmbeddingInfoAsync(System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get information about the used embedding(s). + /// + /// OK + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task> GetEmbeddingInfoAsync(System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "embedding/info" + urlBuilder_.Append("embedding/info"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync>(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Get information about the retrieval processes implemented by this data source. + /// + /// OK + /// A server side error occurred. + public virtual System.Threading.Tasks.Task> GetRetrievalInfoAsync() + { + return GetRetrievalInfoAsync(System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get information about the retrieval processes implemented by this data source. + /// + /// OK + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task> GetRetrievalInfoAsync(System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "retrieval/info" + urlBuilder_.Append("retrieval/info"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync>(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Retrieve information from the data source. + /// + /// OK + /// A server side error occurred. + public virtual System.Threading.Tasks.Task> RetrieveAsync(RetrievalRequest body) + { + return RetrieveAsync(body, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Retrieve information from the data source. + /// + /// OK + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task> RetrieveAsync(RetrievalRequest body, System.Threading.CancellationToken cancellationToken) + { + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "retrieval" + urlBuilder_.Append("retrieval"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync>(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Get the security requirements for this data source. + /// + /// OK + /// A server side error occurred. + public virtual System.Threading.Tasks.Task GetSecurityRequirementsAsync() + { + return GetSecurityRequirementsAsync(System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get the security requirements for this data source. + /// + /// OK + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task GetSecurityRequirementsAsync(System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "security/requirements" + urlBuilder_.Append("security/requirements"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + protected struct ObjectResponseResult + { + public ObjectResponseResult(T responseObject, string responseText) + { + this.Object = responseObject; + this.Text = responseText; + } + + public T Object { get; } + + public string Text { get; } + } + + public bool ReadResponseAsString { get; set; } + + protected virtual async System.Threading.Tasks.Task> ReadObjectResponseAsync(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Threading.CancellationToken cancellationToken) + { + if (response == null || response.Content == null) + { + return new ObjectResponseResult(default(T), string.Empty); + } + + if (ReadResponseAsString) + { + var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + try + { + var typedBody = System.Text.Json.JsonSerializer.Deserialize(responseText, JsonSerializerSettings); + return new ObjectResponseResult(typedBody, responseText); + } + catch (System.Text.Json.JsonException exception) + { + var message = "Could not deserialize the response body string as " + typeof(T).FullName + "."; + throw new ApiException(message, (int)response.StatusCode, responseText, headers, exception); + } + } + else + { + try + { + using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + { + var typedBody = await System.Text.Json.JsonSerializer.DeserializeAsync(responseStream, JsonSerializerSettings, cancellationToken).ConfigureAwait(false); + return new ObjectResponseResult(typedBody, string.Empty); + } + } + catch (System.Text.Json.JsonException exception) + { + var message = "Could not deserialize the response body stream as " + typeof(T).FullName + "."; + throw new ApiException(message, (int)response.StatusCode, string.Empty, headers, exception); + } + } + } + + private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo) + { + if (value == null) + { + return ""; + } + + if (value is System.Enum) + { + var name = System.Enum.GetName(value.GetType(), value); + if (name != null) + { + var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name); + if (field != null) + { + var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute)) + as System.Runtime.Serialization.EnumMemberAttribute; + if (attribute != null) + { + return attribute.Value != null ? attribute.Value : name; + } + } + + var converted = System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo)); + return converted == null ? string.Empty : converted; + } + } + else if (value is bool) + { + return System.Convert.ToString((bool)value, cultureInfo).ToLowerInvariant(); + } + else if (value is byte[]) + { + return System.Convert.ToBase64String((byte[]) value); + } + else if (value is string[]) + { + return string.Join(",", (string[])value); + } + else if (value.GetType().IsArray) + { + var valueArray = (System.Array)value; + var valueTextArray = new string[valueArray.Length]; + for (var i = 0; i < valueArray.Length; i++) + { + valueTextArray[i] = ConvertToString(valueArray.GetValue(i), cultureInfo); + } + return string.Join(",", valueTextArray); + } + + var result = System.Convert.ToString(value, cultureInfo); + return result == null ? "" : result; + } + } + + /// + /// An authentication field. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public enum AuthField + { + + [System.Runtime.Serialization.EnumMember(Value = @"NONE")] + NONE = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"USERNAME")] + USERNAME = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"PASSWORD")] + PASSWORD = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"TOKEN")] + TOKEN = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"KERBEROS_TICKET")] + KERBEROS_TICKET = 4, + + } + + /// + /// The mapping between an AuthField and the field name in the authentication request. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class AuthFieldMapping + { + + [System.Text.Json.Serialization.JsonPropertyName("authField")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public AuthField AuthField { get; set; } + + /// + /// The field name in the authentication request. + /// + + [System.Text.Json.Serialization.JsonPropertyName("fieldName")] + public string FieldName { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public enum AuthMethod + { + + [System.Runtime.Serialization.EnumMember(Value = @"NONE")] + NONE = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"KERBEROS")] + KERBEROS = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"USERNAME_PASSWORD")] + USERNAME_PASSWORD = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"TOKEN")] + TOKEN = 3, + + } + + /// + /// The response to an authentication request. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class AuthResponse + { + /// + /// True, when the authentication was successful. + /// + + [System.Text.Json.Serialization.JsonPropertyName("success")] + public bool Success { get; set; } + + /// + /// The token to use for further requests. + /// + + [System.Text.Json.Serialization.JsonPropertyName("token")] + public string Token { get; set; } + + /// + /// When the authentication was not successful, this contains the reason. + /// + + [System.Text.Json.Serialization.JsonPropertyName("message")] + public string Message { get; set; } + + } + + /// + /// Describes one authentication scheme for this data source. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class AuthScheme + { + + [System.Text.Json.Serialization.JsonPropertyName("authMethod")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public AuthMethod AuthMethod { get; set; } + + /// + /// A list of field mappings for the authentication method. The client must know, + ///
e.g., how the password field is named in the request. + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("authFieldMappings")] + public System.Collections.Generic.ICollection AuthFieldMappings { get; set; } + + } + + /// + /// A chat thread, which is a list of content blocks. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ChatThread + { + /// + /// The content blocks in this chat thread. + /// + + [System.Text.Json.Serialization.JsonPropertyName("contentBlocks")] + public System.Collections.Generic.ICollection ContentBlocks { get; set; } + + } + + /// + /// A block of content of a chat thread. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ContentBlock + { + /// + /// The content of the block. Remember that images and other media are base64 encoded. + /// + + [System.Text.Json.Serialization.JsonPropertyName("content")] + public string Content { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("role")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public Role Role { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("type")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public ContentType Type { get; set; } + + } + + /// + /// The type of content. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public enum ContentType + { + + [System.Runtime.Serialization.EnumMember(Value = @"NONE")] + NONE = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"UNKNOWN")] + UNKNOWN = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"TEXT")] + TEXT = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"IMAGE")] + IMAGE = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"VIDEO")] + VIDEO = 4, + + [System.Runtime.Serialization.EnumMember(Value = @"AUDIO")] + AUDIO = 5, + + [System.Runtime.Serialization.EnumMember(Value = @"SPEECH")] + SPEECH = 6, + + } + + /// + /// Matching context returned by the data source as a result of a retrieval request. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Context + { + /// + /// The name of the source, e.g., a document name, database name, + ///
collection name, etc. + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// What are the contents of the source? For example, is it a + ///
dictionary, a book chapter, business concept, a paper, etc. + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("category")] + public string Category { get; set; } + + /// + /// The path to the content, e.g., a URL, a file path, a path in a + ///
graph database, etc. + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("path")] + public string Path { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("type")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public ContentType Type { get; set; } + + /// + /// The content that matched the user prompt. For text, you + ///
return the matched text and, e.g., three words before and after it. + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("matchedContent")] + public string MatchedContent { get; set; } + + /// + /// The surrounding content of the matched content. + ///
For text, you may return, e.g., one sentence or paragraph before and after + ///
the matched content. + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("surroundingContent")] + public System.Collections.Generic.ICollection SurroundingContent { get; set; } + + /// + /// Links to related content, e.g., links to Wikipedia articles, + ///
links to sources, etc. + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("links")] + public System.Collections.Generic.ICollection Links { get; set; } + + } + + /// + /// Information about the data source. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class DataSourceInfo + { + /// + /// The name of the data source, e.g., "Internal Organization Documents." + /// + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// A short description of the data source. What kind of data does it contain? + ///
What is the data source used for? + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("description")] + public string Description { get; set; } + + } + + /// + /// Represents information about the used embedding for this data source. The purpose of this information is to give the + ///
interested user an idea of what kind of embedding is used and what it does. + ///
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class EmbeddingInfo + { + /// + /// What kind of embedding is used. For example, "Transformer Embedding," "Contextual Word + ///
Embedding," "Graph Embedding," etc. + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("embeddingType")] + public string EmbeddingType { get; set; } + + /// + /// Name the embedding used. This can be a library, a framework, or the name of the used + ///
algorithm. + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("embeddingName")] + public string EmbeddingName { get; set; } + + /// + /// A short description of the embedding. Describe what the embedding is doing. + /// + + [System.Text.Json.Serialization.JsonPropertyName("description")] + public string Description { get; set; } + + /// + /// Describe when the embedding is used. For example, when the user prompt contains certain + ///
keywords, or anytime? + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("usedWhen")] + public string UsedWhen { get; set; } + + /// + /// A link to the embedding's documentation or the source code. Might be null. + /// + + [System.Text.Json.Serialization.JsonPropertyName("link")] + public string Link { get; set; } + + } + + /// + /// Known types of providers that can process data. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public enum ProviderType + { + + [System.Runtime.Serialization.EnumMember(Value = @"NONE")] + NONE = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"ANY")] + ANY = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"SELF_HOSTED")] + SELF_HOSTED = 2, + + } + + /// + /// Information about a retrieval process, which this data source implements. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RetrievalInfo + { + /// + /// A unique identifier for the retrieval process. This can be a GUID, a unique name, or an increasing integer. + /// + + [System.Text.Json.Serialization.JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// The name of the retrieval process, e.g., "Keyword-Based Wikipedia Article Retrieval". + /// + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// A short description of the retrieval process. What kind of retrieval process is it? + /// + + [System.Text.Json.Serialization.JsonPropertyName("description")] + public string Description { get; set; } + + /// + /// A link to the retrieval process's documentation, paper, Wikipedia article, or the source code. Might be null. + /// + + [System.Text.Json.Serialization.JsonPropertyName("link")] + public string Link { get; set; } + + /// + /// A dictionary that describes the parameters of the retrieval process. The key is the parameter name, + ///
and the value is a description of the parameter. Although each parameter will be sent as a string, the description should indicate the + ///
expected type and range, e.g., 0.0 to 1.0 for a float parameter. + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("parametersDescription")] + public System.Collections.Generic.IDictionary ParametersDescription { get; set; } + + /// + /// A list of embeddings used in this retrieval process. It might be empty in case no embedding is used. + /// + + [System.Text.Json.Serialization.JsonPropertyName("embeddings")] + public System.Collections.Generic.ICollection Embeddings { get; set; } + + } + + /// + /// The retrieval request sent by AI Studio. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RetrievalRequest + { + /// + /// The latest user prompt that AI Studio received. + /// + + [System.Text.Json.Serialization.JsonPropertyName("latestUserPrompt")] + public string LatestUserPrompt { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("latestUserPromptType")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public ContentType LatestUserPromptType { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("thread")] + public ChatThread Thread { get; set; } + + /// + /// Optional. The ID of the retrieval process that the data source should use. + ///
When null, the data source chooses an appropriate retrieval process. Selecting a retrieval process is optional + ///
for AI Studio users. Most users do not specify a retrieval process. + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("retrievalProcessId")] + public string RetrievalProcessId { get; set; } + + /// + /// A dictionary of parameters that the data source should use for the retrieval process. + ///
Although each parameter will be sent as a string, the retrieval process specifies the expected type and range. + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("parameters")] + public System.Collections.Generic.IDictionary Parameters { get; set; } + + /// + /// The maximum number of matches that the data source should return. AI Studio uses + ///
any value below 1 to indicate that the data source should return as many matches as appropriate. + ///
+ + [System.Text.Json.Serialization.JsonPropertyName("maxMatches")] + public int MaxMatches { get; set; } + + } + + /// + /// Possible roles of any chat thread. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public enum Role + { + + [System.Runtime.Serialization.EnumMember(Value = @"NONE")] + NONE = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"UNKNOW")] + UNKNOW = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"SYSTEM")] + SYSTEM = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"USER")] + USER = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"AI")] + AI = 4, + + [System.Runtime.Serialization.EnumMember(Value = @"AGENT")] + AGENT = 5, + + } + + /// + /// Represents the security requirements for this data source. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class SecurityRequirements + { + + [System.Text.Json.Serialization.JsonPropertyName("allowedProviderType")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public ProviderType AllowedProviderType { get; set; } + + } + + + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ApiException : System.Exception + { + public int StatusCode { get; private set; } + + public string Response { get; private set; } + + public System.Collections.Generic.IReadOnlyDictionary> Headers { get; private set; } + + public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Exception innerException) + : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + ((response == null) ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length)), innerException) + { + StatusCode = statusCode; + Response = response; + Headers = headers; + } + + public override string ToString() + { + return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString()); + } + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ApiException : ApiException + { + public TResult Result { get; private set; } + + public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary> headers, TResult result, System.Exception innerException) + : base(message, statusCode, response, headers, innerException) + { + Result = result; + } + } + +} + +#pragma warning restore 108 +#pragma warning restore 114 +#pragma warning restore 472 +#pragma warning restore 612 +#pragma warning restore 1573 +#pragma warning restore 1591 +#pragma warning restore 8073 +#pragma warning restore 3016 +#pragma warning restore 8603 +#pragma warning restore 8604 +#pragma warning restore 8625 \ No newline at end of file diff --git a/app/ERIClientV1/ERIClientV1.csproj b/app/ERIClientV1/ERIClientV1.csproj new file mode 100644 index 0000000..8ca0662 --- /dev/null +++ b/app/ERIClientV1/ERIClientV1.csproj @@ -0,0 +1,10 @@ + + + + net8.0 + latest + enable + enable + + + diff --git a/app/MindWork AI Studio.sln b/app/MindWork AI Studio.sln index 696edae..6bf20b2 100644 --- a/app/MindWork AI Studio.sln +++ b/app/MindWork AI Studio.sln @@ -2,6 +2,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindWork AI Studio\MindWork AI Studio.csproj", "{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ERIClients", "ERIClients", "{5C2AF789-287B-4FCB-B675-7273D8CD4579}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ERIClientV1", "ERIClientV1\ERIClientV1.csproj", "{9E35A273-0FA6-4BD5-8880-A1DDAC106926}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +16,12 @@ Global {059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU {059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU {059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.Build.0 = Release|Any CPU + {9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {9E35A273-0FA6-4BD5-8880-A1DDAC106926} = {5C2AF789-287B-4FCB-B675-7273D8CD4579} EndGlobalSection EndGlobal diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor index 5e008e2..19ad57c 100644 --- a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor +++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor @@ -52,7 +52,7 @@ else Add ERI server preset - + Delete this server preset @@ -346,4 +346,4 @@ else - + diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs index 8534731..ca5b4f9 100644 --- a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs +++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs @@ -739,6 +739,17 @@ public partial class AssistantERI : AssistantBaseCore return null; } + + private string? ValidateDirectory(string path) + { + if(!this.writeToFilesystem) + return null; + + if(string.IsNullOrWhiteSpace(path)) + return "Please provide a base directory for the ERI server to write files to."; + + return null; + } private string GetMultiSelectionAuthText(List selectedValues) { diff --git a/app/MindWork AI Studio/Components/SelectDirectory.razor b/app/MindWork AI Studio/Components/SelectDirectory.razor index 95f09d6..29a0fc8 100644 --- a/app/MindWork AI Studio/Components/SelectDirectory.razor +++ b/app/MindWork AI Studio/Components/SelectDirectory.razor @@ -4,6 +4,7 @@ Text="@this.Directory" Label="@this.Label" ReadOnly="@true" + Validation="@this.Validation" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Folder" UserAttributes="@SPELLCHECK_ATTRIBUTES" diff --git a/app/MindWork AI Studio/Components/SelectDirectory.razor.cs b/app/MindWork AI Studio/Components/SelectDirectory.razor.cs index ec4f6cd..79bc18e 100644 --- a/app/MindWork AI Studio/Components/SelectDirectory.razor.cs +++ b/app/MindWork AI Studio/Components/SelectDirectory.razor.cs @@ -21,6 +21,9 @@ public partial class SelectDirectory : ComponentBase [Parameter] public string DirectoryDialogTitle { get; set; } = "Select Directory"; + [Parameter] + public Func Validation { get; set; } = _ => null; + [Inject] private SettingsManager SettingsManager { get; init; } = null!; diff --git a/app/MindWork AI Studio/Components/SelectFile.razor b/app/MindWork AI Studio/Components/SelectFile.razor new file mode 100644 index 0000000..3484236 --- /dev/null +++ b/app/MindWork AI Studio/Components/SelectFile.razor @@ -0,0 +1,17 @@ + + + + + Choose File + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/SelectFile.razor.cs b/app/MindWork AI Studio/Components/SelectFile.razor.cs new file mode 100644 index 0000000..5d1b7f0 --- /dev/null +++ b/app/MindWork AI Studio/Components/SelectFile.razor.cs @@ -0,0 +1,63 @@ +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public partial class SelectFile : ComponentBase +{ + [Parameter] + public string File { get; set; } = string.Empty; + + [Parameter] + public EventCallback FileChanged { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string Label { get; set; } = string.Empty; + + [Parameter] + public string FileDialogTitle { get; set; } = "Select File"; + + [Parameter] + public Func Validation { get; set; } = _ => null; + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + [Inject] + public RustService RustService { get; set; } = null!; + + [Inject] + protected ILogger Logger { get; init; } = null!; + + private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + // Configure the spellchecking for the instance name input: + this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); + await base.OnInitializedAsync(); + } + + #endregion + + private void InternalFileChanged(string file) + { + this.File = file; + this.FileChanged.InvokeAsync(file); + } + + private async Task OpenFileDialog() + { + var response = await this.RustService.SelectFile(this.FileDialogTitle, string.IsNullOrWhiteSpace(this.File) ? null : this.File); + this.Logger.LogInformation($"The user selected the file '{response.SelectedFilePath}'."); + + if (!response.UserCancelled) + this.InternalFileChanged(response.SelectedFilePath); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor new file mode 100644 index 0000000..3f19bf9 --- /dev/null +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor @@ -0,0 +1,67 @@ +@using AIStudio.Settings +@using AIStudio.Settings.DataModel +@inherits SettingsPanelBase + +@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) +{ + + + + Configured Data Sources + + + You might configure different data sources. A data source can include one file, all files + in a directory, or data from your company. Later, you can incorporate these data sources + as needed when the AI requires this data to complete a certain task. + + + + + + + + + + + + # + Name + Type + Embedding + Actions + + + @context.Num + @context.Name + @context.Type.GetDisplayName() + @this.GetEmbeddingName(context) + + + @if (context is IERIDataSource) + { + @* *@ + @* Show Information *@ + @* *@ + } + + Edit + + + Delete + + + + + + @if (this.SettingsManager.ConfigurationData.DataSources.Count == 0) + { + No data sources configured yet. + } + + + External Data (ERI-Server v1) + Local Directory + Local File + + +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor.cs new file mode 100644 index 0000000..0312452 --- /dev/null +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor.cs @@ -0,0 +1,233 @@ +using AIStudio.Dialogs; +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +using ERI_Client.V1; + +using Microsoft.AspNetCore.Components; + +using DialogOptions = AIStudio.Dialogs.DialogOptions; + +namespace AIStudio.Components.Settings; + +public partial class SettingsPanelDataSources : SettingsPanelBase +{ + [Parameter] + public List> AvailableDataSources { get; set; } = new(); + + [Parameter] + public EventCallback>> AvailableDataSourcesChanged { get; set; } + + [Parameter] + public Func>> AvailableEmbeddingsFunc { get; set; } = () => []; + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + await this.UpdateDataSources(); + await base.OnInitializedAsync(); + } + + #endregion + + private string GetEmbeddingName(IDataSource dataSource) + { + if(dataSource is IInternalDataSource internalDataSource) + { + var matchedEmbedding = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == internalDataSource.EmbeddingId); + if(matchedEmbedding == default) + return "No valid embedding"; + + return matchedEmbedding.Name; + } + + if(dataSource is IExternalDataSource) + return "External (ERI)"; + + return "Unknown"; + } + + private async Task AddDataSource(DataSourceType type) + { + IDataSource? addedDataSource = null; + switch (type) + { + case DataSourceType.LOCAL_FILE: + var localFileDialogParameters = new DialogParameters + { + { x => x.IsEditing, false }, + { x => x.AvailableEmbeddings, this.AvailableEmbeddingsFunc() } + }; + + var localFileDialogReference = await this.DialogService.ShowAsync("Add Local File as Data Source", localFileDialogParameters, DialogOptions.FULLSCREEN); + var localFileDialogResult = await localFileDialogReference.Result; + if (localFileDialogResult is null || localFileDialogResult.Canceled) + return; + + var localFile = (DataSourceLocalFile)localFileDialogResult.Data!; + localFile = localFile with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ }; + addedDataSource = localFile; + break; + + case DataSourceType.LOCAL_DIRECTORY: + var localDirectoryDialogParameters = new DialogParameters + { + { x => x.IsEditing, false }, + { x => x.AvailableEmbeddings, this.AvailableEmbeddingsFunc() } + }; + + var localDirectoryDialogReference = await this.DialogService.ShowAsync("Add Local Directory as Data Source", localDirectoryDialogParameters, DialogOptions.FULLSCREEN); + var localDirectoryDialogResult = await localDirectoryDialogReference.Result; + if (localDirectoryDialogResult is null || localDirectoryDialogResult.Canceled) + return; + + var localDirectory = (DataSourceLocalDirectory)localDirectoryDialogResult.Data!; + localDirectory = localDirectory with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ }; + addedDataSource = localDirectory; + break; + + case DataSourceType.ERI_V1: + var eriDialogParameters = new DialogParameters + { + { x => x.IsEditing, false }, + }; + + var eriDialogReference = await this.DialogService.ShowAsync("Add ERI v1 Data Source", eriDialogParameters, DialogOptions.FULLSCREEN); + var eriDialogResult = await eriDialogReference.Result; + if (eriDialogResult is null || eriDialogResult.Canceled) + return; + + var eriDataSource = (DataSourceERI_V1)eriDialogResult.Data!; + eriDataSource = eriDataSource with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ }; + addedDataSource = eriDataSource; + break; + } + + if(addedDataSource is null) + return; + + this.SettingsManager.ConfigurationData.DataSources.Add(addedDataSource); + await this.UpdateDataSources(); + await this.SettingsManager.StoreSettings(); + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + } + + private async Task EditDataSource(IDataSource dataSource) + { + IDataSource? editedDataSource = null; + switch (dataSource) + { + case DataSourceLocalFile localFile: + var localFileDialogParameters = new DialogParameters + { + { x => x.IsEditing, true }, + { x => x.DataSource, localFile }, + { x => x.AvailableEmbeddings, this.AvailableEmbeddingsFunc() } + }; + + var localFileDialogReference = await this.DialogService.ShowAsync("Edit Local File Data Source", localFileDialogParameters, DialogOptions.FULLSCREEN); + var localFileDialogResult = await localFileDialogReference.Result; + if (localFileDialogResult is null || localFileDialogResult.Canceled) + return; + + editedDataSource = (DataSourceLocalFile)localFileDialogResult.Data!; + break; + + case DataSourceLocalDirectory localDirectory: + var localDirectoryDialogParameters = new DialogParameters + { + { x => x.IsEditing, true }, + { x => x.DataSource, localDirectory }, + { x => x.AvailableEmbeddings, this.AvailableEmbeddingsFunc() } + }; + + var localDirectoryDialogReference = await this.DialogService.ShowAsync("Edit Local Directory Data Source", localDirectoryDialogParameters, DialogOptions.FULLSCREEN); + var localDirectoryDialogResult = await localDirectoryDialogReference.Result; + if (localDirectoryDialogResult is null || localDirectoryDialogResult.Canceled) + return; + + editedDataSource = (DataSourceLocalDirectory)localDirectoryDialogResult.Data!; + break; + + case DataSourceERI_V1 eriDataSource: + var eriDialogParameters = new DialogParameters + { + { x => x.IsEditing, true }, + { x => x.DataSource, eriDataSource }, + }; + + var eriDialogReference = await this.DialogService.ShowAsync("Edit ERI v1 Data Source", eriDialogParameters, DialogOptions.FULLSCREEN); + var eriDialogResult = await eriDialogReference.Result; + if (eriDialogResult is null || eriDialogResult.Canceled) + return; + + editedDataSource = (DataSourceERI_V1)eriDialogResult.Data!; + break; + } + + if(editedDataSource is null) + return; + + this.SettingsManager.ConfigurationData.DataSources[this.SettingsManager.ConfigurationData.DataSources.IndexOf(dataSource)] = editedDataSource; + + await this.UpdateDataSources(); + await this.SettingsManager.StoreSettings(); + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + } + + private async Task DeleteDataSource(IDataSource dataSource) + { + var dialogParameters = new DialogParameters + { + { "Message", $"Are you sure you want to delete the data source '{dataSource.Name}' of type {dataSource.Type.GetDisplayName()}?" }, + }; + + var dialogReference = await this.DialogService.ShowAsync("Delete Data Source", dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + var applyChanges = dataSource is IInternalDataSource; + + // External data sources may need a secret for authentication: + if (dataSource is IExternalDataSource externalDataSource) + { + // When the auth method is NONE or KERBEROS, we don't need to delete a secret. + // In the case of KERBEROS, we don't store the Kerberos ticket in the secret store. + if(dataSource is IERIDataSource { AuthMethod: AuthMethod.NONE or AuthMethod.KERBEROS }) + applyChanges = true; + + // All other auth methods require a secret, which we need to delete now: + else + { + var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource); + if (deleteSecretResponse.Success) + applyChanges = true; + } + } + + if(applyChanges) + { + this.SettingsManager.ConfigurationData.DataSources.Remove(dataSource); + await this.SettingsManager.StoreSettings(); + await this.UpdateDataSources(); + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + } + } + + private Task ShowInformation(IDataSource dataSource) + { + #warning Implement the information dialog for ERI data sources. + return Task.CompletedTask; + } + + private async Task UpdateDataSources() + { + this.AvailableDataSources.Clear(); + foreach (var dataSource in this.SettingsManager.ConfigurationData.DataSources) + this.AvailableDataSources.Add(new (dataSource.Name, dataSource.Id)); + + await this.AvailableDataSourcesChanged.InvokeAsync(this.AvailableDataSources); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs index afa5024..7520e59 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs @@ -21,7 +21,17 @@ public partial class SettingsPanelEmbeddings : SettingsPanelBase var modelName = provider.Model.ToString(); return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName; } - + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + await this.UpdateEmbeddingProviders(); + await base.OnInitializedAsync(); + } + + #endregion + private async Task AddEmbeddingProvider() { var dialogParameters = new DialogParameters diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor new file mode 100644 index 0000000..64e307b --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor @@ -0,0 +1,128 @@ +@using ERI_Client.V1 + + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + + + + @if (!this.IsConnectionEncrypted()) + { + + Please note: the connection to the ERI v1 server is not encrypted. This means that all + data sent to the server is transmitted in plain text. Please ask the ERI server administrator + to enable encryption. + + } + + @if (this.IsConnectionPossible()) + { + + + Test connection & read available metadata + + + @this.GetTestResultText() + + + } + + @if(this.availableAuthMethods.Count > 0 || this.dataAuthMethod != default) + { + + @foreach (var authMethod in this.availableAuthMethods) + { + @authMethod.DisplayName() + } + + } + + @if (this.NeedsSecret()) + { + if (this.dataAuthMethod is AuthMethod.USERNAME_PASSWORD) + { + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + } + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + } + + + + + + Cancel + + @if(this.IsEditing) + { + @:Update + } + else + { + @:Add + } + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs new file mode 100644 index 0000000..1e4eaf7 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs @@ -0,0 +1,277 @@ +using AIStudio.Settings; +using AIStudio.Settings.DataModel; +using AIStudio.Tools.Validation; + +using ERI_Client.V1; + +using Microsoft.AspNetCore.Components; + +// ReSharper disable InconsistentNaming +namespace AIStudio.Dialogs; + +public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId +{ + [CascadingParameter] + private MudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public bool IsEditing { get; set; } + + [Parameter] + public DataSourceERI_V1 DataSource { get; set; } + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + [Inject] + private ILogger Logger { get; init; } = null!; + + [Inject] + private RustService RustService { get; init; } = null!; + + private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); + + private readonly DataSourceValidation dataSourceValidation; + private readonly Encryption encryption = Program.ENCRYPTION; + + /// + /// The list of used data source names. We need this to check for uniqueness. + /// + private List UsedDataSourcesNames { get; set; } = []; + + private bool dataIsValid; + private string[] dataIssues = []; + private string dataSecretStorageIssue = string.Empty; + private string dataEditingPreviousInstanceName = string.Empty; + private HttpClient? httpClient; + private List availableAuthMethods = []; + private bool connectionTested; + private bool connectionSuccessfulTested; + + private uint dataNum; + private string dataSecret = string.Empty; + private string dataId = Guid.NewGuid().ToString(); + private string dataName = string.Empty; + private string dataHostname = string.Empty; + private int dataPort; + private AuthMethod dataAuthMethod; + private string dataUsername = string.Empty; + + // We get the form reference from Blazor code to validate it manually: + private MudForm form = null!; + + public DataSourceERI_V1Dialog() + { + this.dataSourceValidation = new() + { + GetAuthMethod = () => this.dataAuthMethod, + GetPreviousDataSourceName = () => this.dataEditingPreviousInstanceName, + GetUsedDataSourceNames = () => this.UsedDataSourcesNames, + GetSecretStorageIssue = () => this.dataSecretStorageIssue, + GetTestedConnection = () => this.connectionTested, + GetTestedConnectionResult = () => this.connectionSuccessfulTested, + GetAvailableAuthMethods = () => this.availableAuthMethods, + }; + } + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + // Configure the spellchecking for the instance name input: + this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); + + // Load the used instance names: + this.UsedDataSourcesNames = this.SettingsManager.ConfigurationData.DataSources.Select(x => x.Name.ToLowerInvariant()).ToList(); + + // When editing, we need to load the data: + if(this.IsEditing) + { + this.dataEditingPreviousInstanceName = this.DataSource.Name.ToLowerInvariant(); + this.dataNum = this.DataSource.Num; + this.dataId = this.DataSource.Id; + this.dataName = this.DataSource.Name; + this.dataHostname = this.DataSource.Hostname; + this.dataPort = this.DataSource.Port; + this.dataAuthMethod = this.DataSource.AuthMethod; + this.dataUsername = this.DataSource.Username; + + if (this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD) + { + // Load the secret: + var requestedSecret = await this.RustService.GetSecret(this); + if (requestedSecret.Success) + this.dataSecret = await requestedSecret.Secret.Decrypt(this.encryption); + else + { + this.dataSecret = string.Empty; + this.dataSecretStorageIssue = $"Failed to load the auth. secret from the operating system. The message was: {requestedSecret.Issue}. You might ignore this message and provide the secret again."; + await this.form.Validate(); + } + } + } + + await base.OnInitializedAsync(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + // Reset the validation when not editing and on the first render. + // We don't want to show validation errors when the user opens the dialog. + if(!this.IsEditing && firstRender) + this.form.ResetValidation(); + + await base.OnAfterRenderAsync(firstRender); + } + + #endregion + + #region Implementation of ISecretId + + public string SecretId => this.dataId; + + public string SecretName => this.dataName; + + #endregion + + private DataSourceERI_V1 CreateDataSource() + { + var cleanedHostname = this.dataHostname.Trim(); + return new DataSourceERI_V1 + { + Id = this.dataId, + Num = this.dataNum, + Port = this.dataPort, + Name = this.dataName, + Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, + AuthMethod = this.dataAuthMethod, + Username = this.dataUsername, + Type = DataSourceType.ERI_V1, + }; + } + + private bool IsConnectionEncrypted() => this.dataHostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase); + + private bool IsConnectionPossible() + { + if(this.dataSourceValidation.ValidatingHostname(this.dataHostname) is not null) + return false; + + if(this.dataSourceValidation.ValidatePort(this.dataPort) is not null) + return false; + + return true; + } + + private async Task TestConnection() + { + try + { + this.httpClient = new HttpClient + { + BaseAddress = new Uri($"{this.dataHostname}:{this.dataPort}"), + Timeout = TimeSpan.FromSeconds(5), + }; + + using (this.httpClient) + { + var client = new Client(this.httpClient); + var authSchemes = await client.GetAuthMethodsAsync(); + if (authSchemes is null) + { + await this.form.Validate(); + + Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1); + this.dataIssues[^1] = "Failed to connect to the ERI v1 server. The server did not respond."; + return; + } + + this.availableAuthMethods = authSchemes.Select(n => n.AuthMethod).ToList(); + + this.connectionTested = true; + this.connectionSuccessfulTested = true; + this.Logger.LogInformation("Connection to the ERI v1 server was successful tested."); + } + } + catch (Exception e) + { + await this.form.Validate(); + + Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1); + this.dataIssues[^1] = $"Failed to connect to the ERI v1 server. The message was: {e.Message}"; + this.Logger.LogError($"Failed to connect to the ERI v1 server. Message: {e.Message}"); + + this.connectionTested = true; + this.connectionSuccessfulTested = false; + } + } + + private string GetTestResultText() + { + if(!this.connectionTested) + return "Not tested yet."; + + return this.connectionSuccessfulTested ? "Connection successful." : "Connection failed."; + } + + private Color GetTestResultColor() + { + if (!this.connectionTested) + return Color.Default; + + return this.connectionSuccessfulTested ? Color.Success : Color.Error; + } + + private string GetTestResultIcon() + { + if (!this.connectionTested) + return Icons.Material.Outlined.HourglassEmpty; + + return this.connectionSuccessfulTested ? Icons.Material.Outlined.CheckCircle : Icons.Material.Outlined.Error; + } + + private bool NeedsSecret() => this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD; + + private string GetSecretLabel() => this.dataAuthMethod switch + { + AuthMethod.TOKEN => "Access Token", + AuthMethod.USERNAME_PASSWORD => "Password", + _ => "Secret", + }; + + private async Task Store() + { + await this.form.Validate(); + + var testConnectionValidation = this.dataSourceValidation.ValidateTestedConnection(); + if(testConnectionValidation is not null) + { + Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1); + this.dataIssues[^1] = testConnectionValidation; + this.dataIsValid = false; + } + + this.dataSecretStorageIssue = string.Empty; + + // When the data is not valid, we don't store it: + if (!this.dataIsValid) + return; + + var addedDataSource = this.CreateDataSource(); + if (!string.IsNullOrWhiteSpace(this.dataSecret)) + { + // Store the secret in the OS secure storage: + var storeResponse = await this.RustService.SetSecret(this, this.dataSecret); + if (!storeResponse.Success) + { + this.dataSecretStorageIssue = $"Failed to store the auth. secret in the operating system. The message was: {storeResponse.Issue}. Please try again."; + await this.form.Validate(); + return; + } + } + + this.MudDialog.Close(DialogResult.Ok(addedDataSource)); + } + + private void Cancel() => this.MudDialog.Cancel(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor new file mode 100644 index 0000000..22d1c98 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor @@ -0,0 +1,79 @@ + + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + + Select a root directory for this data source. All data in this directory and all + its subdirectories will be processed for this data source. + + + + + In order for the AI to be able to determine the appropriate data at any time, you must + choose an embedding method. + + + @foreach (var embedding in this.AvailableEmbeddings) + { + @embedding.Name + } + + + @if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId)) + { + if (this.SelectedCloudEmbedding) + { + + @if (string.IsNullOrWhiteSpace(this.dataPath)) + { + @: Please note: the embedding you selected runs in the cloud. All your data will be sent to the cloud. + @: Please confirm that you have read and understood this. + } + else + { + @: Please note: the embedding you selected runs in the cloud. All your data from the + @: folder '@this.dataPath' and all its subdirectories will be sent to the cloud. Please + @: confirm that you have read and understood this. + } + + + } + else + { + + The embedding you selected runs locally or in your organization. Your data is not sent to the cloud. + + } + } + + + + + Cancel + + @if(this.IsEditing) + { + @:Update + } + else + { + @:Add + } + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs new file mode 100644 index 0000000..9f1b4f1 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs @@ -0,0 +1,120 @@ +using AIStudio.Settings; +using AIStudio.Settings.DataModel; +using AIStudio.Tools.Validation; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +public partial class DataSourceLocalDirectoryDialog : ComponentBase +{ + [CascadingParameter] + private MudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public bool IsEditing { get; set; } + + [Parameter] + public DataSourceLocalDirectory DataSource { get; set; } + + [Parameter] + public IReadOnlyList> AvailableEmbeddings { get; set; } = []; + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); + + private readonly DataSourceValidation dataSourceValidation; + + /// + /// The list of used data source names. We need this to check for uniqueness. + /// + private List UsedDataSourcesNames { get; set; } = []; + + private bool dataIsValid; + private string[] dataIssues = []; + private string dataEditingPreviousInstanceName = string.Empty; + + private uint dataNum; + private string dataId = Guid.NewGuid().ToString(); + private string dataName = string.Empty; + private bool dataUserAcknowledgedCloudEmbedding; + private string dataEmbeddingId = string.Empty; + private string dataPath = string.Empty; + + // We get the form reference from Blazor code to validate it manually: + private MudForm form = null!; + + public DataSourceLocalDirectoryDialog() + { + this.dataSourceValidation = new() + { + GetSelectedCloudEmbedding = () => this.SelectedCloudEmbedding, + GetPreviousDataSourceName = () => this.dataEditingPreviousInstanceName, + GetUsedDataSourceNames = () => this.UsedDataSourcesNames, + }; + } + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + // Configure the spellchecking for the instance name input: + this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); + + // Load the used instance names: + this.UsedDataSourcesNames = this.SettingsManager.ConfigurationData.DataSources.Select(x => x.Name.ToLowerInvariant()).ToList(); + + // When editing, we need to load the data: + if(this.IsEditing) + { + this.dataEditingPreviousInstanceName = this.DataSource.Name.ToLowerInvariant(); + this.dataNum = this.DataSource.Num; + this.dataId = this.DataSource.Id; + this.dataName = this.DataSource.Name; + this.dataEmbeddingId = this.DataSource.EmbeddingId; + this.dataPath = this.DataSource.Path; + } + + await base.OnInitializedAsync(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + // Reset the validation when not editing and on the first render. + // We don't want to show validation errors when the user opens the dialog. + if(!this.IsEditing && firstRender) + this.form.ResetValidation(); + + await base.OnAfterRenderAsync(firstRender); + } + + #endregion + + private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId).IsSelfHosted; + + private DataSourceLocalDirectory CreateDataSource() => new() + { + Id = this.dataId, + Num = this.dataNum, + Name = this.dataName, + Type = DataSourceType.LOCAL_DIRECTORY, + EmbeddingId = this.dataEmbeddingId, + Path = this.dataPath, + }; + + private async Task Store() + { + await this.form.Validate(); + + // When the data is not valid, we don't store it: + if (!this.dataIsValid) + return; + + var addedDataSource = this.CreateDataSource(); + this.MudDialog.Close(DialogResult.Ok(addedDataSource)); + } + + private void Cancel() => this.MudDialog.Cancel(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor new file mode 100644 index 0000000..c60d32d --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor @@ -0,0 +1,78 @@ + + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + + Select a file for this data source. The content of this file will be processed for the data source. + + + + + In order for the AI to be able to determine the appropriate data at any time, you must + choose an embedding method. + + + @foreach (var embedding in this.AvailableEmbeddings) + { + @embedding.Name + } + + + @if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId)) + { + if (this.SelectedCloudEmbedding) + { + + @if (string.IsNullOrWhiteSpace(this.dataFilePath)) + { + @: Please note: the embedding you selected runs in the cloud. All your data will be sent to the cloud. + @: Please confirm that you have read and understood this. + } + else + { + @: Please note: the embedding you selected runs in the cloud. All your data within the + @: file '@this.dataFilePath' will be sent to the cloud. Please confirm that you have read + @: and understood this. + } + + + } + else + { + + The embedding you selected runs locally or in your organization. Your data is not sent to the cloud. + + } + } + + + + + Cancel + + @if(this.IsEditing) + { + @:Update + } + else + { + @:Add + } + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs new file mode 100644 index 0000000..feaf5a7 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs @@ -0,0 +1,121 @@ +using AIStudio.Settings; +using AIStudio.Settings.DataModel; +using AIStudio.Tools.Validation; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +public partial class DataSourceLocalFileDialog : ComponentBase +{ + [CascadingParameter] + private MudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public bool IsEditing { get; set; } + + [Parameter] + public DataSourceLocalFile DataSource { get; set; } + + [Parameter] + public IReadOnlyList> AvailableEmbeddings { get; set; } = []; + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + + private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); + + private readonly DataSourceValidation dataSourceValidation; + + /// + /// The list of used data source names. We need this to check for uniqueness. + /// + private List UsedDataSourcesNames { get; set; } = []; + + private bool dataIsValid; + private string[] dataIssues = []; + private string dataEditingPreviousInstanceName = string.Empty; + + private uint dataNum; + private string dataId = Guid.NewGuid().ToString(); + private string dataName = string.Empty; + private bool dataUserAcknowledgedCloudEmbedding; + private string dataEmbeddingId = string.Empty; + private string dataFilePath = string.Empty; + + // We get the form reference from Blazor code to validate it manually: + private MudForm form = null!; + + public DataSourceLocalFileDialog() + { + this.dataSourceValidation = new() + { + GetSelectedCloudEmbedding = () => this.SelectedCloudEmbedding, + GetPreviousDataSourceName = () => this.dataEditingPreviousInstanceName, + GetUsedDataSourceNames = () => this.UsedDataSourcesNames, + }; + } + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + // Configure the spellchecking for the instance name input: + this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); + + // Load the used instance names: + this.UsedDataSourcesNames = this.SettingsManager.ConfigurationData.DataSources.Select(x => x.Name.ToLowerInvariant()).ToList(); + + // When editing, we need to load the data: + if(this.IsEditing) + { + this.dataEditingPreviousInstanceName = this.DataSource.Name.ToLowerInvariant(); + this.dataNum = this.DataSource.Num; + this.dataId = this.DataSource.Id; + this.dataName = this.DataSource.Name; + this.dataEmbeddingId = this.DataSource.EmbeddingId; + this.dataFilePath = this.DataSource.FilePath; + } + + await base.OnInitializedAsync(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + // Reset the validation when not editing and on the first render. + // We don't want to show validation errors when the user opens the dialog. + if(!this.IsEditing && firstRender) + this.form.ResetValidation(); + + await base.OnAfterRenderAsync(firstRender); + } + + #endregion + + private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId).IsSelfHosted; + + private DataSourceLocalFile CreateDataSource() => new() + { + Id = this.dataId, + Num = this.dataNum, + Name = this.dataName, + Type = DataSourceType.LOCAL_FILE, + EmbeddingId = this.dataEmbeddingId, + FilePath = this.dataFilePath, + }; + + private async Task Store() + { + await this.form.Validate(); + + // When the data is not valid, we don't store it: + if (!this.dataIsValid) + return; + + var addedDataSource = this.CreateDataSource(); + this.MudDialog.Close(DialogResult.Ok(addedDataSource)); + } + + private void Cancel() => this.MudDialog.Cancel(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs index 3a7b092..14e8cc2 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs @@ -197,8 +197,7 @@ public partial class EmbeddingProviderDialog : ComponentBase, ISecretId private async Task Store() { await this.form.Validate(); - if (!string.IsNullOrWhiteSpace(this.dataAPIKeyStorageIssue)) - this.dataAPIKeyStorageIssue = string.Empty; + this.dataAPIKeyStorageIssue = string.Empty; // When the data is not valid, we don't store it: if (!this.dataIsValid) diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index 0d6fc28..ccc96df 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -53,6 +53,10 @@ + + + + diff --git a/app/MindWork AI Studio/Pages/Settings.razor b/app/MindWork AI Studio/Pages/Settings.razor index 4a00473..f09a180 100644 --- a/app/MindWork AI Studio/Pages/Settings.razor +++ b/app/MindWork AI Studio/Pages/Settings.razor @@ -7,6 +7,7 @@ + diff --git a/app/MindWork AI Studio/Pages/Settings.razor.cs b/app/MindWork AI Studio/Pages/Settings.razor.cs index d06ab02..6baf7f1 100644 --- a/app/MindWork AI Studio/Pages/Settings.razor.cs +++ b/app/MindWork AI Studio/Pages/Settings.razor.cs @@ -11,6 +11,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable private List> availableLLMProviders = new(); private List> availableEmbeddingProviders = new(); + private List> availableDataSources = new(); #region Overrides of ComponentBase diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index f6f08a5..2dd099e 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -25,6 +25,11 @@ public sealed class Data /// A collection of embedding providers configured. /// public List EmbeddingProviders { get; init; } = []; + + /// + /// A collection of data sources configured. + /// + public List DataSources { get; set; } = []; /// /// List of configured profiles. @@ -41,6 +46,11 @@ public sealed class Data /// public uint NextEmbeddingNum { get; set; } = 1; + /// + /// The next data source number to use. + /// + public uint NextDataSourceNum { get; set; } = 1; + /// /// The next profile number to use. /// diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs new file mode 100644 index 0000000..387accf --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs @@ -0,0 +1,46 @@ +using ERI_Client.V1; + +// ReSharper disable InconsistentNaming +namespace AIStudio.Settings.DataModel; + +/// +/// An external data source, accessed via an ERI server, cf. https://github.com/MindWorkAI/ERI. +/// +public readonly record struct DataSourceERI_V1 : IERIDataSource +{ + public DataSourceERI_V1() + { + } + + /// + public uint Num { get; init; } + + /// + public string Id { get; init; } = Guid.Empty.ToString(); + + /// + public string Name { get; init; } = string.Empty; + + /// + public DataSourceType Type { get; init; } = DataSourceType.NONE; + + /// + /// The hostname of the ERI server. + /// + public string Hostname { get; init; } = string.Empty; + + /// + /// The port of the ERI server. + /// + public int Port { get; init; } + + /// + /// The authentication method to use. + /// + public AuthMethod AuthMethod { get; init; } = AuthMethod.NONE; + + /// + /// The username to use for authentication, when the auth. method is USERNAME_PASSWORD. + /// + public string Username { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalDirectory.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalDirectory.cs new file mode 100644 index 0000000..963b9ba --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalDirectory.cs @@ -0,0 +1,31 @@ +namespace AIStudio.Settings.DataModel; + +/// +/// Represents a local directory as a data source. +/// +public readonly record struct DataSourceLocalDirectory : IInternalDataSource +{ + public DataSourceLocalDirectory() + { + } + + /// + public uint Num { get; init; } + + /// + public string Id { get; init; } = Guid.Empty.ToString(); + + /// + public string Name { get; init; } = string.Empty; + + /// + public DataSourceType Type { get; init; } = DataSourceType.NONE; + + /// + public string EmbeddingId { get; init; } = Guid.Empty.ToString(); + + /// + /// The path to the directory. + /// + public string Path { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalFile.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalFile.cs new file mode 100644 index 0000000..a608819 --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalFile.cs @@ -0,0 +1,31 @@ +namespace AIStudio.Settings.DataModel; + +/// +/// Represents one local file as a data source. +/// +public readonly record struct DataSourceLocalFile : IInternalDataSource +{ + public DataSourceLocalFile() + { + } + + /// + public uint Num { get; init; } + + /// + public string Id { get; init; } = Guid.Empty.ToString(); + + /// + public string Name { get; init; } = string.Empty; + + /// + public DataSourceType Type { get; init; } = DataSourceType.NONE; + + /// + public string EmbeddingId { get; init; } = Guid.Empty.ToString(); + + /// + /// The path to the file. + /// + public string FilePath { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceType.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceType.cs new file mode 100644 index 0000000..9c8b031 --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceType.cs @@ -0,0 +1,27 @@ +namespace AIStudio.Settings.DataModel; + +/// +/// AI Studio data source types. +/// +public enum DataSourceType +{ + /// + /// No data source. + /// + NONE = 0, + + /// + /// One file on the local machine (or a network share). + /// + LOCAL_FILE, + + /// + /// A directory on the local machine (or a network share). + /// + LOCAL_DIRECTORY, + + /// + /// External data source accessed via an ERI server, cf. https://github.com/MindWorkAI/ERI. + /// + ERI_V1, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs new file mode 100644 index 0000000..a630a92 --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs @@ -0,0 +1,24 @@ +namespace AIStudio.Settings.DataModel; + +/// +/// Extension methods for data source types. +/// +public static class DataSourceTypeExtension +{ + /// + /// Get the display name of the data source type. + /// + /// The data source type. + /// The display name of the data source type. + public static string GetDisplayName(this DataSourceType type) + { + return type switch + { + DataSourceType.LOCAL_FILE => "Local File", + DataSourceType.LOCAL_DIRECTORY => "Local Directory", + DataSourceType.ERI_V1 => "External ERI Server (v1)", + + _ => "None", + }; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/IDataSource.cs b/app/MindWork AI Studio/Settings/IDataSource.cs new file mode 100644 index 0000000..28bc3b9 --- /dev/null +++ b/app/MindWork AI Studio/Settings/IDataSource.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; + +using AIStudio.Settings.DataModel; + +namespace AIStudio.Settings; + +/// +/// The common interface for all data sources. +/// +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type_discriminator")] +[JsonDerivedType(typeof(DataSourceLocalDirectory), nameof(DataSourceType.LOCAL_DIRECTORY))] +[JsonDerivedType(typeof(DataSourceLocalFile), nameof(DataSourceType.LOCAL_FILE))] +[JsonDerivedType(typeof(DataSourceERI_V1), nameof(DataSourceType.ERI_V1))] +public interface IDataSource +{ + /// + /// The number of the data source. + /// + public uint Num { get; init; } + + /// + /// The unique identifier of the data source. + /// + public string Id { get; init; } + + /// + /// The name of the data source. + /// + public string Name { get; init; } + + /// + /// Which type of data source is this? + /// + public DataSourceType Type { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/IERIDataSource.cs b/app/MindWork AI Studio/Settings/IERIDataSource.cs new file mode 100644 index 0000000..34874fc --- /dev/null +++ b/app/MindWork AI Studio/Settings/IERIDataSource.cs @@ -0,0 +1,12 @@ +using ERI_Client.V1; + +namespace AIStudio.Settings; + +public interface IERIDataSource : IExternalDataSource +{ + public string Hostname { get; init; } + + public int Port { get; init; } + + public AuthMethod AuthMethod { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/IExternalDataSource.cs b/app/MindWork AI Studio/Settings/IExternalDataSource.cs new file mode 100644 index 0000000..8a7c067 --- /dev/null +++ b/app/MindWork AI Studio/Settings/IExternalDataSource.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace AIStudio.Settings; + +public interface IExternalDataSource : IDataSource, ISecretId +{ + #region Implementation of ISecretId + + [JsonIgnore] + string ISecretId.SecretId => this.Id; + + [JsonIgnore] + string ISecretId.SecretName => this.Name; + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/IInternalDataSource.cs b/app/MindWork AI Studio/Settings/IInternalDataSource.cs new file mode 100644 index 0000000..0ffa7de --- /dev/null +++ b/app/MindWork AI Studio/Settings/IInternalDataSource.cs @@ -0,0 +1,9 @@ +namespace AIStudio.Settings; + +public interface IInternalDataSource : IDataSource +{ + /// + /// The unique identifier of the embedding method used by this internal data source. + /// + public string EmbeddingId { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs b/app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs new file mode 100644 index 0000000..e26a20b --- /dev/null +++ b/app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs @@ -0,0 +1,16 @@ +using ERI_Client.V1; + +namespace AIStudio.Tools; + +public static class AuthMethodsV1Extensions +{ + public static string DisplayName(this AuthMethod authMethod) => authMethod switch + { + AuthMethod.NONE => "None", + AuthMethod.USERNAME_PASSWORD => "Username & Password", + AuthMethod.KERBEROS => "SSO (Kerberos)", + AuthMethod.TOKEN => "Access Token", + + _ => "Unknown authentication method", + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/FileSelectionResponse.cs b/app/MindWork AI Studio/Tools/Rust/FileSelectionResponse.cs new file mode 100644 index 0000000..fd3bed6 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/FileSelectionResponse.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools.Rust; + +/// +/// Data structure for selecting a file. +/// +/// Was the file selection canceled? +/// The selected file, if any. +public readonly record struct FileSelectionResponse(bool UserCancelled, string SelectedFilePath); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/PreviousFile.cs b/app/MindWork AI Studio/Tools/Rust/PreviousFile.cs new file mode 100644 index 0000000..217ea06 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/PreviousFile.cs @@ -0,0 +1,7 @@ +namespace AIStudio.Tools.Rust; + +/// +/// Data structure for selecting a file when a previous file was selected. +/// +/// The path of the previous file. +public readonly record struct PreviousFile(string FilePath); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RustService.APIKeys.cs b/app/MindWork AI Studio/Tools/RustService.APIKeys.cs new file mode 100644 index 0000000..1a3b4af --- /dev/null +++ b/app/MindWork AI Studio/Tools/RustService.APIKeys.cs @@ -0,0 +1,76 @@ +using AIStudio.Tools.Rust; + +namespace AIStudio.Tools; + +public sealed partial class RustService +{ + /// + /// Try to get the API key for the given secret ID. + /// + /// The secret ID to get the API key for. + /// Indicates if we are trying to get the API key. In that case, we don't log errors. + /// The requested secret. + public async Task GetAPIKey(ISecretId secretId, bool isTrying = false) + { + var secretRequest = new SelectSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, isTrying); + var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) + { + if(!isTrying) + this.logger!.LogError($"Failed to get the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); + return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the API key due to an API issue."); + } + + var secret = await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + if (!secret.Success && !isTrying) + this.logger!.LogError($"Failed to get the API key for secret ID '{secretId.SecretId}': '{secret.Issue}'"); + + return secret; + } + + /// + /// Try to store the API key for the given secret ID. + /// + /// The secret ID to store the API key for. + /// The API key to store. + /// The store secret response. + public async Task SetAPIKey(ISecretId secretId, string key) + { + var encryptedKey = await this.encryptor!.Encrypt(key); + var request = new StoreSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, encryptedKey); + var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to store the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); + return new StoreSecretResponse(false, "Failed to get the API key due to an API issue."); + } + + var state = await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + if (!state.Success) + this.logger!.LogError($"Failed to store the API key for secret ID '{secretId.SecretId}': '{state.Issue}'"); + + return state; + } + + /// + /// Tries to delete the API key for the given secret ID. + /// + /// The secret ID to delete the API key for. + /// The delete secret response. + public async Task DeleteAPIKey(ISecretId secretId) + { + var request = new SelectSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, false); + var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); + return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = "Failed to delete the API key due to an API issue."}; + } + + var state = await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + if (!state.Success) + this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}': '{state.Issue}'"); + + return state; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RustService.App.cs b/app/MindWork AI Studio/Tools/RustService.App.cs new file mode 100644 index 0000000..ea27f6d --- /dev/null +++ b/app/MindWork AI Studio/Tools/RustService.App.cs @@ -0,0 +1,120 @@ +using System.Security.Cryptography; + +namespace AIStudio.Tools; + +public sealed partial class RustService +{ + public async Task GetAppPort() + { + Console.WriteLine("Trying to get app port from Rust runtime..."); + + // + // Note I: In the production environment, the Rust runtime is already running + // and listening on the given port. In the development environment, the IDE + // starts the Rust runtime in parallel with the .NET runtime. Since the + // Rust runtime needs some time to start, we have to wait for it to be ready. + // + const int MAX_TRIES = 160; + var tris = 0; + var wait4Try = TimeSpan.FromMilliseconds(250); + var url = new Uri($"https://127.0.0.1:{this.apiPort}/system/dotnet/port"); + while (tris++ < MAX_TRIES) + { + // + // Note II: We use a new HttpClient instance for each try to avoid + // .NET is caching the result. When we use the same HttpClient + // instance, we would always get the same result (403 forbidden), + // without even trying to connect to the Rust server. + // + + using var initialHttp = new HttpClient(new HttpClientHandler + { + // + // Note III: We have to create also a new HttpClientHandler instance + // for each try to avoid .NET is caching the result. This is necessary + // because it gets disposed when the HttpClient instance gets disposed. + // + ServerCertificateCustomValidationCallback = (_, certificate, _, _) => + { + if(certificate is null) + return false; + + var currentCertificateFingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256); + return currentCertificateFingerprint == this.certificateFingerprint; + } + }); + + initialHttp.DefaultRequestVersion = Version.Parse("2.0"); + initialHttp.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; + initialHttp.DefaultRequestHeaders.AddApiToken(); + + try + { + var response = await initialHttp.GetAsync(url); + if (!response.IsSuccessStatusCode) + { + Console.WriteLine($"Try {tris}/{MAX_TRIES} to get the app port from Rust runtime"); + await Task.Delay(wait4Try); + continue; + } + + var appPortContent = await response.Content.ReadAsStringAsync(); + var appPort = int.Parse(appPortContent); + Console.WriteLine($"Received app port from Rust runtime: '{appPort}'"); + return appPort; + } + catch (Exception e) + { + Console.WriteLine($"Error: Was not able to get the app port from Rust runtime: '{e.Message}'"); + Console.WriteLine(e.InnerException); + throw; + } + } + + Console.WriteLine("Failed to receive the app port from Rust runtime."); + return 0; + } + + public async Task AppIsReady() + { + const string URL = "/system/dotnet/ready"; + this.logger!.LogInformation("Notifying Rust runtime that the app is ready."); + try + { + var response = await this.http.GetAsync(URL); + if (!response.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'"); + } + } + catch (Exception e) + { + this.logger!.LogError(e, "Failed to notify the Rust runtime that the app is ready."); + throw; + } + } + + public async Task GetConfigDirectory() + { + var response = await this.http.GetAsync("/system/directories/config"); + if (!response.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to get the config directory from Rust: '{response.StatusCode}'"); + return string.Empty; + } + + return await response.Content.ReadAsStringAsync(); + } + + public async Task GetDataDirectory() + { + var response = await this.http.GetAsync("/system/directories/data"); + if (!response.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to get the data directory from Rust: '{response.StatusCode}'"); + return string.Empty; + } + + return await response.Content.ReadAsStringAsync(); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RustService.Clipboard.cs b/app/MindWork AI Studio/Tools/RustService.Clipboard.cs new file mode 100644 index 0000000..d3e6520 --- /dev/null +++ b/app/MindWork AI Studio/Tools/RustService.Clipboard.cs @@ -0,0 +1,50 @@ +using AIStudio.Tools.Rust; + +namespace AIStudio.Tools; + +public sealed partial class RustService +{ + /// + /// Tries to copy the given text to the clipboard. + /// + /// The snackbar to show the result. + /// The text to copy to the clipboard. + public async Task CopyText2Clipboard(ISnackbar snackbar, string text) + { + var message = "Successfully copied the text to your clipboard"; + var iconColor = Color.Error; + var severity = Severity.Error; + try + { + var encryptedText = await text.Encrypt(this.encryptor!); + var response = await this.http.PostAsync("/clipboard/set", new StringContent(encryptedText.EncryptedData)); + if (!response.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to copy the text to the clipboard due to an network error: '{response.StatusCode}'"); + message = "Failed to copy the text to your clipboard."; + return; + } + + var state = await response.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + if (!state.Success) + { + this.logger!.LogError("Failed to copy the text to the clipboard."); + message = "Failed to copy the text to your clipboard."; + return; + } + + iconColor = Color.Success; + severity = Severity.Success; + this.logger!.LogDebug("Successfully copied the text to the clipboard."); + } + finally + { + snackbar.Add(message, severity, config => + { + config.Icon = Icons.Material.Filled.ContentCopy; + config.IconSize = Size.Large; + config.IconColor = iconColor; + }); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RustService.FileSystem.cs b/app/MindWork AI Studio/Tools/RustService.FileSystem.cs new file mode 100644 index 0000000..bd4c26a --- /dev/null +++ b/app/MindWork AI Studio/Tools/RustService.FileSystem.cs @@ -0,0 +1,32 @@ +using AIStudio.Tools.Rust; + +namespace AIStudio.Tools; + +public sealed partial class RustService +{ + public async Task SelectDirectory(string title, string? initialDirectory = null) + { + PreviousDirectory? previousDirectory = initialDirectory is null ? null : new (initialDirectory); + var result = await this.http.PostAsJsonAsync($"/select/directory?title={title}", previousDirectory, this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'"); + return new DirectorySelectionResponse(true, string.Empty); + } + + return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + } + + public async Task SelectFile(string title, string? initialFile = null) + { + PreviousFile? previousFile = initialFile is null ? null : new (initialFile); + var result = await this.http.PostAsJsonAsync($"/select/file?title={title}", previousFile, this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to select a file: '{result.StatusCode}'"); + return new FileSelectionResponse(true, string.Empty); + } + + return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RustService.Secrets.cs b/app/MindWork AI Studio/Tools/RustService.Secrets.cs new file mode 100644 index 0000000..a70a127 --- /dev/null +++ b/app/MindWork AI Studio/Tools/RustService.Secrets.cs @@ -0,0 +1,76 @@ +using AIStudio.Tools.Rust; + +namespace AIStudio.Tools; + +public sealed partial class RustService +{ + /// + /// Try to get the secret data for the given secret ID. + /// + /// The secret ID to get the data for. + /// Indicates if we are trying to get the data. In that case, we don't log errors. + /// The requested secret. + public async Task GetSecret(ISecretId secretId, bool isTrying = false) + { + var secretRequest = new SelectSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, isTrying); + var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) + { + if(!isTrying) + this.logger!.LogError($"Failed to get the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); + return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the secret data due to an API issue."); + } + + var secret = await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + if (!secret.Success && !isTrying) + this.logger!.LogError($"Failed to get the secret data for secret ID '{secretId.SecretId}': '{secret.Issue}'"); + + return secret; + } + + /// + /// Try to store the secret data for the given secret ID. + /// + /// The secret ID to store the data for. + /// The data to store. + /// The store secret response. + public async Task SetSecret(ISecretId secretId, string secretData) + { + var encryptedSecret = await this.encryptor!.Encrypt(secretData); + var request = new StoreSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, encryptedSecret); + var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to store the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); + return new StoreSecretResponse(false, "Failed to get the secret data due to an API issue."); + } + + var state = await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + if (!state.Success) + this.logger!.LogError($"Failed to store the secret data for secret ID '{secretId.SecretId}': '{state.Issue}'"); + + return state; + } + + /// + /// Tries to delete the secret data for the given secret ID. + /// + /// The secret ID to delete the data for. + /// The delete secret response. + public async Task DeleteSecret(ISecretId secretId) + { + var request = new SelectSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, false); + var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to delete the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); + return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = "Failed to delete the secret data due to an API issue."}; + } + + var state = await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + if (!state.Success) + this.logger!.LogError($"Failed to delete the secret data for secret ID '{secretId.SecretId}': '{state.Issue}'"); + + return state; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RustService.Updates.cs b/app/MindWork AI Studio/Tools/RustService.Updates.cs new file mode 100644 index 0000000..edb1466 --- /dev/null +++ b/app/MindWork AI Studio/Tools/RustService.Updates.cs @@ -0,0 +1,40 @@ +using AIStudio.Tools.Rust; + +namespace AIStudio.Tools; + +public sealed partial class RustService +{ + public async Task CheckForUpdate() + { + try + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45)); + var response = await this.http.GetFromJsonAsync("/updates/check", this.jsonRustSerializerOptions, cts.Token); + this.logger!.LogInformation($"Checked for an update: update available='{response.UpdateIsAvailable}'; error='{response.Error}'; next version='{response.NewVersion}'; changelog len='{response.Changelog.Length}'"); + return response; + } + catch (Exception e) + { + this.logger!.LogError(e, "Failed to check for an update."); + return new UpdateResponse + { + Error = true, + UpdateIsAvailable = false, + }; + } + } + + public async Task InstallUpdate() + { + try + { + var cts = new CancellationTokenSource(); + await this.http.GetAsync("/updates/install", cts.Token); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RustService.cs b/app/MindWork AI Studio/Tools/RustService.cs index 99476e5..08d68d3 100644 --- a/app/MindWork AI Studio/Tools/RustService.cs +++ b/app/MindWork AI Studio/Tools/RustService.cs @@ -1,8 +1,6 @@ using System.Security.Cryptography; using System.Text.Json; -using AIStudio.Tools.Rust; - // ReSharper disable NotAccessedPositionalProperty.Local namespace AIStudio.Tools; @@ -10,7 +8,7 @@ namespace AIStudio.Tools; /// /// Calling Rust functions. /// -public sealed class RustService : IDisposable +public sealed partial class RustService : IDisposable { private readonly HttpClient http; @@ -60,281 +58,6 @@ public sealed class RustService : IDisposable { this.encryptor = encryptionService; } - - public async Task GetAppPort() - { - Console.WriteLine("Trying to get app port from Rust runtime..."); - - // - // Note I: In the production environment, the Rust runtime is already running - // and listening on the given port. In the development environment, the IDE - // starts the Rust runtime in parallel with the .NET runtime. Since the - // Rust runtime needs some time to start, we have to wait for it to be ready. - // - const int MAX_TRIES = 160; - var tris = 0; - var wait4Try = TimeSpan.FromMilliseconds(250); - var url = new Uri($"https://127.0.0.1:{this.apiPort}/system/dotnet/port"); - while (tris++ < MAX_TRIES) - { - // - // Note II: We use a new HttpClient instance for each try to avoid - // .NET is caching the result. When we use the same HttpClient - // instance, we would always get the same result (403 forbidden), - // without even trying to connect to the Rust server. - // - - using var initialHttp = new HttpClient(new HttpClientHandler - { - // - // Note III: We have to create also a new HttpClientHandler instance - // for each try to avoid .NET is caching the result. This is necessary - // because it gets disposed when the HttpClient instance gets disposed. - // - ServerCertificateCustomValidationCallback = (_, certificate, _, _) => - { - if(certificate is null) - return false; - - var currentCertificateFingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256); - return currentCertificateFingerprint == this.certificateFingerprint; - } - }); - - initialHttp.DefaultRequestVersion = Version.Parse("2.0"); - initialHttp.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; - initialHttp.DefaultRequestHeaders.AddApiToken(); - - try - { - var response = await initialHttp.GetAsync(url); - if (!response.IsSuccessStatusCode) - { - Console.WriteLine($"Try {tris}/{MAX_TRIES} to get the app port from Rust runtime"); - await Task.Delay(wait4Try); - continue; - } - - var appPortContent = await response.Content.ReadAsStringAsync(); - var appPort = int.Parse(appPortContent); - Console.WriteLine($"Received app port from Rust runtime: '{appPort}'"); - return appPort; - } - catch (Exception e) - { - Console.WriteLine($"Error: Was not able to get the app port from Rust runtime: '{e.Message}'"); - Console.WriteLine(e.InnerException); - throw; - } - } - - Console.WriteLine("Failed to receive the app port from Rust runtime."); - return 0; - } - - public async Task AppIsReady() - { - const string URL = "/system/dotnet/ready"; - this.logger!.LogInformation("Notifying Rust runtime that the app is ready."); - try - { - var response = await this.http.GetAsync(URL); - if (!response.IsSuccessStatusCode) - { - this.logger!.LogError($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'"); - } - } - catch (Exception e) - { - this.logger!.LogError(e, "Failed to notify the Rust runtime that the app is ready."); - throw; - } - } - - public async Task GetConfigDirectory() - { - var response = await this.http.GetAsync("/system/directories/config"); - if (!response.IsSuccessStatusCode) - { - this.logger!.LogError($"Failed to get the config directory from Rust: '{response.StatusCode}'"); - return string.Empty; - } - - return await response.Content.ReadAsStringAsync(); - } - - public async Task GetDataDirectory() - { - var response = await this.http.GetAsync("/system/directories/data"); - if (!response.IsSuccessStatusCode) - { - this.logger!.LogError($"Failed to get the data directory from Rust: '{response.StatusCode}'"); - return string.Empty; - } - - return await response.Content.ReadAsStringAsync(); - } - - /// - /// Tries to copy the given text to the clipboard. - /// - /// The snackbar to show the result. - /// The text to copy to the clipboard. - public async Task CopyText2Clipboard(ISnackbar snackbar, string text) - { - var message = "Successfully copied the text to your clipboard"; - var iconColor = Color.Error; - var severity = Severity.Error; - try - { - var encryptedText = await text.Encrypt(this.encryptor!); - var response = await this.http.PostAsync("/clipboard/set", new StringContent(encryptedText.EncryptedData)); - if (!response.IsSuccessStatusCode) - { - this.logger!.LogError($"Failed to copy the text to the clipboard due to an network error: '{response.StatusCode}'"); - message = "Failed to copy the text to your clipboard."; - return; - } - - var state = await response.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); - if (!state.Success) - { - this.logger!.LogError("Failed to copy the text to the clipboard."); - message = "Failed to copy the text to your clipboard."; - return; - } - - iconColor = Color.Success; - severity = Severity.Success; - this.logger!.LogDebug("Successfully copied the text to the clipboard."); - } - finally - { - snackbar.Add(message, severity, config => - { - config.Icon = Icons.Material.Filled.ContentCopy; - config.IconSize = Size.Large; - config.IconColor = iconColor; - }); - } - } - - public async Task CheckForUpdate() - { - try - { - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45)); - var response = await this.http.GetFromJsonAsync("/updates/check", this.jsonRustSerializerOptions, cts.Token); - this.logger!.LogInformation($"Checked for an update: update available='{response.UpdateIsAvailable}'; error='{response.Error}'; next version='{response.NewVersion}'; changelog len='{response.Changelog.Length}'"); - return response; - } - catch (Exception e) - { - this.logger!.LogError(e, "Failed to check for an update."); - return new UpdateResponse - { - Error = true, - UpdateIsAvailable = false, - }; - } - } - - public async Task InstallUpdate() - { - try - { - var cts = new CancellationTokenSource(); - await this.http.GetAsync("/updates/install", cts.Token); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - } - - /// - /// Try to get the API key for the given secret ID. - /// - /// The secret ID to get the API key for. - /// Indicates if we are trying to get the API key. In that case, we don't log errors. - /// The requested secret. - public async Task GetAPIKey(ISecretId secretId, bool isTrying = false) - { - var secretRequest = new SelectSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, isTrying); - var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions); - if (!result.IsSuccessStatusCode) - { - if(!isTrying) - this.logger!.LogError($"Failed to get the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); - return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the API key due to an API issue."); - } - - var secret = await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); - if (!secret.Success && !isTrying) - this.logger!.LogError($"Failed to get the API key for secret ID '{secretId.SecretId}': '{secret.Issue}'"); - - return secret; - } - - /// - /// Try to store the API key for the given secret ID. - /// - /// The secret ID to store the API key for. - /// The API key to store. - /// The store secret response. - public async Task SetAPIKey(ISecretId secretId, string key) - { - var encryptedKey = await this.encryptor!.Encrypt(key); - var request = new StoreSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, encryptedKey); - var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions); - if (!result.IsSuccessStatusCode) - { - this.logger!.LogError($"Failed to store the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); - return new StoreSecretResponse(false, "Failed to get the API key due to an API issue."); - } - - var state = await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); - if (!state.Success) - this.logger!.LogError($"Failed to store the API key for secret ID '{secretId.SecretId}': '{state.Issue}'"); - - return state; - } - - /// - /// Tries to delete the API key for the given secret ID. - /// - /// The secret ID to delete the API key for. - /// The delete secret response. - public async Task DeleteAPIKey(ISecretId secretId) - { - var request = new SelectSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, false); - var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions); - if (!result.IsSuccessStatusCode) - { - this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); - return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = "Failed to delete the API key due to an API issue."}; - } - - var state = await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); - if (!state.Success) - this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}': '{state.Issue}'"); - - return state; - } - - public async Task SelectDirectory(string title, string? initialDirectory = null) - { - PreviousDirectory? previousDirectory = initialDirectory is null ? null : new (initialDirectory); - var result = await this.http.PostAsJsonAsync($"/select/directory?title={title}", previousDirectory, this.jsonRustSerializerOptions); - if (!result.IsSuccessStatusCode) - { - this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'"); - return new DirectorySelectionResponse(true, string.Empty); - } - - return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); - } #region IDisposable diff --git a/app/MindWork AI Studio/Tools/Validation/DataSourceValidation.cs b/app/MindWork AI Studio/Tools/Validation/DataSourceValidation.cs new file mode 100644 index 0000000..2ba7727 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Validation/DataSourceValidation.cs @@ -0,0 +1,149 @@ +using ERI_Client.V1; + +namespace AIStudio.Tools.Validation; + +public sealed class DataSourceValidation +{ + public Func GetSecretStorageIssue { get; init; } = () => string.Empty; + + public Func GetPreviousDataSourceName { get; init; } = () => string.Empty; + + public Func> GetUsedDataSourceNames { get; init; } = () => []; + + public Func GetAuthMethod { get; init; } = () => AuthMethod.NONE; + + public Func GetSelectedCloudEmbedding { get; init; } = () => false; + + public Func GetTestedConnection { get; init; } = () => false; + + public Func GetTestedConnectionResult { get; init; } = () => false; + + public Func> GetAvailableAuthMethods { get; init; } = () => []; + + public string? ValidatingHostname(string hostname) + { + if(string.IsNullOrWhiteSpace(hostname)) + return "Please enter a hostname, e.g., http://localhost:1234"; + + if(!hostname.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !hostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) + return "The hostname must start with either http:// or https://"; + + if(!Uri.TryCreate(hostname, UriKind.Absolute, out _)) + return "The hostname is not a valid HTTP(S) URL."; + + return null; + } + + public string? ValidatePort(int port) + { + if(port is < 1 or > 65535) + return "The port must be between 1 and 65535."; + + return null; + } + + public string? ValidateUsername(string username) + { + if(this.GetAuthMethod() is not AuthMethod.USERNAME_PASSWORD) + return null; + + if(string.IsNullOrWhiteSpace(username)) + return "The username must not be empty."; + + return null; + } + + public string? ValidatingSecret(string secret) + { + var authMethod = this.GetAuthMethod(); + if(authMethod is AuthMethod.NONE or AuthMethod.KERBEROS) + return null; + + var secretStorageIssue = this.GetSecretStorageIssue(); + if(!string.IsNullOrWhiteSpace(secretStorageIssue)) + return secretStorageIssue; + + if (string.IsNullOrWhiteSpace(secret)) + return authMethod switch + { + AuthMethod.TOKEN => "Please enter your secure access token.", + AuthMethod.USERNAME_PASSWORD => "Please enter your password.", + + _ => "Please enter the secret necessary for authentication." + }; + + return null; + } + + public string? ValidatingName(string dataSourceName) + { + if(string.IsNullOrWhiteSpace(dataSourceName)) + return "The name must not be empty."; + + if (dataSourceName.Length > 40) + return "The name must not exceed 40 characters."; + + var lowerName = dataSourceName.ToLowerInvariant(); + if(lowerName != this.GetPreviousDataSourceName() && this.GetUsedDataSourceNames().Contains(lowerName)) + return "The name is already used by another data source. Please choose a different name."; + + return null; + } + + public string? ValidatePath(string path) + { + if(string.IsNullOrWhiteSpace(path)) + return "The path must not be empty. Please select a directory."; + + if(!Directory.Exists(path)) + return "The path does not exist. Please select a valid directory."; + + return null; + } + + public string? ValidateFilePath(string filePath) + { + if(string.IsNullOrWhiteSpace(filePath)) + return "The file path must not be empty. Please select a file."; + + if(!File.Exists(filePath)) + return "The file does not exist. Please select a valid file."; + + return null; + } + + public string? ValidateEmbeddingId(string embeddingId) + { + if(string.IsNullOrWhiteSpace(embeddingId)) + return "Please select an embedding provider."; + + return null; + } + + public string? ValidateUserAcknowledgedCloudEmbedding(bool value) + { + if(this.GetSelectedCloudEmbedding() && !value) + return "Please acknowledge that you are aware of the cloud embedding implications."; + + return null; + } + + public string? ValidateTestedConnection() + { + if(!this.GetTestedConnection()) + return "Please test the connection before saving."; + + if(!this.GetTestedConnectionResult()) + return "The connection test failed. Please check the connection settings."; + + return null; + } + + public string? ValidateAuthMethod(AuthMethod authMethod) + { + if(!this.GetAvailableAuthMethods().Contains(authMethod)) + return "Please select one valid authentication method."; + + return null; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 8ad3bbb..d1c6dae 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -205,6 +205,9 @@ "type": "Transitive", "resolved": "0.16.9", "contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ==" + }, + "ericlientv1": { + "type": "Project" } }, "net8.0/osx-arm64": {} diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.26.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.26.md new file mode 100644 index 0000000..38c6e0f --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.26.md @@ -0,0 +1,3 @@ +# v0.9.26, build 201 (2025-01-xx xx:xx UTC) +- Added the ability to configure local and remote (ERI) data sources in the settings as a preview feature behind the RAG feature flag. +- Fixed a bug in the ERI server assistant that allowed an empty directory as a base directory for the code generation. \ No newline at end of file diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index e83b2b5..f6af44c 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -270,4 +270,53 @@ pub struct PreviousDirectory { pub struct DirectorySelectionResponse { user_cancelled: bool, selected_directory: String, +} + +/// Let the user select a file. +#[post("/select/file?", data = "<previous_file>")] +pub fn select_file(_token: APIToken, title: &str, previous_file: Option<Json<PreviousFile>>) -> Json<FileSelectionResponse> { + let file_path = match previous_file { + Some(previous) => { + let previous_path = previous.file_path.as_str(); + FileDialogBuilder::new() + .set_title(title) + .set_directory(previous_path) + .pick_file() + }, + + None => { + FileDialogBuilder::new() + .set_title(title) + .pick_file() + }, + }; + + match file_path { + Some(path) => { + info!("User selected file: {path:?}"); + Json(FileSelectionResponse { + user_cancelled: false, + selected_file_path: path.to_str().unwrap().to_string(), + }) + }, + + None => { + info!("User cancelled file selection."); + Json(FileSelectionResponse { + user_cancelled: true, + selected_file_path: String::from(""), + }) + }, + } +} + +#[derive(Clone, Deserialize)] +pub struct PreviousFile { + file_path: String, +} + +#[derive(Serialize)] +pub struct FileSelectionResponse { + user_cancelled: bool, + selected_file_path: String, } \ No newline at end of file diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 963900d..26bbbf9 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -85,6 +85,7 @@ pub fn start_runtime_api() { crate::app_window::check_for_update, crate::app_window::install_update, crate::app_window::select_directory, + crate::app_window::select_file, crate::secret::get_secret, crate::secret::store_secret, crate::secret::delete_secret,