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