From d02d6b5870eafb6f9951ef2b612337ecf78534df Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 31 May 2025 18:57:40 +0200 Subject: [PATCH 01/28] Add enterprise config support via env vars & Windows registry --- app/MindWork AI Studio/Program.cs | 24 +++++ .../Tools/Services/RustService.Enterprise.cs | 44 +++++++++ runtime/Cargo.lock | 30 +++++- runtime/Cargo.toml | 4 + runtime/src/environment.rs | 96 +++++++++++++++++++ runtime/src/runtime_api.rs | 2 + 6 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 1630a7a7..81b25fa7 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -208,6 +208,30 @@ public static async Task Main() await rust.AppIsReady(); programLogger.LogInformation("The AI Studio server is ready."); + // + // Read the enterprise environment for the current user's configuration: + // + var enterpriseConfigServerUrl = await RUST_SERVICE.EnterpriseEnvConfigServerUrl(); + var enterpriseConfigId = await RUST_SERVICE.EnterpriseEnvConfigId(); + switch (enterpriseConfigServerUrl) + { + case null when enterpriseConfigId == Guid.Empty: + programLogger.LogInformation("AI Studio runs without an enterprise configuration."); + break; + + case null: + programLogger.LogWarning($"AI Studio runs with an enterprise configuration id ('{enterpriseConfigId}'), but the configuration server URL is not set."); + break; + + case not null when enterpriseConfigId == Guid.Empty: + programLogger.LogWarning($"AI Studio runs with an enterprise configuration server URL ('{enterpriseConfigServerUrl}'), but the configuration ID is not set."); + break; + + default: + programLogger.LogInformation($"AI Studio runs with an enterprise configuration id ('{enterpriseConfigId}') and configuration server URL ('{enterpriseConfigServerUrl}')."); + break; + } + TaskScheduler.UnobservedTaskException += (sender, taskArgs) => { programLogger.LogError(taskArgs.Exception, $"Unobserved task exception by sender '{sender ?? "n/a"}'."); diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs b/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs new file mode 100644 index 00000000..6fae98af --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs @@ -0,0 +1,44 @@ +namespace AIStudio.Tools.Services; + +public sealed partial class RustService +{ + /// + /// Tries to read the enterprise environment for the current user's configuration ID. + /// + /// + /// Returns the empty Guid when the environment is not set or the request fails. + /// Otherwise, the configuration ID. + /// + public async Task EnterpriseEnvConfigId() + { + var result = await this.http.GetAsync("/system/enterprise/config/id"); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to query the enterprise configuration ID: '{result.StatusCode}'"); + return Guid.Empty; + } + + Guid.TryParse(await result.Content.ReadAsStringAsync(), out var configurationId); + return configurationId; + } + + /// + /// Tries to read the enterprise environment for the current user's configuration server URL. + /// + /// + /// Returns null when the environment is not set or the request fails. + /// Otherwise, the configuration server URL. + /// + public async Task EnterpriseEnvConfigServerUrl() + { + var result = await this.http.GetAsync("/system/enterprise/config/server"); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to query the enterprise configuration server URL: '{result.StatusCode}'"); + return null; + } + + var serverUrl = await result.Content.ReadAsStringAsync(); + return string.IsNullOrWhiteSpace(serverUrl) ? null : serverUrl; + } +} \ No newline at end of file diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index e6db9da3..c6375c26 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -2632,6 +2632,7 @@ dependencies = [ "base64 0.22.1", "calamine", "cbc", + "cfg-if", "cipher", "crossbeam-channel", "file-format", @@ -2660,6 +2661,7 @@ dependencies = [ "tokio", "tokio-stream", "url", + "windows-registry 0.5.2", ] [[package]] @@ -3978,7 +3980,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-registry", + "windows-registry 0.4.0", ] [[package]] @@ -5949,15 +5951,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", - "windows-strings", + "windows-strings 0.3.1", "windows-targets 0.53.0", ] +[[package]] +name = "windows-registry" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings 0.4.2", +] + [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] @@ -5971,6 +5984,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.42.0" diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 1687f162..6bb18649 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -37,6 +37,7 @@ file-format = "0.27.0" calamine = "0.27.0" pdfium-render = "0.8.31" sys-locale = "0.3.2" +cfg-if = "1.0.0" # Fixes security vulnerability downstream, where the upstream is not fixed yet: url = "2.5" @@ -50,5 +51,8 @@ reqwest = { version = "0.12.15", features = ["native-tls-vendored"] } # Fixes security vulnerability downstream, where the upstream is not fixed yet: openssl = "0.10.72" +[target.'cfg(target_os = "windows")'.dependencies] +windows-registry = "0.5.2" + [features] custom-protocol = ["tauri/custom-protocol"] diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index af3435b1..dbcf7361 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -1,4 +1,6 @@ +use std::env; use std::sync::OnceLock; +use log::info; use rocket::get; use sys_locale::get_locale; use crate::api_token::APIToken; @@ -43,4 +45,98 @@ pub fn read_user_language(_token: APIToken) -> String { log::warn!("Could not determine the system language. Use default 'en-US'."); String::from("en-US") }) +} + +#[get("/system/enterprise/config/id")] +pub fn read_enterprise_env_config_id(_token: APIToken) -> Option { + // + // When we are on a Windows machine, we try to read the enterprise config from + // the Windows registry. In case we can't find the registry key, or we are on a + // macOS or Linux machine, we try to read the enterprise config from the + // environment variables. + // + // The registry key is: + // HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT + // + // In this registry key, we expect the following values: + // - config_id + // + // The environment variable is: + // MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID + // + get_enterprise_configuration( + "config_id", + "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID", + ) +} + +#[get("/system/enterprise/config/server")] +pub fn read_enterprise_env_config_server_url(_token: APIToken) -> Option { + // + // When we are on a Windows machine, we try to read the enterprise config from + // the Windows registry. In case we can't find the registry key, or we are on a + // macOS or Linux machine, we try to read the enterprise config from the + // environment variables. + // + // The registry key is: + // HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT + // + // In this registry key, we expect the following values: + // - config_server_url + // + // The environment variable is: + // MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL + // + get_enterprise_configuration( + "config_server_url", + "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL", + ) +} + +fn get_enterprise_configuration(reg_value: &str, env_name: &str) -> Option { + info!("Trying to read the enterprise environment for some predefined configuration."); + cfg_if::cfg_if! { + if #[cfg(target_os = "windows")] { + info!(r"Detected a Windows machine, trying to read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT' or the environment variables"); + use windows_registry::*; + let key_path = r"Software\github\MindWork AI Studio\Enterprise IT"; + let key = match CURRENT_USER.open(key_path) { + Ok(key) => key, + Err(_) => { + info!(r"Could not read the registry key HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT. Falling back to environment variables."); + return match env::var(env_name) { + Ok(val) => { + info!("Falling back to the environment variable '{}' was successful.", env_name); + Some(val) + }, + Err(_) => { + info!("Falling back to the environment variable '{}' was not successful. It appears that this is not an enterprise environment.", env_name); + None + }, + } + }, + }; + + match key.get_string(reg_value) { + Ok(val) => Some(val), + Err(_) => { + info!(r"We could read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT', but the value '{}' could not be read. Falling back to environment variables.", reg_value); + match env::var(env_name) { + Ok(val) => { + info!("Falling back to the environment variable '{}' was successful.", env_name); + Some(val) + }, + Err(_) => { + info!("Falling back to the environment variable '{}' was not successful. It appears that this is not an enterprise environment.", env_name); + None + } + } + }, + } + } else { + // In the case of macOS or Linux, we just read the environment variable: + info!(r"Detected a Unix machine, trying to read the environment variable '{}'.", env_name) + env::var(env_name).ok() + } + } } \ No newline at end of file diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 459fc936..a90b43f1 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -78,6 +78,8 @@ pub fn start_runtime_api() { crate::environment::get_data_directory, crate::environment::get_config_directory, crate::environment::read_user_language, + crate::environment::read_enterprise_env_config_id, + crate::environment::read_enterprise_env_config_server_url, crate::file_data::extract_data, crate::file_data::read_pdf, crate::log::get_log_paths, From ea3271bc0ce7f4276b63548c934a46779a0f054b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 31 May 2025 19:00:40 +0200 Subject: [PATCH 02/28] Fixed the Unix implementation --- runtime/src/environment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index dbcf7361..382f9e88 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -135,7 +135,7 @@ fn get_enterprise_configuration(reg_value: &str, env_name: &str) -> Option Date: Sat, 31 May 2025 19:07:44 +0200 Subject: [PATCH 03/28] Refactor enterprise config functions to return String instead of Option --- runtime/src/environment.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index 382f9e88..944f1009 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -48,7 +48,7 @@ pub fn read_user_language(_token: APIToken) -> String { } #[get("/system/enterprise/config/id")] -pub fn read_enterprise_env_config_id(_token: APIToken) -> Option { +pub fn read_enterprise_env_config_id(_token: APIToken) -> String { // // When we are on a Windows machine, we try to read the enterprise config from // the Windows registry. In case we can't find the registry key, or we are on a @@ -71,7 +71,7 @@ pub fn read_enterprise_env_config_id(_token: APIToken) -> Option { } #[get("/system/enterprise/config/server")] -pub fn read_enterprise_env_config_server_url(_token: APIToken) -> Option { +pub fn read_enterprise_env_config_server_url(_token: APIToken) -> String { // // When we are on a Windows machine, we try to read the enterprise config from // the Windows registry. In case we can't find the registry key, or we are on a @@ -93,11 +93,11 @@ pub fn read_enterprise_env_config_server_url(_token: APIToken) -> Option ) } -fn get_enterprise_configuration(reg_value: &str, env_name: &str) -> Option { +fn get_enterprise_configuration(_reg_value: &str, env_name: &str) -> String { info!("Trying to read the enterprise environment for some predefined configuration."); cfg_if::cfg_if! { if #[cfg(target_os = "windows")] { - info!(r"Detected a Windows machine, trying to read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT' or the environment variables"); + info!(r"Detected a Windows machine, trying to read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT' or environment variables."); use windows_registry::*; let key_path = r"Software\github\MindWork AI Studio\Enterprise IT"; let key = match CURRENT_USER.open(key_path) { @@ -107,28 +107,28 @@ fn get_enterprise_configuration(reg_value: &str, env_name: &str) -> Option { info!("Falling back to the environment variable '{}' was successful.", env_name); - Some(val) + val }, Err(_) => { info!("Falling back to the environment variable '{}' was not successful. It appears that this is not an enterprise environment.", env_name); - None + "".to_string() }, } }, }; - match key.get_string(reg_value) { - Ok(val) => Some(val), + match key.get_string(_reg_value) { + Ok(val) => val, Err(_) => { - info!(r"We could read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT', but the value '{}' could not be read. Falling back to environment variables.", reg_value); + info!(r"We could read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT', but the value '{}' could not be read. Falling back to environment variables.", _reg_value); match env::var(env_name) { Ok(val) => { info!("Falling back to the environment variable '{}' was successful.", env_name); - Some(val) + val }, Err(_) => { info!("Falling back to the environment variable '{}' was not successful. It appears that this is not an enterprise environment.", env_name); - None + "".to_string() } } }, @@ -136,7 +136,13 @@ fn get_enterprise_configuration(reg_value: &str, env_name: &str) -> Option val, + Err(_) => { + info!("The environment variable '{}' was not found. It appears that this is not an enterprise environment.", env_name); + "".to_string() + } + } } } } \ No newline at end of file From 86f52550e28e6d29d8c2d42870b5d5bdf2dc1685 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:43:59 +0200 Subject: [PATCH 04/28] Fixed config plugins action column --- app/MindWork AI Studio/Pages/Plugins.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Pages/Plugins.razor b/app/MindWork AI Studio/Pages/Plugins.razor index e64acfe0..b5a39ef4 100644 --- a/app/MindWork AI Studio/Pages/Plugins.razor +++ b/app/MindWork AI Studio/Pages/Plugins.razor @@ -63,7 +63,7 @@ - @if (!context.IsInternal) + @if (context is { IsInternal: false, Type: not PluginType.CONFIGURATION }) { var isEnabled = this.SettingsManager.IsPluginEnabled(context); From f4b203863cccd3bf87f9e23368386c75520d6462 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:45:10 +0200 Subject: [PATCH 05/28] Added delete endpoint for enterprise config ID --- runtime/src/environment.rs | 25 ++++++++++++++++++++++++- runtime/src/runtime_api.rs | 1 + 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index 944f1009..06c9447c 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -1,7 +1,7 @@ use std::env; use std::sync::OnceLock; use log::info; -use rocket::get; +use rocket::{delete, get}; use sys_locale::get_locale; use crate::api_token::APIToken; @@ -70,6 +70,29 @@ pub fn read_enterprise_env_config_id(_token: APIToken) -> String { ) } +#[delete("/system/enterprise/config/id")] +pub fn delete_enterprise_env_config_id(_token: APIToken) -> String { + // + // When we are on a Windows machine, we try to read the enterprise config from + // the Windows registry. In case we can't find the registry key, or we are on a + // macOS or Linux machine, we try to read the enterprise config from the + // environment variables. + // + // The registry key is: + // HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT + // + // In this registry key, we expect the following values: + // - delete_config_id + // + // The environment variable is: + // MINDWORK_AI_STUDIO_ENTERPRISE_DELETE_CONFIG_ID + // + get_enterprise_configuration( + "delete_config_id", + "MINDWORK_AI_STUDIO_ENTERPRISE_DELETE_CONFIG_ID", + ) +} + #[get("/system/enterprise/config/server")] pub fn read_enterprise_env_config_server_url(_token: APIToken) -> String { // diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index a90b43f1..eece5973 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -79,6 +79,7 @@ pub fn start_runtime_api() { crate::environment::get_config_directory, crate::environment::read_user_language, crate::environment::read_enterprise_env_config_id, + crate::environment::delete_enterprise_env_config_id, crate::environment::read_enterprise_env_config_server_url, crate::file_data::extract_data, crate::file_data::read_pdf, From ed006f3a4b12ef0f690cd66f6e210791f3364f66 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:46:33 +0200 Subject: [PATCH 06/28] Added SettingsLocker service and implementation --- app/MindWork AI Studio/Program.cs | 1 + .../Settings/SettingsLocker.cs | 78 +++++++++++++++++++ .../Tools/PluginSystem/PluginFactory.cs | 1 + 3 files changed, 80 insertions(+) create mode 100644 app/MindWork AI Studio/Settings/SettingsLocker.cs diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 81b25fa7..04247f56 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -126,6 +126,7 @@ public static async Task Main() builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); diff --git a/app/MindWork AI Studio/Settings/SettingsLocker.cs b/app/MindWork AI Studio/Settings/SettingsLocker.cs new file mode 100644 index 00000000..b23e1085 --- /dev/null +++ b/app/MindWork AI Studio/Settings/SettingsLocker.cs @@ -0,0 +1,78 @@ +using System.Linq.Expressions; + +namespace AIStudio.Settings; + +public sealed class SettingsLocker +{ + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); + private readonly Dictionary> lockedProperties = new(); + + public void Register(Expression> propertyExpression, Guid configurationPluginId) + { + var memberExpression = GetMemberExpression(propertyExpression); + var className = typeof(T).Name; + var propertyName = memberExpression.Member.Name; + + if (!this.lockedProperties.ContainsKey(className)) + this.lockedProperties[className] = []; + + this.lockedProperties[className].TryAdd(propertyName, configurationPluginId); + } + + public void Remove(Expression> propertyExpression) + { + var memberExpression = GetMemberExpression(propertyExpression); + var className = typeof(T).Name; + var propertyName = memberExpression.Member.Name; + + if (this.lockedProperties.TryGetValue(className, out var props)) + { + if (props.Remove(propertyName)) + { + // If the property was removed, check if the class has no more locked properties: + if (props.Count == 0) + this.lockedProperties.Remove(className); + } + } + } + + public Guid GetConfigurationPluginId(Expression> propertyExpression) + { + var memberExpression = GetMemberExpression(propertyExpression); + var className = typeof(T).Name; + var propertyName = memberExpression.Member.Name; + + if (this.lockedProperties.TryGetValue(className, out var props) && props.TryGetValue(propertyName, out var configurationPluginId)) + return configurationPluginId; + + // No configuration plugin ID found for this property: + return Guid.Empty; + } + + public bool IsLocked(Expression> propertyExpression) + { + var memberExpression = GetMemberExpression(propertyExpression); + var className = typeof(T).Name; + var propertyName = memberExpression.Member.Name; + + return this.lockedProperties.TryGetValue(className, out var props) && props.ContainsKey(propertyName); + } + + private static MemberExpression GetMemberExpression(Expression> expression) + { + switch (expression.Body) + { + // Case for value types, which are wrapped in UnaryExpression: + case UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression: + return (MemberExpression)unaryExpression.Operand; + + // Case for reference types, which are directly MemberExpressions: + case MemberExpression memberExpression: + return memberExpression; + + default: + LOGGER.LogError($"Expression '{expression}' is not a valid property expression."); + throw new ArgumentException($"Expression '{expression}' is not a valid property expression.", nameof(expression)); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index 8dc83966..f424a133 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -6,6 +6,7 @@ public static partial class PluginFactory { private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginFactory)); private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); + private static readonly SettingsLocker SETTINGS_LOCKER = Program.SERVICE_PROVIDER.GetRequiredService(); private static bool IS_INITIALIZED; private static string DATA_DIR = string.Empty; From 03bf288844541e3703c409b4cb84fe92e3a5120a Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:46:45 +0200 Subject: [PATCH 07/28] Refactor TemporaryChatService for cleaner logging and improvements --- .../Tools/Services/TemporaryChatService.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs b/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs index ea7a26ee..ab2f39e7 100644 --- a/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs +++ b/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs @@ -3,13 +3,11 @@ namespace AIStudio.Tools.Services; -public class TemporaryChatService(ILogger logger, SettingsManager settingsManager) : BackgroundService +public sealed class TemporaryChatService(ILogger logger, SettingsManager settingsManager) : BackgroundService { private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromDays(1); private static bool IS_INITIALIZED; - private readonly ILogger logger = logger; - #region Overrides of BackgroundService protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -17,12 +15,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED) await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken); - this.logger.LogInformation("The temporary chat maintenance service was initialized."); + logger.LogInformation("The temporary chat maintenance service was initialized."); await settingsManager.LoadSettings(); if(settingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy is WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE) { - this.logger.LogWarning("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service."); + logger.LogWarning("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service."); return; } @@ -38,11 +36,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) private Task StartMaintenance() { - this.logger.LogInformation("Starting maintenance of temporary chat storage."); + logger.LogInformation("Starting maintenance of temporary chat storage."); var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats"); if(!Directory.Exists(temporaryDirectories)) { - this.logger.LogWarning("Temporary chat storage directory does not exist. End maintenance."); + logger.LogWarning("Temporary chat storage directory does not exist. End maintenance."); return Task.CompletedTask; } @@ -67,12 +65,12 @@ private Task StartMaintenance() if(deleteChat) { - this.logger.LogInformation($"Deleting temporary chat storage directory '{tempChatDirPath}' due to maintenance policy."); + logger.LogInformation($"Deleting temporary chat storage directory '{tempChatDirPath}' due to maintenance policy."); Directory.Delete(tempChatDirPath, true); } } - this.logger.LogInformation("Finished maintenance of temporary chat storage."); + logger.LogInformation("Finished maintenance of temporary chat storage."); return Task.CompletedTask; } From 944f4a836ecc4abbf1421c5ffed1386db37d262e Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:47:05 +0200 Subject: [PATCH 08/28] Add method to remove enterprise config ID --- .../Tools/Services/RustService.Enterprise.cs | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs b/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs index 6fae98af..76931c0b 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs @@ -21,6 +21,30 @@ public async Task EnterpriseEnvConfigId() Guid.TryParse(await result.Content.ReadAsStringAsync(), out var configurationId); return configurationId; } + + /// + /// Tries to read the enterprise environment for a configuration ID, which must be removed. + /// + /// + /// Removing a configuration ID is necessary when the user moved to another department or + /// left the company, or when the configuration ID is no longer valid. + /// + /// + /// Returns the empty Guid when the environment is not set or the request fails. + /// Otherwise, the configuration ID. + /// + public async Task EnterpriseEnvRemoveConfigId() + { + var result = await this.http.DeleteAsync("/system/enterprise/config/id"); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to query the enterprise configuration ID for removal: '{result.StatusCode}'"); + return Guid.Empty; + } + + Guid.TryParse(await result.Content.ReadAsStringAsync(), out var configurationId); + return configurationId; + } /// /// Tries to read the enterprise environment for the current user's configuration server URL. @@ -29,16 +53,16 @@ public async Task EnterpriseEnvConfigId() /// Returns null when the environment is not set or the request fails. /// Otherwise, the configuration server URL. /// - public async Task EnterpriseEnvConfigServerUrl() + public async Task EnterpriseEnvConfigServerUrl() { var result = await this.http.GetAsync("/system/enterprise/config/server"); if (!result.IsSuccessStatusCode) { this.logger!.LogError($"Failed to query the enterprise configuration server URL: '{result.StatusCode}'"); - return null; + return string.Empty; } var serverUrl = await result.Content.ReadAsStringAsync(); - return string.IsNullOrWhiteSpace(serverUrl) ? null : serverUrl; + return string.IsNullOrWhiteSpace(serverUrl) ? string.Empty : serverUrl; } } \ No newline at end of file From 72ea91d5aab8264938cc986cc095cac36b609cff Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:48:49 +0200 Subject: [PATCH 09/28] Add EnterpriseEnvironmentService and plugin management --- app/MindWork AI Studio/Program.cs | 1 + .../PluginSystem/PluginFactory.Download.cs | 56 ++++++++++++++ .../PluginSystem/PluginFactory.Remove.cs | 54 +++++++++++++ .../Tools/PluginSystem/PluginFactory.cs | 2 + .../Services/EnterpriseEnvironmentService.cs | 75 +++++++++++++++++++ 5 files changed, 188 insertions(+) create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Remove.cs create mode 100644 app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 04247f56..fb7654ea 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -133,6 +133,7 @@ public static async Task Main() builder.Services.AddTransient(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); + builder.Services.AddHostedService(); builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddHubOptions(options => diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs new file mode 100644 index 00000000..30482b6e --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs @@ -0,0 +1,56 @@ +using System.IO.Compression; + +namespace AIStudio.Tools.PluginSystem; + +public static partial class PluginFactory +{ + public static async Task TryDownloadingConfigPluginAsync(Guid configPlugId, string configServerUrl, CancellationToken cancellationToken = default) + { + if (!IS_INITIALIZED) + return false; + + LOG.LogInformation($"Downloading configuration plugin with ID: {configPlugId} from server: {configServerUrl}"); + var tempDownloadFile = Path.GetTempFileName(); + try + { + using var httpClient = new HttpClient(); + var response = await httpClient.GetAsync($"{configServerUrl}/{configPlugId}.zip", cancellationToken); + if (response.IsSuccessStatusCode) + { + await using var tempFileStream = File.Create(tempDownloadFile); + await response.Content.CopyToAsync(tempFileStream, cancellationToken); + + var pluginDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString()); + if(Directory.Exists(pluginDirectory)) + Directory.Delete(pluginDirectory, true); + + Directory.CreateDirectory(pluginDirectory); + ZipFile.ExtractToDirectory(tempDownloadFile, pluginDirectory); + + LOG.LogInformation($"Configuration plugin with ID='{configPlugId}' downloaded and extracted successfully to '{pluginDirectory}'."); + } + else + LOG.LogError($"Failed to download the enterprise configuration plugin. HTTP Status: {response.StatusCode}"); + } + catch (Exception e) + { + LOG.LogError(e, "An error occurred while downloading or extracting the enterprise configuration plugin."); + } + finally + { + if (File.Exists(tempDownloadFile)) + { + try + { + File.Delete(tempDownloadFile); + } + catch (Exception e) + { + LOG.LogError(e, "Failed to delete the temporary download file."); + } + } + } + + return true; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Remove.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Remove.cs new file mode 100644 index 00000000..9fa82a66 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Remove.cs @@ -0,0 +1,54 @@ +namespace AIStudio.Tools.PluginSystem; + +public static partial class PluginFactory +{ + public static void RemovePluginAsync(Guid pluginId) + { + if (!IS_INITIALIZED) + return; + + LOG.LogWarning($"Try to remove plugin with ID: {pluginId}"); + + // + // Remove the plugin from the available plugins list: + // + var availablePluginToRemove = AVAILABLE_PLUGINS.FirstOrDefault(p => p.Id == pluginId); + if (availablePluginToRemove == null) + { + LOG.LogWarning($"No plugin found with ID: {pluginId}"); + return; + } + + AVAILABLE_PLUGINS.Remove(availablePluginToRemove); + + // + // Remove the plugin from the running plugins list: + // + var runningPluginToRemove = RUNNING_PLUGINS.FirstOrDefault(p => p.Id == pluginId); + if (runningPluginToRemove == null) + LOG.LogWarning($"No running plugin found with ID: {pluginId}"); + else + RUNNING_PLUGINS.Remove(runningPluginToRemove); + + // + // Delete the plugin directory: + // + var pluginDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, availablePluginToRemove.Id.ToString()); + if (Directory.Exists(pluginDirectory)) + { + try + { + Directory.Delete(pluginDirectory, true); + LOG.LogInformation($"Plugin directory '{pluginDirectory}' deleted successfully."); + } + catch (Exception ex) + { + LOG.LogError(ex, $"Failed to delete plugin directory '{pluginDirectory}'."); + } + } + else + LOG.LogWarning($"Plugin directory '{pluginDirectory}' does not exist."); + + LOG.LogInformation($"Plugin with ID: {pluginId} removed successfully."); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index f424a133..a5aaef37 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -12,6 +12,7 @@ public static partial class PluginFactory private static string DATA_DIR = string.Empty; private static string PLUGINS_ROOT = string.Empty; private static string INTERNAL_PLUGINS_ROOT = string.Empty; + private static string CONFIGURATION_PLUGINS_ROOT = string.Empty; private static FileSystemWatcher HOT_RELOAD_WATCHER = null!; private static ILanguagePlugin BASE_LANGUAGE_PLUGIN = NoPluginLanguage.INSTANCE; @@ -29,6 +30,7 @@ public static bool Setup() DATA_DIR = SettingsManager.DataDirectory!; PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins"); INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal"); + CONFIGURATION_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".config"); if (!Directory.Exists(PLUGINS_ROOT)) Directory.CreateDirectory(PLUGINS_ROOT); diff --git a/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs b/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs new file mode 100644 index 00000000..b0b480ec --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs @@ -0,0 +1,75 @@ +using AIStudio.Tools.PluginSystem; + +namespace AIStudio.Tools.Services; + +public sealed class EnterpriseEnvironmentService(ILogger logger, RustService rustService) : BackgroundService +{ + public static EnterpriseEnvironment CURRENT_ENVIRONMENT; + + private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromMinutes(16); + + #region Overrides of BackgroundService + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + logger.LogInformation("The enterprise environment service was initialized."); + + await this.StartUpdating(); + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(CHECK_INTERVAL, stoppingToken); + await this.StartUpdating(); + } + } + + #endregion + + private async Task StartUpdating() + { + try + { + logger.LogInformation("Starting update of the enterprise environment."); + + var enterpriseRemoveConfigId = await rustService.EnterpriseEnvRemoveConfigId(); + var isPlugin2RemoveInUse = PluginFactory.AvailablePlugins.Any(plugin => plugin.Id == enterpriseRemoveConfigId); + if (enterpriseRemoveConfigId != Guid.Empty && isPlugin2RemoveInUse) + { + logger.LogWarning($"The enterprise environment configuration ID '{enterpriseRemoveConfigId}' must be removed."); + PluginFactory.RemovePluginAsync(enterpriseRemoveConfigId); + } + + var enterpriseConfigServerUrl = await rustService.EnterpriseEnvConfigServerUrl(); + var enterpriseConfigId = await rustService.EnterpriseEnvConfigId(); + var nextEnterpriseEnvironment = new EnterpriseEnvironment(enterpriseConfigServerUrl, enterpriseConfigId); + if (CURRENT_ENVIRONMENT != nextEnterpriseEnvironment) + { + logger.LogInformation("The enterprise environment has changed. Updating the current environment."); + CURRENT_ENVIRONMENT = nextEnterpriseEnvironment; + + switch (enterpriseConfigServerUrl) + { + case null when enterpriseConfigId == Guid.Empty: + logger.LogInformation("AI Studio runs without an enterprise configuration."); + break; + + case null: + logger.LogWarning($"AI Studio runs with an enterprise configuration id ('{enterpriseConfigId}'), but the configuration server URL is not set."); + break; + + case not null when enterpriseConfigId == Guid.Empty: + logger.LogWarning($"AI Studio runs with an enterprise configuration server URL ('{enterpriseConfigServerUrl}'), but the configuration ID is not set."); + break; + + default: + logger.LogInformation($"AI Studio runs with an enterprise configuration id ('{enterpriseConfigId}') and configuration server URL ('{enterpriseConfigServerUrl}')."); + await PluginFactory.TryDownloadingConfigPluginAsync(enterpriseConfigId, enterpriseConfigServerUrl); + break; + } + } + } + catch (Exception e) + { + logger.LogError(e, "An error occurred while updating the enterprise environment."); + } + } +} \ No newline at end of file From 3f9726428e037bd60b58d0a0bb9300f0b0c98547 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:49:13 +0200 Subject: [PATCH 10/28] Disable update frequency when locked via SettingsLocker --- .../Components/Settings/SettingsPanelApp.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor index 9c42ccb0..1ad60ba2 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor @@ -13,7 +13,7 @@ - + From f1fcae3ab81ac5ab9f0a6770fe7a967046086ce0 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:49:24 +0200 Subject: [PATCH 11/28] Inject SettingsLocker into SettingsPanelBase --- .../Components/Settings/SettingsPanelBase.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs index bad3fca3..dded906c 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs @@ -15,4 +15,7 @@ public abstract class SettingsPanelBase : MSGComponentBase [Inject] protected RustService RustService { get; init; } = null!; + + [Inject] + protected SettingsLocker SettingsLocker { get; init; } = null!; } \ No newline at end of file From ab84cf1093e2dc653a27d0b448d62fee2761aac4 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:49:52 +0200 Subject: [PATCH 12/28] Add enterprise configuration handling in SettingsPanelProviders --- .../Settings/SettingsPanelProviders.razor | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor index 1ec52625..1389425a 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor @@ -45,15 +45,24 @@ - - - - - - - - - + @if (context.IsEnterpriseConfiguration) + { + + + + } + else + { + + + + + + + + + + } From 9404e83ec16d1abdd89860a36cc1bfb8ef152ded Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:50:23 +0200 Subject: [PATCH 13/28] Manage IsEnterpriseConfiguration for manual providers --- app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index 562e424e..78592b33 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -133,6 +133,7 @@ private AIStudio.Settings.Provider CreateProviderSettings() _ => this.DataModel }, IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED, + IsEnterpriseConfiguration = false, Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, Host = this.DataHost, HFInferenceProvider = this.HFInferenceProviderId, From c320d05f8f3de774dcdb335ce2ccdc0d07469335 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:50:34 +0200 Subject: [PATCH 14/28] Add enterprise environment details to About page --- app/MindWork AI Studio/Pages/About.razor | 4 +++- app/MindWork AI Studio/Pages/About.razor.cs | 24 ++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/MindWork AI Studio/Pages/About.razor b/app/MindWork AI Studio/Pages/About.razor index ff764e0c..b063672f 100644 --- a/app/MindWork AI Studio/Pages/About.razor +++ b/app/MindWork AI Studio/Pages/About.razor @@ -23,6 +23,7 @@ + @@ -106,7 +107,7 @@ - + @@ -116,6 +117,7 @@ + diff --git a/app/MindWork AI Studio/Pages/About.razor.cs b/app/MindWork AI Studio/Pages/About.razor.cs index 7a65d05f..d159ab53 100644 --- a/app/MindWork AI Studio/Pages/About.razor.cs +++ b/app/MindWork AI Studio/Pages/About.razor.cs @@ -3,6 +3,7 @@ using AIStudio.Components; using AIStudio.Dialogs; using AIStudio.Tools.Metadata; +using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Rust; using AIStudio.Tools.Services; @@ -30,7 +31,7 @@ public partial class About : MSGComponentBase private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute()!; private static readonly MetaDataLibrariesAttribute META_DATA_LIBRARIES = ASSEMBLY.GetCustomAttribute()!; - private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(About).Namespace, nameof(About)); + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(About).Namespace, nameof(About)); private string osLanguage = string.Empty; @@ -116,6 +117,27 @@ private async Task ShowPandocDialog() await this.DeterminePandocVersion(); } + private string GetEnterpriseEnvironment() + { + var configPlug = PluginFactory.AvailablePlugins.FirstOrDefault(x => x.Type is PluginType.CONFIGURATION); + var currentEnvironment = EnterpriseEnvironmentService.CURRENT_ENVIRONMENT; + + switch (currentEnvironment) + { + case { IsActive: false } when configPlug is null: + return T("AI Studio runs without an enterprise configuration."); + + case { IsActive: false }: + return string.Format(T("AI Studio runs with an enterprise configuration using the configuration plugin '{0}', without central configuration management."), configPlug.Id); + + case { IsActive: true } when configPlug is null: + return string.Format(T("AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is not yet available."), currentEnvironment.ConfigurationId, currentEnvironment.ConfigurationServerUrl); + + case { IsActive: true }: + return string.Format(T("AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is active."), currentEnvironment.ConfigurationId, currentEnvironment.ConfigurationServerUrl); + } + } + private async Task CopyStartupLogPath() { await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath); From fa48dd8383f929770fe891e7194526626cbef31b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:52:03 +0200 Subject: [PATCH 15/28] Added example config plugin --- .../Plugins/configuration/icon.lua | 1 + .../Plugins/configuration/plugin.lua | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 app/MindWork AI Studio/Plugins/configuration/icon.lua create mode 100644 app/MindWork AI Studio/Plugins/configuration/plugin.lua diff --git a/app/MindWork AI Studio/Plugins/configuration/icon.lua b/app/MindWork AI Studio/Plugins/configuration/icon.lua new file mode 100644 index 00000000..045bd983 --- /dev/null +++ b/app/MindWork AI Studio/Plugins/configuration/icon.lua @@ -0,0 +1 @@ +SVG = [[]] \ No newline at end of file diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua new file mode 100644 index 00000000..d80fc0d9 --- /dev/null +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -0,0 +1,67 @@ +require("icon") + +-- ------ +-- This is an example of a configuration plugin. Please replace +-- the placeholders and assign a valid ID. +-- ------ + +-- The ID for this plugin: +ID = "00000000-0000-0000-0000-000000000000" + +-- The icon for the plugin: +ICON_SVG = SVG + +-- The name of the plugin: +NAME = " - Configuration for " + +-- The description of the plugin: +DESCRIPTION = "This is a pre-defined configuration of " + +-- The version of the plugin: +VERSION = "1.0.0" + +-- The type of the plugin: +TYPE = "CONFIGURATION" + +-- The authors of the plugin: +AUTHORS = {""} + +-- The support contact for the plugin: +SUPPORT_CONTACT = "" + +-- The source URL for the plugin: +SOURCE_URL = "" + +-- The categories for the plugin: +CATEGORIES = { "CORE" } + +-- The target groups for the plugin: +TARGET_GROUPS = { "EVERYONE" } + +-- The flag for whether the plugin is maintained: +IS_MAINTAINED = true + +-- When the plugin is deprecated, this message will be shown to users: +DEPRECATION_MESSAGE = "" + +CONFIG = {} +CONFIG["LLM_PROVIDERS"] = {} + +-- An example of a configuration for a self-hosted ollama server: +CONFIG["LLM_PROVIDERS"][#CONFIG["LLM_PROVIDERS"]+1] = { + ["Id"] = "00000000-0000-0000-0000-000000000000", + ["InstanceName"] = "", + ["UsedLLMProvider"] = "SELF_HOSTED", + ["Host"] = "OLLAMA", + ["Hostname"] = "", + ["Model"] = { + ["Id"] = "", + ["DisplayName"] = "", + } +} + +CONFIG["SETTINGS"] = {} + +-- Configure the update behavior: +-- Allowed values are: NO_CHECK, ONCE_STARTUP, HOURLY, DAILY, WEEKLY +-- CONFIG["SETTINGS"]["DataApp.UpdateBehavior"] = "NO_CHECK" \ No newline at end of file From a44eaa407c6e635fad5f715f8c1f8e2741b245fb Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:52:17 +0200 Subject: [PATCH 16/28] Update IsPluginEnabled to include configuration plugins --- app/MindWork AI Studio/Settings/SettingsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 19423a6e..e6166196 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -159,7 +159,7 @@ public ConfidenceLevel GetMinimumConfidenceLevel(Tools.Components component) /// /// The plugin to check. /// True, when the plugin is enabled, false otherwise. - public bool IsPluginEnabled(IPluginMetadata plugin) => this.ConfigurationData.EnabledPlugins.Contains(plugin.Id); + public bool IsPluginEnabled(IPluginMetadata plugin) => plugin.Type is PluginType.CONFIGURATION || this.ConfigurationData.EnabledPlugins.Contains(plugin.Id); /// /// Returns the active language plugin. From 28484da11101ae72725cef073b9d1ae58aadad14 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:53:07 +0200 Subject: [PATCH 17/28] Added config plugin class --- .../Tools/PluginSystem/PluginConfiguration.cs | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs new file mode 100644 index 00000000..5ec96eda --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -0,0 +1,193 @@ +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +using Lua; + +using Host = AIStudio.Provider.SelfHosted.Host; +using Model = AIStudio.Provider.Model; + +namespace AIStudio.Tools.PluginSystem; + +public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginType type) : PluginBase(isInternal, state, type) +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfiguration).Namespace, nameof(PluginConfiguration)); + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); + private static readonly SettingsLocker SETTINGS_LOCKER = Program.SERVICE_PROVIDER.GetRequiredService(); + private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); + + public async Task InitializeAsync() + { + if(!this.TryProcessConfiguration(out var issue)) + this.pluginIssues.Add(issue); + + await SETTINGS_MANAGER.StoreSettings(); + await MessageBus.INSTANCE.SendMessage(null, Event.CONFIGURATION_CHANGED); + } + + /// + /// Tries to initialize the UI text content of the plugin. + /// + /// The error message, when the UI text content could not be read. + /// True, when the UI text content could be read successfully. + private bool TryProcessConfiguration(out string message) + { + // Ensure that the main CONFIG table exists and is a valid Lua table: + if (!this.state.Environment["CONFIG"].TryRead(out var mainTable)) + { + message = TB("The CONFIG table does not exist or is not a valid table."); + return false; + } + + // + // Configured settings + // + if (!mainTable.TryGetValue("SETTINGS", out var settingsValue) || !settingsValue.TryRead(out var settingsTable)) + { + message = TB("The SETTINGS table does not exist or is not a valid table."); + return false; + } + + if (settingsTable.TryGetValue("DataApp.UpdateBehavior", out var updateBehaviorValue) && updateBehaviorValue.TryRead(out var updateBehaviorText) && Enum.TryParse(updateBehaviorText, true, out var updateBehavior)) + { + SETTINGS_LOCKER.Register(x => x.UpdateBehavior, this.Id); + SETTINGS_MANAGER.ConfigurationData.App.UpdateBehavior = updateBehavior; + } + + // + // Configured providers + // + if (!mainTable.TryGetValue("LLM_PROVIDERS", out var providersValue) || !providersValue.TryRead(out var providersTable)) + { + message = TB("The LLM_PROVIDERS table does not exist or is not a valid table."); + return false; + } + + message = string.Empty; + var numberProviders = providersTable.ArrayLength; + var configuredProviders = new List(numberProviders); + for (var i = 1; i <= numberProviders; i++) + { + var providerLuaTableValue = providersTable[i]; + if (!providerLuaTableValue.TryRead(out var providerLuaTable)) + { + LOGGER.LogWarning($"The LLM_PROVIDERS table at index {i} is not a valid table."); + continue; + } + + if(this.TryReadProviderTable(i, providerLuaTable, out var provider)) + configuredProviders.Add(provider); + else + LOGGER.LogWarning($"The LLM_PROVIDERS table at index {i} does not contain a valid provider configuration."); + } + + // + // Apply the configured providers to the system settings: + // + #pragma warning disable MWAIS0001 + foreach (var configuredProvider in configuredProviders) + { + // The iterating variable is immutable, so we need to create a local copy: + var provider = configuredProvider; + + var providerIndex = SETTINGS_MANAGER.ConfigurationData.Providers.FindIndex(p => p.Id == provider.Id); + if (providerIndex > -1) + { + // Case: The provider already exists, we update it: + var existingProvider = SETTINGS_MANAGER.ConfigurationData.Providers[providerIndex]; + provider = provider with { Num = existingProvider.Num }; // Keep the original number + SETTINGS_MANAGER.ConfigurationData.Providers[providerIndex] = provider; + } + else + { + // Case: The provider does not exist, we add it: + provider = provider with { Num = SETTINGS_MANAGER.ConfigurationData.NextProviderNum++ }; + SETTINGS_MANAGER.ConfigurationData.Providers.Add(provider); + } + } + #pragma warning restore MWAIS0001 + + return true; + } + + private bool TryReadProviderTable(int idx, LuaTable table, out Settings.Provider provider) + { + provider = default; + if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var idText) || !Guid.TryParse(idText, out var id)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid ID. The ID must be a valid GUID."); + return false; + } + + if (!table.TryGetValue("InstanceName", out var instanceNameValue) || !instanceNameValue.TryRead(out var instanceName)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid instance name."); + return false; + } + + if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead(out var usedLLMProviderText) || !Enum.TryParse(usedLLMProviderText, true, out var usedLLMProvider)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid LLM provider enum value."); + return false; + } + + if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead(out var hostText) || !Enum.TryParse(hostText, true, out var host)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid host enum value."); + return false; + } + + if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead(out var hostname)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid hostname."); + return false; + } + + if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead(out var modelTable)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model table."); + return false; + } + + if (!this.TryReadModelTable(idx, modelTable, out var model)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model configuration."); + return false; + } + + provider = new() + { + Num = 0, + Id = id.ToString(), + InstanceName = instanceName, + UsedLLMProvider = usedLLMProvider, + Model = model, + IsSelfHosted = usedLLMProvider is LLMProviders.SELF_HOSTED, + IsEnterpriseConfiguration = true, + EnterpriseConfigurationPluginId = this.Id, + Hostname = hostname, + Host = host + }; + + return true; + } + + private bool TryReadModelTable(int idx, LuaTable table, out Model model) + { + model = default; + if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var id)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model ID."); + return false; + } + + if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead(out var displayName)) + { + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model display name."); + return false; + } + + model = new(id, displayName); + return true; + } +} \ No newline at end of file From d51093bc30894bf3678177f4f93433c27df70ac3 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:53:21 +0200 Subject: [PATCH 18/28] Remove enterprise environment configuration logging --- app/MindWork AI Studio/Program.cs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index fb7654ea..85667ac9 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -210,30 +210,6 @@ public static async Task Main() await rust.AppIsReady(); programLogger.LogInformation("The AI Studio server is ready."); - // - // Read the enterprise environment for the current user's configuration: - // - var enterpriseConfigServerUrl = await RUST_SERVICE.EnterpriseEnvConfigServerUrl(); - var enterpriseConfigId = await RUST_SERVICE.EnterpriseEnvConfigId(); - switch (enterpriseConfigServerUrl) - { - case null when enterpriseConfigId == Guid.Empty: - programLogger.LogInformation("AI Studio runs without an enterprise configuration."); - break; - - case null: - programLogger.LogWarning($"AI Studio runs with an enterprise configuration id ('{enterpriseConfigId}'), but the configuration server URL is not set."); - break; - - case not null when enterpriseConfigId == Guid.Empty: - programLogger.LogWarning($"AI Studio runs with an enterprise configuration server URL ('{enterpriseConfigServerUrl}'), but the configuration ID is not set."); - break; - - default: - programLogger.LogInformation($"AI Studio runs with an enterprise configuration id ('{enterpriseConfigId}') and configuration server URL ('{enterpriseConfigServerUrl}')."); - break; - } - TaskScheduler.UnobservedTaskException += (sender, taskArgs) => { programLogger.LogError(taskArgs.Exception, $"Unobserved task exception by sender '{sender ?? "n/a"}'."); From e1478d9364fee1c4504166fd5776f8cef1bc5389 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:53:30 +0200 Subject: [PATCH 19/28] Mark configuration plugins as partially implemented --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6383bba5..0bf61b9d 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,10 @@ Things we are currently working on: - [x] ~~Plan & implement the base plugin system ([PR #322](https://github.com/MindWorkAI/AI-Studio/pull/322))~~ - [x] ~~Start the plugin system ([PR #372](https://github.com/MindWorkAI/AI-Studio/pull/372))~~ - [x] ~~Added hot-reload support for plugins ([PR #377](https://github.com/MindWorkAI/AI-Studio/pull/377), [PR #391](https://github.com/MindWorkAI/AI-Studio/pull/391))~~ - - [x] Add support for other languages (I18N) to AI Studio (~~[PR #381](https://github.com/MindWorkAI/AI-Studio/pull/381), [PR #400](https://github.com/MindWorkAI/AI-Studio/pull/400), [PR #404](https://github.com/MindWorkAI/AI-Studio/pull/404), [PR #429](https://github.com/MindWorkAI/AI-Studio/pull/429), [PR #446](https://github.com/MindWorkAI/AI-Studio/pull/446), [PR #451](https://github.com/MindWorkAI/AI-Studio/pull/451), [PR #455](https://github.com/MindWorkAI/AI-Studio/pull/455), [PR #458](https://github.com/MindWorkAI/AI-Studio/pull/458), [PR #462](https://github.com/MindWorkAI/AI-Studio/pull/462), [PR #469](https://github.com/MindWorkAI/AI-Studio/pull/469), [PR #486](https://github.com/MindWorkAI/AI-Studio/pull/486))~~ + - [x] ~~Add support for other languages (I18N) to AI Studio ([PR #381](https://github.com/MindWorkAI/AI-Studio/pull/381), [PR #400](https://github.com/MindWorkAI/AI-Studio/pull/400), [PR #404](https://github.com/MindWorkAI/AI-Studio/pull/404), [PR #429](https://github.com/MindWorkAI/AI-Studio/pull/429), [PR #446](https://github.com/MindWorkAI/AI-Studio/pull/446), [PR #451](https://github.com/MindWorkAI/AI-Studio/pull/451), [PR #455](https://github.com/MindWorkAI/AI-Studio/pull/455), [PR #458](https://github.com/MindWorkAI/AI-Studio/pull/458), [PR #462](https://github.com/MindWorkAI/AI-Studio/pull/462), [PR #469](https://github.com/MindWorkAI/AI-Studio/pull/469), [PR #486](https://github.com/MindWorkAI/AI-Studio/pull/486))~~ - [x] ~~Add an I18N assistant to translate all AI Studio texts to a certain language & culture ([PR #422](https://github.com/MindWorkAI/AI-Studio/pull/422))~~ - - [x] Provide MindWork AI Studio in German (~~[PR #430](https://github.com/MindWorkAI/AI-Studio/pull/430), [PR #446](https://github.com/MindWorkAI/AI-Studio/pull/446), [PR #451](https://github.com/MindWorkAI/AI-Studio/pull/451), [PR #455](https://github.com/MindWorkAI/AI-Studio/pull/455), [PR #458](https://github.com/MindWorkAI/AI-Studio/pull/458), [PR #462](https://github.com/MindWorkAI/AI-Studio/pull/462), [PR #469](https://github.com/MindWorkAI/AI-Studio/pull/469), [PR #486](https://github.com/MindWorkAI/AI-Studio/pull/486)~~) - - [ ] Add configuration plugins, which allow pre-defining some LLM providers in organizations + - [x] ~~Provide MindWork AI Studio in German ([PR #430](https://github.com/MindWorkAI/AI-Studio/pull/430), [PR #446](https://github.com/MindWorkAI/AI-Studio/pull/446), [PR #451](https://github.com/MindWorkAI/AI-Studio/pull/451), [PR #455](https://github.com/MindWorkAI/AI-Studio/pull/455), [PR #458](https://github.com/MindWorkAI/AI-Studio/pull/458), [PR #462](https://github.com/MindWorkAI/AI-Studio/pull/462), [PR #469](https://github.com/MindWorkAI/AI-Studio/pull/469), [PR #486](https://github.com/MindWorkAI/AI-Studio/pull/486)~~) + - [ ] Add configuration plugins, which allow pre-defining some LLM providers in organizations (~~[PR #491](https://github.com/MindWorkAI/AI-Studio/pull/491)~~) - [ ] Add an app store for plugins, showcasing community-contributed plugins from public GitHub and GitLab repositories. This will enable AI Studio users to discover, install, and update plugins directly within the platform. - [ ] Add assistant plugins From 7903f13a93a8550a236a65c41e137b694b54da72 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:53:45 +0200 Subject: [PATCH 20/28] Add enterprise config plugin support in Provider --- app/MindWork AI Studio/Settings/Provider.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/MindWork AI Studio/Settings/Provider.cs b/app/MindWork AI Studio/Settings/Provider.cs index 33d39d3d..4cef58df 100644 --- a/app/MindWork AI Studio/Settings/Provider.cs +++ b/app/MindWork AI Studio/Settings/Provider.cs @@ -23,6 +23,8 @@ public readonly record struct Provider( LLMProviders UsedLLMProvider, Model Model, bool IsSelfHosted = false, + bool IsEnterpriseConfiguration = false, + Guid EnterpriseConfigurationPluginId = default, string Hostname = "http://localhost:1234", Host Host = Host.NONE, HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE) : ISecretId From 85fcb4a6c862165677982268f88180024ac0f701 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:54:25 +0200 Subject: [PATCH 21/28] Refactor hot reload system to improve event handling & monitor delete events as well --- .../PluginSystem/PluginFactory.HotReload.cs | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs index 4eb3b0c3..6cd8eb55 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs @@ -15,32 +15,11 @@ public static void SetUpHotReloading() LOG.LogInformation($"Start hot reloading plugins for path '{HOT_RELOAD_WATCHER.Path}'."); try { - var messageBus = Program.SERVICE_PROVIDER.GetRequiredService(); - HOT_RELOAD_WATCHER.IncludeSubdirectories = true; HOT_RELOAD_WATCHER.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName; HOT_RELOAD_WATCHER.Filter = "*.lua"; - HOT_RELOAD_WATCHER.Changed += async (_, args) => - { - var changeType = args.ChangeType.ToString().ToLowerInvariant(); - if (!await HOT_RELOAD_SEMAPHORE.WaitAsync(0)) - { - LOG.LogInformation($"File changed ({changeType}): {args.FullPath}. Already processing another change."); - return; - } - - try - { - LOG.LogInformation($"File changed ({changeType}): {args.FullPath}. Reloading plugins..."); - await LoadAll(); - await messageBus.SendMessage(null, Event.PLUGINS_RELOADED); - } - finally - { - HOT_RELOAD_SEMAPHORE.Release(); - } - }; - + HOT_RELOAD_WATCHER.Changed += HotReloadEventHandler; + HOT_RELOAD_WATCHER.Deleted += HotReloadEventHandler; HOT_RELOAD_WATCHER.EnableRaisingEvents = true; } catch (Exception e) @@ -52,4 +31,32 @@ public static void SetUpHotReloading() LOG.LogInformation("Hot reloading plugins set up."); } } + + private static async void HotReloadEventHandler(object _, FileSystemEventArgs args) + { + try + { + var changeType = args.ChangeType.ToString().ToLowerInvariant(); + if (!await HOT_RELOAD_SEMAPHORE.WaitAsync(0)) + { + LOG.LogInformation($"File changed ({changeType}): {args.FullPath}. Already processing another change."); + return; + } + + try + { + LOG.LogInformation($"File changed ({changeType}): {args.FullPath}. Reloading plugins..."); + await LoadAll(); + await MessageBus.INSTANCE.SendMessage(null, Event.PLUGINS_RELOADED); + } + finally + { + HOT_RELOAD_SEMAPHORE.Release(); + } + } + catch (Exception e) + { + LOG.LogError(e, $"Error while handling hot reload event for file '{args.FullPath}' with change type '{args.ChangeType}'."); + } + } } \ No newline at end of file From f1d0549a4fe3d02db1691c77c0cb72b79cfd5d60 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:54:47 +0200 Subject: [PATCH 22/28] Log warning on plugin load cancellation due to timeout --- .../Tools/PluginSystem/PluginFactory.Loading.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 288bb9fc..07e83799 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -58,8 +58,11 @@ public static async Task LoadAll(CancellationToken cancellationToken = default) try { if (cancellationToken.IsCancellationRequested) + { + LOG.LogWarning("Was not able to load all plugins, because the operation was cancelled. It seems to be a timeout."); break; - + } + LOG.LogInformation($"Try to load plugin: {pluginMainFile}"); var fileInfo = new FileInfo(pluginMainFile); string code; From e573b4d744377ff4069986ae3c4499ae28a6cd06 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:55:54 +0200 Subject: [PATCH 23/28] Remove unavailable enterprise configuration plugins and reset related settings --- .../PluginSystem/PluginFactory.Loading.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 07e83799..7edba32f 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -116,6 +116,61 @@ public static async Task LoadAll(CancellationToken cancellationToken = default) PLUGIN_LOAD_SEMAPHORE.Release(); LOG.LogInformation("Finished loading plugins."); } + + // + // Next, we have to clean up our settings. It is possible that a configuration plugin was removed. + // We have to remove the related settings as well: + // + var wasConfigurationChanged = false; + + // + // Check LLM providers: + // + #pragma warning disable MWAIS0001 + var configuredProviders = SETTINGS_MANAGER.ConfigurationData.Providers.ToList(); + foreach (var configuredProvider in configuredProviders) + { + if(!configuredProvider.IsEnterpriseConfiguration) + continue; + + var providerSourcePluginId = configuredProvider.EnterpriseConfigurationPluginId; + if(providerSourcePluginId == Guid.Empty) + continue; + + var providerSourcePlugin = AVAILABLE_PLUGINS.FirstOrDefault(plugin => plugin.Id == providerSourcePluginId); + if(providerSourcePlugin is null) + { + LOG.LogWarning($"The configured LLM provider '{configuredProvider.InstanceName}' (id={configuredProvider.Id}) is based on a plugin that is not available anymore. Removing the provider from the settings."); + SETTINGS_MANAGER.ConfigurationData.Providers.Remove(configuredProvider); + wasConfigurationChanged = true; + } + } + #pragma warning restore MWAIS0001 + + // + // Check all possible settings: + // + if (SETTINGS_LOCKER.GetConfigurationPluginId(x => x.UpdateBehavior) is var updateBehaviorPluginId && updateBehaviorPluginId != Guid.Empty) + { + var sourcePlugin = AVAILABLE_PLUGINS.FirstOrDefault(plugin => plugin.Id == updateBehaviorPluginId); + if (sourcePlugin is null) + { + // Remove the locked state: + SETTINGS_LOCKER.Remove(x => x.UpdateBehavior); + + // Reset the setting to the default value: + SETTINGS_MANAGER.ConfigurationData.App.UpdateBehavior = UpdateBehavior.HOURLY; + + LOG.LogWarning($"The configured update behavior is based on a plugin that is not available anymore. Resetting the setting to the default value: {SETTINGS_MANAGER.ConfigurationData.App.UpdateBehavior}."); + wasConfigurationChanged = true; + } + } + + if (wasConfigurationChanged) + { + await SETTINGS_MANAGER.StoreSettings(); + await MessageBus.INSTANCE.SendMessage(null, Event.CONFIGURATION_CHANGED); + } } public static async Task Load(string? pluginPath, string code, CancellationToken cancellationToken = default) From 00778752c60e772ab527f2721f1eefc874658ebb Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:56:25 +0200 Subject: [PATCH 24/28] Add support for configuration plugins in PluginFactory --- .../Tools/PluginSystem/PluginFactory.Loading.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 7edba32f..244e3984 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -1,5 +1,7 @@ using System.Text; +using AIStudio.Settings.DataModel; + using Lua; using Lua.Standard; @@ -216,11 +218,18 @@ public static async Task Load(string? pluginPath, string code, Cance return new NoPlugin($"TYPE is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues()}"); var isInternal = !string.IsNullOrWhiteSpace(pluginPath) && pluginPath.StartsWith(INTERNAL_PLUGINS_ROOT, StringComparison.OrdinalIgnoreCase); - return type switch + switch (type) { - PluginType.LANGUAGE => new PluginLanguage(isInternal, state, type), + case PluginType.LANGUAGE: + return new PluginLanguage(isInternal, state, type); + + case PluginType.CONFIGURATION: + var configPlug = new PluginConfiguration(isInternal, state, type); + await configPlug.InitializeAsync(); + return configPlug; - _ => new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.") - }; + default: + return new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio."); + } } } \ No newline at end of file From 83d7ab69171313263b8fa123667f3c2b2a8791db Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:57:31 +0200 Subject: [PATCH 25/28] Refactored startup code of the plugin factory & handle config plugins as well --- .../PluginSystem/PluginFactory.Starting.cs | 68 +++++++++++-------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs index 8fe1b9d8..983b84da 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs @@ -22,48 +22,59 @@ private static async Task RestartAllPlugins(CancellationToken cancellationToken var baseLanguagePluginId = InternalPlugin.LANGUAGE_EN_US.MetaData().Id; var baseLanguagePluginMetaData = AVAILABLE_PLUGINS.FirstOrDefault(p => p.Id == baseLanguagePluginId); if (baseLanguagePluginMetaData is null) - { LOG.LogError($"Was not able to find the base language plugin: Id='{baseLanguagePluginId}'. Please check your installation."); - return; - } - - var startedBasePlugin = await Start(baseLanguagePluginMetaData, cancellationToken); - if (startedBasePlugin is NoPlugin noPlugin) - { - LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {noPlugin.Issues.First()}"); - return; - } - - if (startedBasePlugin is PluginLanguage languagePlugin) - { - BASE_LANGUAGE_PLUGIN = languagePlugin; - RUNNING_PLUGINS.Add(languagePlugin); - LOG.LogInformation($"Successfully started the base language plugin: Id='{languagePlugin.Id}', Type='{languagePlugin.Type}', Name='{languagePlugin.Name}', Version='{languagePlugin.Version}'"); - } else { - LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {string.Join("; ", startedBasePlugin.Issues)}"); - return; + try + { + var startedBasePlugin = await Start(baseLanguagePluginMetaData, cancellationToken); + if (startedBasePlugin is NoPlugin noPlugin) + LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {noPlugin.Issues.First()}"); + + if (startedBasePlugin is PluginLanguage languagePlugin) + { + BASE_LANGUAGE_PLUGIN = languagePlugin; + RUNNING_PLUGINS.Add(languagePlugin); + LOG.LogInformation($"Successfully started the base language plugin: Id='{languagePlugin.Id}', Type='{languagePlugin.Type}', Name='{languagePlugin.Name}', Version='{languagePlugin.Version}'"); + } + else + LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {string.Join("; ", startedBasePlugin.Issues)}"); + } + catch (Exception e) + { + LOG.LogError(e, $"An error occurred while starting the base language plugin: Id='{baseLanguagePluginId}'."); + BASE_LANGUAGE_PLUGIN = NoPluginLanguage.INSTANCE; + } } - + // // Iterate over all available plugins and try to start them. // foreach (var availablePlugin in AVAILABLE_PLUGINS) { if(cancellationToken.IsCancellationRequested) + { + LOG.LogWarning("Cancellation requested while starting plugins. Stopping the plugin startup process. Probably due to a timeout."); break; - + } + if (availablePlugin.Id == baseLanguagePluginId) continue; - - if (availablePlugin.IsInternal || SETTINGS_MANAGER.IsPluginEnabled(availablePlugin)) - if(await Start(availablePlugin, cancellationToken) is { IsValid: true } plugin) - RUNNING_PLUGINS.Add(plugin); - // Inform all components that the plugins have been reloaded or started: - await MessageBus.INSTANCE.SendMessage(null, Event.PLUGINS_RELOADED); + try + { + if (availablePlugin.IsInternal || SETTINGS_MANAGER.IsPluginEnabled(availablePlugin) || availablePlugin.Type == PluginType.CONFIGURATION) + if(await Start(availablePlugin, cancellationToken) is { IsValid: true } plugin) + RUNNING_PLUGINS.Add(plugin); + } + catch (Exception e) + { + LOG.LogError(e, $"An error occurred while starting the plugin: Id='{availablePlugin.Id}', Type='{availablePlugin.Type}', Name='{availablePlugin.Name}', Version='{availablePlugin.Version}'."); + } } + + // Inform all components that the plugins have been reloaded or started: + await MessageBus.INSTANCE.SendMessage(null, Event.PLUGINS_RELOADED); } private static async Task Start(IAvailablePlugin meta, CancellationToken cancellationToken = default) @@ -91,6 +102,9 @@ private static async Task Start(IAvailablePlugin meta, CancellationT if (plugin is PluginLanguage languagePlugin && BASE_LANGUAGE_PLUGIN != NoPluginLanguage.INSTANCE) languagePlugin.SetBaseLanguage(BASE_LANGUAGE_PLUGIN); + if(plugin is PluginConfiguration configPlugin) + await configPlugin.InitializeAsync(); + LOG.LogInformation($"Successfully started plugin: Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}'"); return plugin; } From a8d96c9dd7b66f412b5f941e96d0aba367d79593 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 20:57:43 +0200 Subject: [PATCH 26/28] Add EnterpriseEnvironment record struct to manage configuration state --- app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs diff --git a/app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs b/app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs new file mode 100644 index 00000000..fd61b949 --- /dev/null +++ b/app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs @@ -0,0 +1,6 @@ +namespace AIStudio.Tools; + +public readonly record struct EnterpriseEnvironment(string ConfigurationServerUrl, Guid ConfigurationId) +{ + public bool IsActive => !string.IsNullOrEmpty(this.ConfigurationServerUrl) && this.ConfigurationId != Guid.Empty; +} \ No newline at end of file From 98aaea4604bc1066a55b2e2410df843580458a0e Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 21:09:39 +0200 Subject: [PATCH 27/28] Updated I18M --- .../Assistants/I18N/allTexts.lua | 33 +++++++++++++++++-- .../plugin.lua | 33 +++++++++++++++++-- .../plugin.lua | 33 +++++++++++++++++-- .../wwwroot/changelog/v0.9.46.md | 2 +- 4 files changed, 91 insertions(+), 10 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index a141a771..98ed0089 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1972,6 +1972,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T331371 -- Add LLM Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3346433704"] = "Add LLM Provider" +-- This provider is managed by your organization. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3415927576"] = "This provider is managed by your organization." + -- LLM Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3612415205"] = "LLM Provider" @@ -4153,12 +4156,21 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1020427799"] = "About MindWork AI Stud -- Browse AI Studio's source code on GitHub — we welcome your contributions. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1107156991"] = "Browse AI Studio's source code on GitHub — we welcome your contributions." +-- AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is not yet available. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1297057566"] = "AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is not yet available." + -- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat." -- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library." +-- AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is active. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1454889560"] = "AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is active." + +-- AI Studio runs with an enterprise configuration using the configuration plugin '{0}', without central configuration management. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1530477579"] = "AI Studio runs with an enterprise configuration using the configuration plugin '{0}', without central configuration management." + -- We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T162898512"] = "We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library." @@ -4183,6 +4195,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1890416390"] = "Check for updates" -- Vision UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1892426825"] = "Vision" +-- In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1915240766"] = "In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library." + -- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant." @@ -4195,15 +4210,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2173617769"] = "This library is used t -- For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2174764529"] = "For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose." +-- AI Studio runs without an enterprise configuration. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2244723851"] = "AI Studio runs without an enterprise configuration." + -- OK UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2246359087"] = "OK" -- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2273492381"] = "We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose." --- In order to use any LLM, each user must store their so-called token for each LLM provider. This token must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T228561878"] = "In order to use any LLM, each user must store their so-called token for each LLM provider. This token must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library." - -- The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2329884315"] = "The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK." @@ -4291,6 +4306,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3722989559"] = "This library is used t -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3813932670"] = "this version does not met the requirements" +-- This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3874337003"] = "This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration." + -- Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3908558992"] = "Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json." @@ -5359,6 +5377,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T90450 -- Content Creation UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T914642375"] = "Content Creation" +-- The SETTINGS table does not exist or is not a valid table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1148682011"] = "The SETTINGS table does not exist or is not a valid table." + +-- The CONFIG table does not exist or is not a valid table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3331620576"] = "The CONFIG table does not exist or is not a valid table." + +-- The LLM_PROVIDERS table does not exist or is not a valid table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T806592324"] = "The LLM_PROVIDERS table does not exist or is not a valid table." + -- The field IETF_TAG does not exist or is not a valid string. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "The field IETF_TAG does not exist or is not a valid string." diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 8572102a..905283cb 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -1974,6 +1974,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T331371 -- Add LLM Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3346433704"] = "LLM-Anbieter hinzufügen" +-- This provider is managed by your organization. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3415927576"] = "Dieser Anbieter wird von ihrer Organisation verwaltet." + -- LLM Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3612415205"] = "LLM-Anbieter" @@ -4155,12 +4158,21 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1020427799"] = "Über MindWork AI Stud -- Browse AI Studio's source code on GitHub — we welcome your contributions. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1107156991"] = "Sehen Sie sich den Quellcode von AI Studio auf GitHub an – wir freuen uns über ihre Beiträge." +-- AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is not yet available. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1297057566"] = "AI Studio läuft mit der Konfigurations-ID '{0}' ihrer Organisation und dem Konfigurationsserver '{1}'. Das Konfigurations-Plugin ist noch nicht verfügbar." + -- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1388816916"] = "Diese Bibliothek wird verwendet, um PDF-Dateien zu lesen. Das ist zum Beispiel notwendig, um PDFs als Datenquelle für einen Chat zu nutzen." -- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1421513382"] = "Diese Bibliothek wird verwendet, um die MudBlazor-Bibliothek zu erweitern. Sie stellt zusätzliche Komponenten bereit, die nicht Teil der MudBlazor-Bibliothek sind." +-- AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is active. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1454889560"] = "AI Studio läuft mit der Konfigurations-ID '{0}' ihrer Organisation und dem Konfigurationsserver '{1}'. Das Konfigurations-Plugin ist aktiv." + +-- AI Studio runs with an enterprise configuration using the configuration plugin '{0}', without central configuration management. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1530477579"] = "AI Studio läuft mit einer Unternehmenseinstellung und verwendet das Konfigurations-Plugin '{0}', jedoch ohne zentrale Konfigurationsverwaltung." + -- We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T162898512"] = "Wir verwenden Lua als Sprache für Plugins. Lua-CSharp ermöglicht die Kommunikation zwischen Lua-Skripten und AI Studio in beide Richtungen. Vielen Dank an Yusuke Nakada für diese großartige Bibliothek." @@ -4185,6 +4197,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1890416390"] = "Nach Updates suchen" -- Vision UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1892426825"] = "Vision" +-- In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1915240766"] = "Um ein beliebiges LLM nutzen zu können, muss jeder User seinen sogenannten API-Schlüssel für jeden LLM-Anbieter speichern. Dieser Schlüssel muss sicher aufbewahrt werden – ähnlich wie ein Passwort. Die sicherste Methode hierfür bieten Betriebssysteme wie macOS, Windows und Linux: Sie verfügen über Mechanismen, solche Daten – sofern vorhanden – auf spezieller Sicherheits-Hardware zu speichern. Da dies derzeit in .NET nicht möglich ist, verwenden wir diese Rust-Bibliothek." + -- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1924365263"] = "Diese Bibliothek wird verwendet, um HTML in Markdown umzuwandeln. Das ist zum Beispiel notwendig, wenn Sie eine URL als Eingabe für einen Assistenten angeben." @@ -4197,15 +4212,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2173617769"] = "Diese Bibliothek wird -- For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2174764529"] = "Für die sichere Kommunikation zwischen der Benutzeroberfläche und der Laufzeit müssen wir Zertifikate erstellen. Diese Rust-Bibliothek eignet sich hervorragend dafür." +-- AI Studio runs without an enterprise configuration. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2244723851"] = "AI Studio läuft ohne eine Konfiguration ihrer Organisation." + -- OK UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2246359087"] = "OK" -- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2273492381"] = "Wir müssen Zufallszahlen erzeugen, z. B. um die Kommunikation zwischen der Benutzeroberfläche und der Laufzeitumgebung abzusichern. Die rand-Bibliothek eignet sich dafür hervorragend." --- In order to use any LLM, each user must store their so-called token for each LLM provider. This token must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T228561878"] = "Um ein beliebiges LLM nutzen zu können, muss jeder Benutzer seinen sogenannten Token für jeden LLM-Anbieter speichern. Dieser Token muss sicher aufbewahrt werden, ähnlich wie ein Passwort. Am sichersten gelingt dies mit den Betriebssystemen wie macOS, Windows und Linux: Sie verfügen über Mechanismen, solche Daten – sofern vorhanden – auf spezieller Sicherheits-Hardware zu speichern. Da dies in .NET derzeit nicht möglich ist, verwenden wir diese Rust-Bibliothek." - -- The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2329884315"] = "Die Programmiersprache C# wird für die Umsetzung der Benutzeroberfläche und des Backends verwendet. Für die Entwicklung der Benutzeroberfläche mit C# kommt die Blazor-Technologie aus ASP.NET Core zum Einsatz. Alle diese Technologien sind im .NET SDK integriert." @@ -4293,6 +4308,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3722989559"] = "Diese Bibliothek wird -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3813932670"] = "diese Version erfüllt die Anforderungen nicht" +-- This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3874337003"] = "Diese Bibliothek wird verwendet, um auf die Windows-Registry zuzugreifen. Wir nutzen sie in Windows-Unternehmensumgebungen, um die gewünschte Konfiguration auszulesen." + -- Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3908558992"] = "Jetzt haben wir mehrere Systeme, einige entwickelt in .NET und andere in Rust. Das Datenformat JSON ist dafür zuständig, Daten zwischen beiden Welten zu übersetzen (dies nennt man Serialisierung und Deserialisierung von Daten). In der Rust-Welt übernimmt Serde diese Aufgabe. Das Pendant in der .NET-Welt ist ein fester Bestandteil von .NET und findet sich in System.Text.Json." @@ -5361,6 +5379,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T90450 -- Content Creation UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T914642375"] = "Inhalte erstellen" +-- The SETTINGS table does not exist or is not a valid table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1148682011"] = "Die Tabelle SETTINGS existiert nicht oder ist keine gültige Tabelle." + +-- The CONFIG table does not exist or is not a valid table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3331620576"] = "Die Tabelle CONFIG existiert nicht oder ist keine gültige Tabelle." + +-- The LLM_PROVIDERS table does not exist or is not a valid table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T806592324"] = "Die Tabelle LLM_PROVIDERS existiert nicht oder ist keine gültige Tabelle." + -- The field IETF_TAG does not exist or is not a valid string. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "Das Feld IETF_TAG existiert nicht oder ist keine gültige Zeichenkette." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 600bb74f..065c7e0e 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -1974,6 +1974,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T331371 -- Add LLM Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3346433704"] = "Add LLM Provider" +-- This provider is managed by your organization. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3415927576"] = "This provider is managed by your organization." + -- LLM Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3612415205"] = "LLM Provider" @@ -4155,12 +4158,21 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1020427799"] = "About MindWork AI Stud -- Browse AI Studio's source code on GitHub — we welcome your contributions. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1107156991"] = "Browse AI Studio's source code on GitHub — we welcome your contributions." +-- AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is not yet available. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1297057566"] = "AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is not yet available." + -- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat." -- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library." +-- AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is active. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1454889560"] = "AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is active." + +-- AI Studio runs with an enterprise configuration using the configuration plugin '{0}', without central configuration management. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1530477579"] = "AI Studio runs with an enterprise configuration using the configuration plugin '{0}', without central configuration management." + -- We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T162898512"] = "We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library." @@ -4185,6 +4197,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1890416390"] = "Check for updates" -- Vision UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1892426825"] = "Vision" +-- In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1915240766"] = "In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library." + -- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant." @@ -4197,15 +4212,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2173617769"] = "This library is used t -- For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2174764529"] = "For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose." +-- AI Studio runs without an enterprise configuration. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2244723851"] = "AI Studio runs without an enterprise configuration." + -- OK UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2246359087"] = "OK" -- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2273492381"] = "We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose." --- In order to use any LLM, each user must store their so-called token for each LLM provider. This token must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T228561878"] = "In order to use any LLM, each user must store their so-called token for each LLM provider. This token must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library." - -- The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2329884315"] = "The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK." @@ -4293,6 +4308,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3722989559"] = "This library is used t -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3813932670"] = "this version does not met the requirements" +-- This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3874337003"] = "This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration." + -- Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3908558992"] = "Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json." @@ -5361,6 +5379,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T90450 -- Content Creation UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T914642375"] = "Content Creation" +-- The SETTINGS table does not exist or is not a valid table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1148682011"] = "The SETTINGS table does not exist or is not a valid table." + +-- The CONFIG table does not exist or is not a valid table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3331620576"] = "The CONFIG table does not exist or is not a valid table." + +-- The LLM_PROVIDERS table does not exist or is not a valid table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T806592324"] = "The LLM_PROVIDERS table does not exist or is not a valid table." + -- The field IETF_TAG does not exist or is not a valid string. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "The field IETF_TAG does not exist or is not a valid string." diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.46.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.46.md index 46108869..9c688e51 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.46.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.46.md @@ -1,6 +1,6 @@ # v0.9.46, build 221 (2025-06-xx xx:xx UTC) - We just finished the first version of our plugin system. Right now, there are language plugins to help localize AI Studio. In the future, companies will be able to give their employees a predefined setup through a plugin. You’ll also be able to develop custom assistants as plugins. Languages and assistants will be available in public repositories, and AI Studio will have an app-store-like view for easy access. We’re proud to have set the foundation with this version. -- Completed the I18N system and made all 1,847 AI Studio text contents localizable. +- Completed the I18N system and made all 1,856 AI Studio text contents localizable. - AI Studio comes with two standard plugins: one for English (US) and one for German (Germany). When you start AI Studio, it tries to pick the language set on your operating system. If your language isn't supported yet, it uses English instead. - Added the ability to configure the maximum number of results returned per request for all data sources. Please note that this feature remains in preview and is not visible to all users. - Added the Pandoc integration, which enables us to use Pandoc for data processing (e.g., RAG) and for generating files (e.g., Office documents). We thank Nils `nilskruthoff` for the excellent contribution. \ No newline at end of file From 441cc0f72f793393d7c5c1fdfde4f3e34ea3147e Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 1 Jun 2025 21:13:03 +0200 Subject: [PATCH 28/28] Updated changelog --- app/MindWork AI Studio/wwwroot/changelog/v0.9.46.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.46.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.46.md index 9c688e51..3bee3c62 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.46.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.46.md @@ -1,5 +1,5 @@ # v0.9.46, build 221 (2025-06-xx xx:xx UTC) -- We just finished the first version of our plugin system. Right now, there are language plugins to help localize AI Studio. In the future, companies will be able to give their employees a predefined setup through a plugin. You’ll also be able to develop custom assistants as plugins. Languages and assistants will be available in public repositories, and AI Studio will have an app-store-like view for easy access. We’re proud to have set the foundation with this version. +- We just finished the first version of our plugin system. Right now, there are language plugins to help localize AI Studio and configuration plugins for enterprise environments. In the future, you’ll also be able to develop custom assistants as plugins. Languages and assistants will be available in public repositories, and AI Studio will have an app-store-like view for easy access. We’re proud to have set the foundation with this version. - Completed the I18N system and made all 1,856 AI Studio text contents localizable. - AI Studio comes with two standard plugins: one for English (US) and one for German (Germany). When you start AI Studio, it tries to pick the language set on your operating system. If your language isn't supported yet, it uses English instead. - Added the ability to configure the maximum number of results returned per request for all data sources. Please note that this feature remains in preview and is not visible to all users.