diff --git a/README.md b/README.md index 79aa3526..0ad1f280 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Things we are currently working on: - [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~ - [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~ - [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~ - - [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259))~~ + - [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~ - [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files - [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs) - [ ] App: Implement external embedding providers diff --git a/app/ERIClientV1/Client.Generated.cs b/app/ERIClientV1/Client.Generated.cs deleted file mode 100644 index 96698e16..00000000 --- a/app/ERIClientV1/Client.Generated.cs +++ /dev/null @@ -1,1277 +0,0 @@ -//---------------------- -// -// 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 deleted file mode 100644 index 8ca0662e..00000000 --- a/app/ERIClientV1/ERIClientV1.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - net8.0 - latest - enable - enable - - - diff --git a/app/MindWork AI Studio.sln b/app/MindWork AI Studio.sln index 6bf20b24..ef02b517 100644 --- a/app/MindWork AI Studio.sln +++ b/app/MindWork AI Studio.sln @@ -2,10 +2,6 @@ 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 @@ -16,12 +12,7 @@ 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/ERIVersionExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs index c7ef7c16..99631e14 100644 --- a/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs +++ b/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs @@ -7,7 +7,7 @@ public static class ERIVersionExtensions try { var url = version.SpecificationURL(); - var response = await httpClient.GetAsync(url); + using var response = await httpClient.GetAsync(url); return await response.Content.ReadAsStringAsync(); } catch diff --git a/app/MindWork AI Studio/Components/Changelog.razor.cs b/app/MindWork AI Studio/Components/Changelog.razor.cs index c90a46c6..bba8b8a5 100644 --- a/app/MindWork AI Studio/Components/Changelog.razor.cs +++ b/app/MindWork AI Studio/Components/Changelog.razor.cs @@ -23,7 +23,7 @@ public partial class Changelog : ComponentBase private async Task ReadLogAsync() { - var response = await this.HttpClient.GetAsync($"changelog/{this.SelectedLog.Filename}"); + using var response = await this.HttpClient.GetAsync($"changelog/{this.SelectedLog.Filename}"); this.LogContent = await response.Content.ReadAsStringAsync(); } } \ 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 index 3f19bf9f..506c63b9 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor @@ -1,4 +1,3 @@ -@using AIStudio.Settings @using AIStudio.Settings.DataModel @inherits SettingsPanelBase @@ -37,12 +36,7 @@ @this.GetEmbeddingName(context) - @if (context is IERIDataSource) - { - @* *@ - @* Show Information *@ - @* *@ - } + Edit diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor.cs index 03124527..6f7e74b8 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelDataSources.razor.cs @@ -1,8 +1,7 @@ using AIStudio.Dialogs; using AIStudio.Settings; using AIStudio.Settings.DataModel; - -using ERI_Client.V1; +using AIStudio.Tools.ERIClient.DataModel; using Microsoft.AspNetCore.Components; @@ -216,10 +215,37 @@ public partial class SettingsPanelDataSources : SettingsPanelBase } } - private Task ShowInformation(IDataSource dataSource) + private async Task ShowInformation(IDataSource dataSource) { - #warning Implement the information dialog for ERI data sources. - return Task.CompletedTask; + switch (dataSource) + { + case DataSourceLocalFile localFile: + var localFileDialogParameters = new DialogParameters + { + { x => x.DataSource, localFile }, + }; + + await this.DialogService.ShowAsync("Local File Data Source Information", localFileDialogParameters, DialogOptions.FULLSCREEN); + break; + + case DataSourceLocalDirectory localDirectory: + var localDirectoryDialogParameters = new DialogParameters + { + { x => x.DataSource, localDirectory }, + }; + + await this.DialogService.ShowAsync("Local Directory Data Source Information", localDirectoryDialogParameters, DialogOptions.FULLSCREEN); + break; + + case DataSourceERI_V1 eriV1DataSource: + var eriV1DialogParameters = new DialogParameters + { + { x => x.DataSource, eriV1DataSource }, + }; + + await this.DialogService.ShowAsync("ERI v1 Data Source Information", eriV1DialogParameters, DialogOptions.FULLSCREEN); + break; + } } private async Task UpdateDataSources() diff --git a/app/MindWork AI Studio/Components/TextInfoLine.razor b/app/MindWork AI Studio/Components/TextInfoLine.razor new file mode 100644 index 00000000..5e5de4da --- /dev/null +++ b/app/MindWork AI Studio/Components/TextInfoLine.razor @@ -0,0 +1,19 @@ + + + + @if (this.ShowingCopyButton) + { + + + + } + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/TextInfoLine.razor.cs b/app/MindWork AI Studio/Components/TextInfoLine.razor.cs new file mode 100644 index 00000000..4c4e6409 --- /dev/null +++ b/app/MindWork AI Studio/Components/TextInfoLine.razor.cs @@ -0,0 +1,50 @@ +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public partial class TextInfoLine : ComponentBase +{ + [Parameter] + public string Label { get; set; } = string.Empty; + + [Parameter] + public string Icon { get; set; } = Icons.Material.Filled.Info; + + [Parameter] + public string Value { get; set; } = string.Empty; + + [Parameter] + public string ClipboardTooltipSubject { get; set; } = "the text"; + + [Parameter] + public bool ShowingCopyButton { get; set; } = true; + + [Inject] + private RustService RustService { get; init; } = null!; + + [Inject] + private ISnackbar Snackbar { get; init; } = null!; + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + // Configure the spellchecking for the user input: + this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); + + await base.OnInitializedAsync(); + } + + #endregion + + private static readonly Dictionary USER_INPUT_ATTRIBUTES = new(); + + private string ClipboardTooltip => $"Copy {this.ClipboardTooltipSubject} to the clipboard"; + + private async Task CopyToClipboard(string content) => await this.RustService.CopyText2Clipboard(this.Snackbar, content); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/TextInfoLines.razor b/app/MindWork AI Studio/Components/TextInfoLines.razor new file mode 100644 index 00000000..02dadb48 --- /dev/null +++ b/app/MindWork AI Studio/Components/TextInfoLines.razor @@ -0,0 +1,20 @@ + + + + @if (this.ShowingCopyButton) + { + + + + } + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/TextInfoLines.razor.cs b/app/MindWork AI Studio/Components/TextInfoLines.razor.cs new file mode 100644 index 00000000..64d5924d --- /dev/null +++ b/app/MindWork AI Studio/Components/TextInfoLines.razor.cs @@ -0,0 +1,50 @@ +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public partial class TextInfoLines : ComponentBase +{ + [Parameter] + public string Label { get; set; } = string.Empty; + + [Parameter] + public string Value { get; set; } = string.Empty; + + [Parameter] + public string ClipboardTooltipSubject { get; set; } = "the text"; + + [Parameter] + public int MaxLines { get; set; } = 30; + + [Parameter] + public bool ShowingCopyButton { get; set; } = true; + + [Inject] + private RustService RustService { get; init; } = null!; + + [Inject] + private ISnackbar Snackbar { get; init; } = null!; + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + // Configure the spellchecking for the user input: + this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); + + await base.OnInitializedAsync(); + } + + #endregion + + private static readonly Dictionary USER_INPUT_ATTRIBUTES = new(); + + private string ClipboardTooltip => $"Copy {this.ClipboardTooltipSubject} to the clipboard"; + + private async Task CopyToClipboard(string content) => await this.RustService.CopyText2Clipboard(this.Snackbar, content); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI-V1InfoDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceERI-V1InfoDialog.razor new file mode 100644 index 00000000..a1ebe82d --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceERI-V1InfoDialog.razor @@ -0,0 +1,101 @@ +@using AIStudio.Tools.ERIClient.DataModel + + + + + Common data source information + + + + + + + @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.DataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD) + { + + } + + + + + + Retrieval information + + @if (!this.retrievalInfoformation.Any()) + { + + The data source does not provide any retrieval information. + + } + else + { + + @for (var index = 0; index < this.retrievalInfoformation.Count; index++) + { + var info = this.retrievalInfoformation[index]; + + + + + @if (!string.IsNullOrWhiteSpace(info.Link)) + { + + Open web link, show more information + + } + + + Embeddings + + @if (!info.Embeddings.Any()) + { + + The data source does not provide any embedding information. + + } + else + { + + @for (var embeddingIndex = 0; embeddingIndex < info.Embeddings.Count; embeddingIndex++) + { + var embedding = info.Embeddings[embeddingIndex]; + + + + + + @if (!string.IsNullOrWhiteSpace(embedding.Link)) + { + + Open web link, show more information + + } + + } + + } + + } + + } + + + + + @if (this.IsOperationInProgress) + { + + } + Reload + Close + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI-V1InfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceERI-V1InfoDialog.razor.cs new file mode 100644 index 00000000..304c4f67 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceERI-V1InfoDialog.razor.cs @@ -0,0 +1,178 @@ +// ReSharper disable InconsistentNaming + +using System.Text; + +using AIStudio.Assistants.ERI; +using AIStudio.Settings.DataModel; +using AIStudio.Tools.ERIClient; +using AIStudio.Tools.ERIClient.DataModel; + +using Microsoft.AspNetCore.Components; + +using RetrievalInfo = AIStudio.Tools.ERIClient.DataModel.RetrievalInfo; + +namespace AIStudio.Dialogs; + +public partial class DataSourceERI_V1InfoDialog : ComponentBase, IAsyncDisposable, ISecretId +{ + [CascadingParameter] + private MudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public DataSourceERI_V1 DataSource { get; set; } + + [Inject] + private RustService RustService { get; init; } = null!; + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + this.eriServerTasks.Add(this.GetERIMetadata()); + await base.OnInitializedAsync(); + } + + #endregion + + private readonly CancellationTokenSource cts = new(); + private readonly List eriServerTasks = new(); + private readonly List dataIssues = []; + + private string serverDescription = string.Empty; + private ProviderType securityRequirements = ProviderType.NONE; + private IReadOnlyList retrievalInfoformation = []; + + private bool IsOperationInProgress { get; set; } = true; + + private bool IsConnectionEncrypted() => this.DataSource.Hostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase); + + private string Port => this.DataSource.Port == 0 ? string.Empty : $"{this.DataSource.Port}"; + + private string RetrievalName(RetrievalInfo retrievalInfo) + { + var hasId = !string.IsNullOrWhiteSpace(retrievalInfo.Id); + var hasName = !string.IsNullOrWhiteSpace(retrievalInfo.Name); + + if (hasId && hasName) + return $"[{retrievalInfo.Id}] {retrievalInfo.Name}"; + + if (hasId) + return $"[{retrievalInfo.Id}] Unnamed retrieval process"; + + return hasName ? retrievalInfo.Name : "Unnamed retrieval process"; + } + + private string RetrievalParameters(RetrievalInfo retrievalInfo) + { + var parameters = retrievalInfo.ParametersDescription; + if (parameters is null || parameters.Count == 0) + return "This retrieval process has no parameters."; + + var sb = new StringBuilder(); + foreach (var (paramName, description) in parameters) + { + sb.Append("Parameter: "); + sb.AppendLine(paramName); + sb.AppendLine(description); + sb.AppendLine(); + } + + return sb.ToString(); + } + + private async Task GetERIMetadata() + { + this.dataIssues.Clear(); + + try + { + this.IsOperationInProgress = true; + this.StateHasChanged(); + + using var client = ERIClientFactory.Get(ERIVersion.V1, this.DataSource); + if(client is null) + { + this.dataIssues.Add("Failed to connect to the ERI v1 server. The server is not supported."); + return; + } + + var loginResult = await client.AuthenticateAsync(this.DataSource, this.RustService); + if (!loginResult.Successful) + { + this.dataIssues.Add(loginResult.Message); + return; + } + + var dataSourceInfo = await client.GetDataSourceInfoAsync(this.cts.Token); + if (!dataSourceInfo.Successful) + { + this.dataIssues.Add(dataSourceInfo.Message); + return; + } + + this.serverDescription = dataSourceInfo.Data.Description; + + var securityRequirementsResult = await client.GetSecurityRequirementsAsync(this.cts.Token); + if (!securityRequirementsResult.Successful) + { + this.dataIssues.Add(securityRequirementsResult.Message); + return; + } + + this.securityRequirements = securityRequirementsResult.Data.AllowedProviderType; + + var retrievalInfoResult = await client.GetRetrievalInfoAsync(this.cts.Token); + if (!retrievalInfoResult.Successful) + { + this.dataIssues.Add(retrievalInfoResult.Message); + return; + } + + this.retrievalInfoformation = retrievalInfoResult.Data ?? []; + + this.StateHasChanged(); + } + catch (Exception e) + { + this.dataIssues.Add($"Failed to connect to the ERI v1 server. The message was: {e.Message}"); + } + finally + { + this.IsOperationInProgress = false; + this.StateHasChanged(); + } + } + + private void Close() + { + this.cts.Cancel(); + this.MudDialog.Close(); + } + + #region Implementation of ISecretId + + public string SecretId => this.DataSource.Id; + + public string SecretName => this.DataSource.Name; + + #endregion + + #region Implementation of IDisposable + + public async ValueTask DisposeAsync() + { + try + { + await this.cts.CancelAsync(); + await Task.WhenAll(this.eriServerTasks); + + this.cts.Dispose(); + } + catch + { + // ignored + } + } + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor index 64e307b9..ac40ffa5 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor +++ b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor @@ -1,4 +1,4 @@ -@using ERI_Client.V1 +@using AIStudio.Tools.ERIClient.DataModel diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs index 1e4eaf7d..aaa4dac1 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs @@ -1,9 +1,10 @@ +using AIStudio.Assistants.ERI; using AIStudio.Settings; using AIStudio.Settings.DataModel; +using AIStudio.Tools.ERIClient; +using AIStudio.Tools.ERIClient.DataModel; using AIStudio.Tools.Validation; -using ERI_Client.V1; - using Microsoft.AspNetCore.Components; // ReSharper disable InconsistentNaming @@ -43,7 +44,6 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId 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; @@ -167,31 +167,38 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId { try { - this.httpClient = new HttpClient + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(14)); + var dataSource = new DataSourceERI_V1 { - BaseAddress = new Uri($"{this.dataHostname}:{this.dataPort}"), - Timeout = TimeSpan.FromSeconds(5), + Hostname = this.dataHostname, + Port = this.dataPort }; - - using (this.httpClient) + + using var client = ERIClientFactory.Get(ERIVersion.V1, dataSource); + if(client is null) { - 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."); + 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 is not supported."; + return; } + + var authSchemes = await client.GetAuthMethodsAsync(cts.Token); + if (!authSchemes.Successful) + { + await this.form.Validate(); + + Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1); + this.dataIssues[^1] = authSchemes.Message; + return; + } + + this.availableAuthMethods = authSchemes.Data!.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) { diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor new file mode 100644 index 00000000..cbd951dc --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor @@ -0,0 +1,52 @@ + + + + + + @if (!this.IsDirectoryAvailable) + { + + The directory chosen for the data source does not exist anymore. Please edit the data source and correct the path. + + } + else + { + + The directory chosen for the data source exists. + + } + + + @if (this.IsCloudEmbedding) + { + + The embedding runs in the cloud. All your data from the folder '@this.DataSource.Path' and all its subdirectories + will be sent to the cloud. + + } + else + { + + The embedding runs locally or in your organization. Your data is not sent to the cloud. + + } + + + + @if (this.directorySizeNumFiles > 100) + { + + For performance reasons, only the first 100 files are shown. The directory contains @this.NumberFilesInDirectory files in total. + + } + + + + + @if (this.IsOperationInProgress) + { + + } + Close + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs new file mode 100644 index 00000000..ee525e01 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs @@ -0,0 +1,112 @@ +using System.Text; + +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +using Microsoft.AspNetCore.Components; + +using Timer = System.Timers.Timer; + +namespace AIStudio.Dialogs; + +public partial class DataSourceLocalDirectoryInfoDialog : ComponentBase, IAsyncDisposable +{ + [CascadingParameter] + private MudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public DataSourceLocalDirectory DataSource { get; set; } + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + private readonly Timer refreshTimer = new(TimeSpan.FromSeconds(1.6)) + { + AutoReset = true, + }; + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + this.embeddingProvider = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.DataSource.EmbeddingId); + this.directoryInfo = new DirectoryInfo(this.DataSource.Path); + + if (this.directoryInfo.Exists) + { + this.directorySizeTask = this.directoryInfo.DetermineContentSize(this.UpdateDirectorySize, this.UpdateDirectoryFiles, this.UpdateFileList, MAX_FILES_TO_SHOW, this.DirectoryOperationDone, this.cts.Token); + this.refreshTimer.Elapsed += (_, _) => this.InvokeAsync(this.StateHasChanged); + this.refreshTimer.Start(); + } + + await base.OnInitializedAsync(); + } + + #endregion + + private const int MAX_FILES_TO_SHOW = 100; + + private readonly CancellationTokenSource cts = new(); + + private EmbeddingProvider embeddingProvider; + private DirectoryInfo directoryInfo = null!; + private long directorySizeBytes; + private long directorySizeNumFiles; + private readonly StringBuilder directoryFiles = new(); + private Task directorySizeTask = Task.CompletedTask; + + private bool IsOperationInProgress { get; set; } = true; + + private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted; + + private bool IsDirectoryAvailable => this.directoryInfo.Exists; + + private void UpdateFileList(string file) + { + this.directoryFiles.Append("- "); + this.directoryFiles.AppendLine(file); + } + + private void UpdateDirectorySize(long size) + { + this.directorySizeBytes = size; + } + + private void UpdateDirectoryFiles(long numFiles) => this.directorySizeNumFiles = numFiles; + + private void DirectoryOperationDone() + { + this.refreshTimer.Stop(); + this.IsOperationInProgress = false; + this.InvokeAsync(this.StateHasChanged); + } + + private string NumberFilesInDirectory => $"{this.directorySizeNumFiles:###,###,###,###}"; + + private void Close() + { + this.cts.Cancel(); + this.MudDialog.Close(); + } + + #region Implementation of IDisposable + + public async ValueTask DisposeAsync() + { + try + { + await this.cts.CancelAsync(); + await this.directorySizeTask; + + this.cts.Dispose(); + this.refreshTimer.Stop(); + this.refreshTimer.Dispose(); + } + catch + { + // ignored + } + } + + #endregion +} \ 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 index feaf5a79..6a2cd14d 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs @@ -23,7 +23,6 @@ public partial class DataSourceLocalFileDialog : ComponentBase [Inject] private SettingsManager SettingsManager { get; init; } = null!; - private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); private readonly DataSourceValidation dataSourceValidation; diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor new file mode 100644 index 00000000..6fb4b12c --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor @@ -0,0 +1,39 @@ + + + + + + @if (!this.IsFileAvailable) + { + + The file chosen for the data source does not exist anymore. Please edit the data source and choose another file or correct the path. + + } + else + { + + The file chosen for the data source exists. + + } + + + @if (this.IsCloudEmbedding) + { + + The embedding runs in the cloud. All your data within the + file '@this.DataSource.FilePath' will be sent to the cloud. + + } + else + { + + The embedding runs locally or in your organization. Your data is not sent to the cloud. + + } + + + + + Close + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs new file mode 100644 index 00000000..a4a5c4a3 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs @@ -0,0 +1,40 @@ +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +public partial class DataSourceLocalFileInfoDialog : ComponentBase +{ + [CascadingParameter] + private MudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public DataSourceLocalFile DataSource { get; set; } + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + this.embeddingProvider = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.DataSource.EmbeddingId); + this.fileInfo = new FileInfo(this.DataSource.FilePath); + await base.OnInitializedAsync(); + } + + #endregion + + private EmbeddingProvider embeddingProvider; + private FileInfo fileInfo = null!; + + private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted; + + private bool IsFileAvailable => this.fileInfo.Exists; + + private string FileSize => this.fileInfo.FileSize(); + + private void Close() => this.MudDialog.Close(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index ccc96dff..0d6fc288 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -53,10 +53,6 @@ - - - - diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs index 2b2aa4a8..5b8d63e4 100644 --- a/app/MindWork AI Studio/Pages/Home.razor.cs +++ b/app/MindWork AI Studio/Pages/Home.razor.cs @@ -26,7 +26,7 @@ public partial class Home : ComponentBase private async Task ReadLastChangeAsync() { var latest = Changelog.LOGS.MaxBy(n => n.Build); - var response = await this.HttpClient.GetAsync($"changelog/{latest.Filename}"); + using var response = await this.HttpClient.GetAsync($"changelog/{latest.Filename}"); this.LastChangeContent = await response.Content.ReadAsStringAsync(); } diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index 25143834..2899e4f5 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -103,9 +103,14 @@ public abstract class BaseProvider : IProvider, ISecretId { using var request = await requestBuilder(); + // // Send the request with the ResponseHeadersRead option. // This allows us to read the stream as soon as the headers are received. // This is important because we want to stream the responses. + // + // Please notice: We do not dispose the response here. The caller is responsible + // for disposing the response object. This is important because the response + // object is used to read the stream. var nextResponse = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); if (nextResponse.IsSuccessStatusCode) { diff --git a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs index e0d5da77..942cb245 100644 --- a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs +++ b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs @@ -136,8 +136,8 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela if (secretKey is null) return default; - var request = new HttpRequestMessage(HttpMethod.Get, $"models?key={secretKey}"); - var response = await this.httpClient.SendAsync(request, token); + using var request = new HttpRequestMessage(HttpMethod.Get, $"models?key={secretKey}"); + using var response = await this.httpClient.SendAsync(request, token); if(!response.IsSuccessStatusCode) return default; diff --git a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs index 2fc7e88f..f32a31b5 100644 --- a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs +++ b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs @@ -127,10 +127,10 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o if (secretKey is null) return []; - var request = new HttpRequestMessage(HttpMethod.Get, "models"); + using var request = new HttpRequestMessage(HttpMethod.Get, "models"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey); - var response = await this.httpClient.SendAsync(request, token); + using var response = await this.httpClient.SendAsync(request, token); if(!response.IsSuccessStatusCode) return []; diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index ebde4c7b..024f60d3 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -138,10 +138,10 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api. if (secretKey is null) return default; - var request = new HttpRequestMessage(HttpMethod.Get, "models"); + using var request = new HttpRequestMessage(HttpMethod.Get, "models"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey); - var response = await this.httpClient.SendAsync(request, token); + using var response = await this.httpClient.SendAsync(request, token); if(!response.IsSuccessStatusCode) return default; diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index 6ddeeabc..ed092174 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -154,10 +154,10 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o if (secretKey is null) return []; - var request = new HttpRequestMessage(HttpMethod.Get, "models"); + using var request = new HttpRequestMessage(HttpMethod.Get, "models"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey); - var response = await this.httpClient.SendAsync(request, token); + using var response = await this.httpClient.SendAsync(request, token); if(!response.IsSuccessStatusCode) return []; diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index 3abda28c..4ba45c6b 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -154,11 +154,11 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam } }; - var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models"); + using var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models"); if(secretKey is not null) lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKeyProvisional); - var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token); + using var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token); if(!lmStudioResponse.IsSuccessStatusCode) return []; diff --git a/app/MindWork AI Studio/Provider/X/ProviderX.cs b/app/MindWork AI Studio/Provider/X/ProviderX.cs index c915c964..a8334c8d 100644 --- a/app/MindWork AI Studio/Provider/X/ProviderX.cs +++ b/app/MindWork AI Studio/Provider/X/ProviderX.cs @@ -127,10 +127,10 @@ public sealed class ProviderX(ILogger logger) : BaseProvider("https://api.x.ai/v if (secretKey is null) return []; - var request = new HttpRequestMessage(HttpMethod.Get, "models"); + using var request = new HttpRequestMessage(HttpMethod.Get, "models"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey); - var response = await this.httpClient.SendAsync(request, token); + using var response = await this.httpClient.SendAsync(request, token); if(!response.IsSuccessStatusCode) return []; diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs index 387accf0..addec256 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs @@ -1,6 +1,7 @@ -using ERI_Client.V1; - // ReSharper disable InconsistentNaming + +using AIStudio.Tools.ERIClient.DataModel; + namespace AIStudio.Settings.DataModel; /// @@ -24,23 +25,15 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource /// 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/IERIDataSource.cs b/app/MindWork AI Studio/Settings/IERIDataSource.cs index 34874fcc..0d917d18 100644 --- a/app/MindWork AI Studio/Settings/IERIDataSource.cs +++ b/app/MindWork AI Studio/Settings/IERIDataSource.cs @@ -1,12 +1,26 @@ -using ERI_Client.V1; +using AIStudio.Tools.ERIClient.DataModel; namespace AIStudio.Settings; public interface IERIDataSource : IExternalDataSource { + /// + /// The hostname of the ERI server. + /// public string Hostname { get; init; } + /// + /// The port of the ERI server. + /// public int Port { get; init; } + /// + /// The authentication method to use. + /// public AuthMethod AuthMethod { get; init; } + + /// + /// The username to use for authentication, when the auth. method is USERNAME_PASSWORD. + /// + public string Username { 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 index e26a20b5..4d11017a 100644 --- a/app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs +++ b/app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs @@ -1,4 +1,4 @@ -using ERI_Client.V1; +using AIStudio.Tools.ERIClient.DataModel; namespace AIStudio.Tools; diff --git a/app/MindWork AI Studio/Tools/DirectoryInfoExtensions.cs b/app/MindWork AI Studio/Tools/DirectoryInfoExtensions.cs new file mode 100644 index 00000000..70adcab7 --- /dev/null +++ b/app/MindWork AI Studio/Tools/DirectoryInfoExtensions.cs @@ -0,0 +1,66 @@ +namespace AIStudio.Tools; + +public static class DirectoryInfoExtensions +{ + private static readonly EnumerationOptions ENUMERATION_OPTIONS = new() + { + IgnoreInaccessible = true, + RecurseSubdirectories = true, + ReturnSpecialDirectories = false, + }; + + /// + /// Determines the size of the directory and all its subdirectories, as well as the number of files. When desired, + /// it can report the found files up to a certain limit. + /// + /// + /// You might set reportMaxFiles to a negative value to report all files. Any positive value will limit the number + /// of reported files. The cancellation token can be used to stop the operation. The cancellation operation is also able + /// to cancel slow operations, e.g., when the directory is on a slow network drive. + /// + /// After stopping the operation, the total size and number of files are reported as they were at the time of cancellation. + /// + /// Please note that the entire operation is done on a background thread. Thus, when reporting the found files or the + /// current total size, you need to use the appropriate dispatcher to update the UI. Usually, you can use the InvokeAsync + /// method to update the UI from a background thread. + /// + /// The root directory to determine the size of. + /// The callback to report the current total size of the directory. + /// The callback to report the current number of files found. + /// The callback to report the next file found. The file name is relative to the root directory. + /// The maximum number of files to report. A negative value reports all files. + /// The callback to report that the operation is done. + /// The cancellation token to stop the operation. + public static async Task DetermineContentSize(this DirectoryInfo directoryInfo, Action reportCurrentTotalSize, Action reportCurrentNumFiles, Action reportNextFile, int reportMaxFiles = -1, Action? done = null, CancellationToken cancellationToken = default) + { + var rootDirectoryLen = directoryInfo.FullName.Length; + long totalSize = 0; + long numFiles = 0; + + await Task.Factory.StartNew(() => { + foreach (var file in directoryInfo.EnumerateFiles("*", ENUMERATION_OPTIONS)) + { + if (cancellationToken.IsCancellationRequested) + return; + + totalSize += file.Length; + numFiles++; + + if (numFiles % 100 == 0) + { + reportCurrentTotalSize(totalSize); + reportCurrentNumFiles(numFiles); + } + + if (reportMaxFiles < 0 || numFiles <= reportMaxFiles) + reportNextFile(file.FullName[rootDirectoryLen..]); + } + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); + + reportCurrentTotalSize(totalSize); + reportCurrentNumFiles(numFiles); + + if(done is not null) + done(); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/APIResponse.cs b/app/MindWork AI Studio/Tools/ERIClient/APIResponse.cs new file mode 100644 index 00000000..1f82914c --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/APIResponse.cs @@ -0,0 +1,19 @@ +namespace AIStudio.Tools.ERIClient; + +public sealed class APIResponse +{ + /// + /// Was the API call successful? + /// + public bool Successful { get; set; } + + /// + /// When the API call was not successful, this will contain the error message. + /// + public string Message { get; set; } = string.Empty; + + /// + /// The data returned by the API call. + /// + public T? Data { get; set; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthField.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthField.cs new file mode 100644 index 00000000..89727430 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthField.cs @@ -0,0 +1,13 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// An authentication field. +/// +public enum AuthField +{ + NONE, + USERNAME, + PASSWORD, + TOKEN, + KERBEROS_TICKET, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthFieldMapping.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthFieldMapping.cs new file mode 100644 index 00000000..e11c3d9f --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthFieldMapping.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// The mapping between an AuthField and the field name in the authentication request. +/// +/// The AuthField that is mapped to the field name. +/// The field name in the authentication request. +public record AuthFieldMapping(AuthField AuthField, string FieldName); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthMethod.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthMethod.cs new file mode 100644 index 00000000..7494ce9b --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthMethod.cs @@ -0,0 +1,9 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +public enum AuthMethod +{ + NONE, + KERBEROS, + USERNAME_PASSWORD, + TOKEN, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthResponse.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthResponse.cs new file mode 100644 index 00000000..cdc325dd --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthResponse.cs @@ -0,0 +1,9 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// The response to an authentication request. +/// +/// True, when the authentication was successful. +/// The token to use for further requests. +/// When the authentication was not successful, this contains the reason. +public readonly record struct AuthResponse(bool Success, string? Token, string? Message); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthScheme.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthScheme.cs new file mode 100644 index 00000000..bde0175b --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthScheme.cs @@ -0,0 +1,9 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// Describes one authentication scheme for this data source. +/// +/// The method used for authentication, e.g., "API Key," "Username/Password," etc. +/// A list of field mappings for the authentication method. The client must know, +/// e.g., how the password field is named in the request. +public readonly record struct AuthScheme(AuthMethod AuthMethod, List AuthFieldMappings); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/ChatThread.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ChatThread.cs new file mode 100644 index 00000000..8d6a0983 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ChatThread.cs @@ -0,0 +1,7 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// A chat thread, which is a list of content blocks. +/// +/// The content blocks in this chat thread. +public readonly record struct ChatThread(List ContentBlocks); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/ContentBlock.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ContentBlock.cs new file mode 100644 index 00000000..0a46d3b5 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ContentBlock.cs @@ -0,0 +1,12 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// A block of content of a chat thread. +/// +/// +/// Images and other media are base64 encoded. +/// +/// The content of the block. Remember that images and other media are base64 encoded. +/// The role of the content in the chat thread. +/// The type of the content, e.g., text, image, video, etc. +public readonly record struct ContentBlock(string Content, Role Role, ContentType Type); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/ContentType.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ContentType.cs new file mode 100644 index 00000000..14203728 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ContentType.cs @@ -0,0 +1,16 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// The type of content. +/// +public enum ContentType +{ + NONE, + UNKNOWN, + + TEXT, + IMAGE, + VIDEO, + AUDIO, + SPEECH, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/Context.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/Context.cs new file mode 100644 index 00000000..fb5c13f5 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/Context.cs @@ -0,0 +1,27 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// Matching context returned by the data source as a result of a retrieval request. +/// +/// The name of the source, e.g., a document name, database name, +/// collection name, etc. +/// What are the contents of the source? For example, is it a +/// dictionary, a book chapter, business concept, a paper, etc. +/// The path to the content, e.g., a URL, a file path, a path in a +/// graph database, etc. +/// The type of the content, e.g., text, image, video, audio, speech, etc. +/// The content that matched the user prompt. For text, you +/// return the matched text and, e.g., three words before and after it. +/// The surrounding content of the matched content. +/// For text, you may return, e.g., one sentence or paragraph before and after +/// the matched content. +/// Links to related content, e.g., links to Wikipedia articles, +/// links to sources, etc. +public readonly record struct Context( + string Name, + string Category, + string? Path, + ContentType Type, + string MatchedContent, + string[] SurroundingContent, + string[] Links); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/DataSourceInfo.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/DataSourceInfo.cs new file mode 100644 index 00000000..07a8f92e --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/DataSourceInfo.cs @@ -0,0 +1,9 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// Information about the data source. +/// +/// The name of the data source, e.g., "Internal Organization Documents." +/// A short description of the data source. What kind of data does it contain? +/// What is the data source used for? +public readonly record struct DataSourceInfo(string Name, string Description); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/EmbeddingInfo.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/EmbeddingInfo.cs new file mode 100644 index 00000000..47285126 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/EmbeddingInfo.cs @@ -0,0 +1,20 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// 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. +/// +/// What kind of embedding is used. For example, "Transformer Embedding," "Contextual Word +/// Embedding," "Graph Embedding," etc. +/// Name the embedding used. This can be a library, a framework, or the name of the used +/// algorithm. +/// A short description of the embedding. Describe what the embedding is doing. +/// Describe when the embedding is used. For example, when the user prompt contains certain +/// keywords, or anytime? +/// A link to the embedding's documentation or the source code. Might be null. +public readonly record struct EmbeddingInfo( + string EmbeddingType, + string EmbeddingName, + string Description, + string UsedWhen, + string? Link); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderType.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderType.cs new file mode 100644 index 00000000..008811f8 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderType.cs @@ -0,0 +1,22 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// Known types of providers that can process data. +/// +public enum ProviderType +{ + /// + /// The related data is not allowed to be sent to any provider. + /// + NONE, + + /// + /// The related data can be sent to any provider. + /// + ANY, + + /// + /// The related data can be sent to a provider that is hosted by the same organization, either on-premises or locally. + /// + SELF_HOSTED, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderTypeExtensions.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderTypeExtensions.cs new file mode 100644 index 00000000..ecdbcc19 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderTypeExtensions.cs @@ -0,0 +1,13 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +public static class ProviderTypeExtensions +{ + public static string Explain(this ProviderType providerType) => providerType switch + { + ProviderType.NONE => "The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment.", + ProviderType.ANY => "The related data can be sent to any provider, regardless of where it is hosted (cloud or self-hosted).", + ProviderType.SELF_HOSTED => "The related data can be sent to a provider that is hosted by the same organization, either on-premises or locally. Cloud-based providers are not allowed.", + + _ => "Unknown configuration. This data source cannot be used at the moment.", + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/RetrievalInfo.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/RetrievalInfo.cs new file mode 100644 index 00000000..cdd71a6b --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/RetrievalInfo.cs @@ -0,0 +1,20 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// Information about a retrieval process, which this data source implements. +/// +/// A unique identifier for the retrieval process. This can be a GUID, a unique name, or an increasing integer. +/// The name of the retrieval process, e.g., "Keyword-Based Wikipedia Article Retrieval". +/// A short description of the retrieval process. What kind of retrieval process is it? +/// A link to the retrieval process's documentation, paper, Wikipedia article, or the source code. Might be null. +/// 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. +/// A list of embeddings used in this retrieval process. It might be empty in case no embedding is used. +public readonly record struct RetrievalInfo( + string Id, + string Name, + string Description, + string? Link, + Dictionary? ParametersDescription, + List Embeddings); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/RetrievalRequest.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/RetrievalRequest.cs new file mode 100644 index 00000000..abeac50b --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/RetrievalRequest.cs @@ -0,0 +1,25 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// The retrieval request sent by AI Studio. +/// +/// +/// Images and other media are base64 encoded. +/// +/// The latest user prompt that AI Studio received. +/// The type of the latest user prompt, e.g., text, image, etc. +/// The chat thread that the user is currently in. +/// 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. +/// 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. +/// 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. +public readonly record struct RetrievalRequest( + string LatestUserPrompt, + ContentType LatestUserPromptType, + ChatThread Thread, + string? RetrievalProcessId, + Dictionary? Parameters, + int MaxMatches); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/Role.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/Role.cs new file mode 100644 index 00000000..f19376f7 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/Role.cs @@ -0,0 +1,15 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// Possible roles of any chat thread. +/// +public enum Role +{ + NONE, + UNKNOW, + + SYSTEM, + USER, + AI, + AGENT, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/SecurityRequirements.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/SecurityRequirements.cs new file mode 100644 index 00000000..aa4df1f5 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/SecurityRequirements.cs @@ -0,0 +1,7 @@ +namespace AIStudio.Tools.ERIClient.DataModel; + +/// +/// Represents the security requirements for this data source. +/// +/// Which provider types are allowed to process the data? +public readonly record struct SecurityRequirements(ProviderType AllowedProviderType); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/ERIClientBase.cs b/app/MindWork AI Studio/Tools/ERIClient/ERIClientBase.cs new file mode 100644 index 00000000..1906a0d5 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/ERIClientBase.cs @@ -0,0 +1,36 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AIStudio.Tools.ERIClient; + +public abstract class ERIClientBase(string baseAddress) : IDisposable +{ + protected static readonly JsonSerializerOptions JSON_OPTIONS = new() + { + WriteIndented = true, + AllowTrailingCommas = true, + PropertyNamingPolicy = null, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper), + } + }; + + protected readonly HttpClient httpClient = new() + { + BaseAddress = new Uri(baseAddress), + }; + + protected string securityToken = string.Empty; + + #region Implementation of IDisposable + + public void Dispose() + { + this.httpClient.Dispose(); + } + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/ERIClientFactory.cs b/app/MindWork AI Studio/Tools/ERIClient/ERIClientFactory.cs new file mode 100644 index 00000000..28581646 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/ERIClientFactory.cs @@ -0,0 +1,14 @@ +using AIStudio.Assistants.ERI; +using AIStudio.Settings; + +namespace AIStudio.Tools.ERIClient; + +public static class ERIClientFactory +{ + public static IERIClient? Get(ERIVersion version, IERIDataSource dataSource) => version switch + { + ERIVersion.V1 => new ERIClientV1($"{dataSource.Hostname}:{dataSource.Port}"), + + _ => null + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/ERIClientV1.cs b/app/MindWork AI Studio/Tools/ERIClient/ERIClientV1.cs new file mode 100644 index 00000000..62e3f556 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/ERIClientV1.cs @@ -0,0 +1,344 @@ +using System.Text; +using System.Text.Json; + +using AIStudio.Settings; +using AIStudio.Tools.ERIClient.DataModel; + +namespace AIStudio.Tools.ERIClient; + +public class ERIClientV1(string baseAddress) : ERIClientBase(baseAddress), IERIClient +{ + #region Implementation of IERIClient + + public async Task>> GetAuthMethodsAsync(CancellationToken cancellationToken = default) + { + using var response = await this.httpClient.GetAsync("/auth/methods", cancellationToken); + if(!response.IsSuccessStatusCode) + { + return new() + { + Successful = false, + Message = $"Failed to retrieve the authentication methods: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}" + }; + } + + var authMethods = await response.Content.ReadFromJsonAsync>(JSON_OPTIONS, cancellationToken); + if(authMethods is null) + { + return new() + { + Successful = false, + Message = "Failed to retrieve the authentication methods: the ERI server did not return a valid response." + }; + } + + return new() + { + Successful = true, + Data = authMethods + }; + } + + public async Task> AuthenticateAsync(IERIDataSource dataSource, RustService rustService, CancellationToken cancellationToken = default) + { + var authMethod = dataSource.AuthMethod; + var username = dataSource.Username; + switch (dataSource.AuthMethod) + { + case AuthMethod.NONE: + using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}")) + { + using var noneAuthResponse = await this.httpClient.SendAsync(request, cancellationToken); + if(!noneAuthResponse.IsSuccessStatusCode) + { + return new() + { + Successful = false, + Message = $"Failed to authenticate with the ERI server. Code: {noneAuthResponse.StatusCode}, Reason: {noneAuthResponse.ReasonPhrase}" + }; + } + + var noneAuthResult = await noneAuthResponse.Content.ReadFromJsonAsync(JSON_OPTIONS, cancellationToken); + if(noneAuthResult == default) + { + return new() + { + Successful = false, + Message = "Failed to authenticate with the ERI server: the response was invalid." + }; + } + + this.securityToken = noneAuthResult.Token ?? string.Empty; + return new() + { + Successful = true, + Data = noneAuthResult + }; + } + + case AuthMethod.USERNAME_PASSWORD: + var passwordResponse = await rustService.GetSecret(dataSource); + if (!passwordResponse.Success) + { + return new() + { + Successful = false, + Message = "Failed to retrieve the password." + }; + } + + var password = await passwordResponse.Secret.Decrypt(Program.ENCRYPTION); + using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}")) + { + // We must send both values inside the header. The username field is named 'user'. + // The password field is named 'password'. + request.Headers.Add("user", username); + request.Headers.Add("password", password); + + using var usernamePasswordAuthResponse = await this.httpClient.SendAsync(request, cancellationToken); + if(!usernamePasswordAuthResponse.IsSuccessStatusCode) + { + return new() + { + Successful = false, + Message = $"Failed to authenticate with the ERI server. Code: {usernamePasswordAuthResponse.StatusCode}, Reason: {usernamePasswordAuthResponse.ReasonPhrase}" + }; + } + + var usernamePasswordAuthResult = await usernamePasswordAuthResponse.Content.ReadFromJsonAsync(JSON_OPTIONS, cancellationToken); + if(usernamePasswordAuthResult == default) + { + return new() + { + Successful = false, + Message = "Failed to authenticate with the server: the response was invalid." + }; + } + + this.securityToken = usernamePasswordAuthResult.Token ?? string.Empty; + return new() + { + Successful = true, + Data = usernamePasswordAuthResult + }; + } + + case AuthMethod.TOKEN: + var tokenResponse = await rustService.GetSecret(dataSource); + if (!tokenResponse.Success) + { + return new() + { + Successful = false, + Message = "Failed to retrieve the access token." + }; + } + + var token = await tokenResponse.Secret.Decrypt(Program.ENCRYPTION); + using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}")) + { + request.Headers.Add("Authorization", $"Bearer {token}"); + + using var tokenAuthResponse = await this.httpClient.SendAsync(request, cancellationToken); + if(!tokenAuthResponse.IsSuccessStatusCode) + { + return new() + { + Successful = false, + Message = $"Failed to authenticate with the ERI server. Code: {tokenAuthResponse.StatusCode}, Reason: {tokenAuthResponse.ReasonPhrase}" + }; + } + + var tokenAuthResult = await tokenAuthResponse.Content.ReadFromJsonAsync(JSON_OPTIONS, cancellationToken); + if(tokenAuthResult == default) + { + return new() + { + Successful = false, + Message = "Failed to authenticate with the ERI server: the response was invalid." + }; + } + + this.securityToken = tokenAuthResult.Token ?? string.Empty; + return new() + { + Successful = true, + Data = tokenAuthResult + }; + } + + default: + this.securityToken = string.Empty; + return new() + { + Successful = false, + Message = "The authentication method is not supported yet." + }; + } + } + + public async Task> GetDataSourceInfoAsync(CancellationToken cancellationToken = default) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/dataSource"); + request.Headers.Add("token", this.securityToken); + + using var response = await this.httpClient.SendAsync(request, cancellationToken); + if(!response.IsSuccessStatusCode) + { + return new() + { + Successful = false, + Message = $"Failed to retrieve the data source information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}" + }; + } + + var dataSourceInfo = await response.Content.ReadFromJsonAsync(JSON_OPTIONS, cancellationToken); + if(dataSourceInfo == default) + { + return new() + { + Successful = false, + Message = "Failed to retrieve the data source information: the ERI server did not return a valid response." + }; + } + + return new() + { + Successful = true, + Data = dataSourceInfo + }; + } + + public async Task>> GetEmbeddingInfoAsync(CancellationToken cancellationToken = default) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/embedding/info"); + request.Headers.Add("token", this.securityToken); + + using var response = await this.httpClient.SendAsync(request, cancellationToken); + if(!response.IsSuccessStatusCode) + { + return new() + { + Successful = false, + Message = $"Failed to retrieve the embedding information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}" + }; + } + + var embeddingInfo = await response.Content.ReadFromJsonAsync>(JSON_OPTIONS, cancellationToken); + if(embeddingInfo is null) + { + return new() + { + Successful = false, + Message = "Failed to retrieve the embedding information: the ERI server did not return a valid response." + }; + } + + return new() + { + Successful = true, + Data = embeddingInfo + }; + } + + public async Task>> GetRetrievalInfoAsync(CancellationToken cancellationToken = default) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/retrieval/info"); + request.Headers.Add("token", this.securityToken); + + using var response = await this.httpClient.SendAsync(request, cancellationToken); + if(!response.IsSuccessStatusCode) + { + return new() + { + Successful = false, + Message = $"Failed to retrieve the retrieval information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}" + }; + } + + var retrievalInfo = await response.Content.ReadFromJsonAsync>(JSON_OPTIONS, cancellationToken); + if(retrievalInfo is null) + { + return new() + { + Successful = false, + Message = "Failed to retrieve the retrieval information: the ERI server did not return a valid response." + }; + } + + return new() + { + Successful = true, + Data = retrievalInfo + }; + } + + public async Task>> ExecuteRetrievalAsync(RetrievalRequest request, CancellationToken cancellationToken = default) + { + using var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/retrieval"); + requestMessage.Headers.Add("token", this.securityToken); + + using var content = new StringContent(JsonSerializer.Serialize(request, JSON_OPTIONS), Encoding.UTF8, "application/json"); + requestMessage.Content = content; + + using var response = await this.httpClient.SendAsync(requestMessage, cancellationToken); + if(!response.IsSuccessStatusCode) + { + return new() + { + Successful = false, + Message = $"Failed to execute the retrieval request: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}" + }; + } + + var contexts = await response.Content.ReadFromJsonAsync>(JSON_OPTIONS, cancellationToken); + if(contexts is null) + { + return new() + { + Successful = false, + Message = "Failed to execute the retrieval request: the ERI server did not return a valid response." + }; + } + + return new() + { + Successful = true, + Data = contexts + }; + } + + public async Task> GetSecurityRequirementsAsync(CancellationToken cancellationToken = default) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/security/requirements"); + request.Headers.Add("token", this.securityToken); + + using var response = await this.httpClient.SendAsync(request, cancellationToken); + if(!response.IsSuccessStatusCode) + { + return new() + { + Successful = false, + Message = $"Failed to retrieve the security requirements: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}" + }; + } + + var securityRequirements = await response.Content.ReadFromJsonAsync(JSON_OPTIONS, cancellationToken); + if(securityRequirements == default) + { + return new() + { + Successful = false, + Message = "Failed to retrieve the security requirements: the ERI server did not return a valid response." + }; + } + + return new() + { + Successful = true, + Data = securityRequirements + }; + } + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ERIClient/IERIClient.cs b/app/MindWork AI Studio/Tools/ERIClient/IERIClient.cs new file mode 100644 index 00000000..9cf27fbf --- /dev/null +++ b/app/MindWork AI Studio/Tools/ERIClient/IERIClient.cs @@ -0,0 +1,62 @@ +using AIStudio.Settings; +using AIStudio.Tools.ERIClient.DataModel; + +namespace AIStudio.Tools.ERIClient; + +public interface IERIClient : IDisposable +{ + /// + /// Retrieves the available authentication methods from the ERI server. + /// + /// + /// No authentication is required to retrieve the available authentication methods. + /// + /// The cancellation token. + /// The available authentication methods. + public Task>> GetAuthMethodsAsync(CancellationToken cancellationToken = default); + + /// + /// Authenticate the user to the ERI server. + /// + /// The data source to use. + /// The Rust service. + /// The cancellation token. + /// The authentication response. + public Task> AuthenticateAsync(IERIDataSource dataSource, RustService rustService, CancellationToken cancellationToken = default); + + /// + /// Retrieves the data source information from the ERI server. + /// + /// The cancellation token. + /// The data source information. + public Task> GetDataSourceInfoAsync(CancellationToken cancellationToken = default); + + /// + /// Retrieves the embedding information from the ERI server. + /// + /// The cancellation token. + /// A list of embedding information. + public Task>> GetEmbeddingInfoAsync(CancellationToken cancellationToken = default); + + /// + /// Retrieves the retrieval information from the ERI server. + /// + /// The cancellation token. + /// A list of retrieval information. + public Task>> GetRetrievalInfoAsync(CancellationToken cancellationToken = default); + + /// + /// Executes a retrieval request on the ERI server. + /// + /// The retrieval request. + /// The cancellation token. + /// The retrieved contexts to use for augmentation and generation. + public Task>> ExecuteRetrievalAsync(RetrievalRequest request, CancellationToken cancellationToken = default); + + /// + /// Retrieves the security requirements from the ERI server. + /// + /// The cancellation token. + /// The security requirements. + public Task> GetSecurityRequirementsAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/FileInfoExtensions.cs b/app/MindWork AI Studio/Tools/FileInfoExtensions.cs new file mode 100644 index 00000000..c3002624 --- /dev/null +++ b/app/MindWork AI Studio/Tools/FileInfoExtensions.cs @@ -0,0 +1,17 @@ +namespace AIStudio.Tools; + +public static class FileInfoExtensions +{ + /// + /// Returns the file size in human-readable format. + /// + /// The file info object. + /// The file size in human-readable format. + public static string FileSize(this FileInfo fileInfo) + { + if (!fileInfo.Exists) + return "N/A"; + + return fileInfo.Length.FileSize(); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/LongExtensions.cs b/app/MindWork AI Studio/Tools/LongExtensions.cs new file mode 100644 index 00000000..3209e47a --- /dev/null +++ b/app/MindWork AI Studio/Tools/LongExtensions.cs @@ -0,0 +1,22 @@ +namespace AIStudio.Tools; + +public static class LongExtensions +{ + /// + /// Formats the file size in a human-readable format. + /// + /// The size in bytes. + /// The formatted file size. + public static string FileSize(this long sizeBytes) + { + string[] sizes = { "B", "kB", "MB", "GB", "TB" }; + var order = 0; + while (sizeBytes >= 1024 && order < sizes.Length - 1) + { + order++; + sizeBytes /= 1024; + } + + return $"{sizeBytes:0.##} {sizes[order]}"; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Validation/DataSourceValidation.cs b/app/MindWork AI Studio/Tools/Validation/DataSourceValidation.cs index f55f906a..cdd2f131 100644 --- a/app/MindWork AI Studio/Tools/Validation/DataSourceValidation.cs +++ b/app/MindWork AI Studio/Tools/Validation/DataSourceValidation.cs @@ -1,4 +1,4 @@ -using ERI_Client.V1; +using AIStudio.Tools.ERIClient.DataModel; namespace AIStudio.Tools.Validation; diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 17d7a388..2941da3d 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -210,6 +210,6 @@ "type": "Project" } }, - "net8.0/osx-x64": {} + "net8.0/osx-arm64": {} } } \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.28.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.28.md new file mode 100644 index 00000000..29875ade --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.28.md @@ -0,0 +1,4 @@ +# v0.9.28, build 203 (2025-0x-xx xx:xx UTC) +- Added an information view to all data sources to the data source configuration page. The data source configuration is a preview feature behind the RAG feature flag. +- Added a ERI ((E)xternal (R)etrieval (I)nterface) client for communication with any ERI server. +- Improved the resource handling when loading models. \ No newline at end of file