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 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/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 @@ - + 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 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 + { + + + + + + + + + + } 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 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId _ => this.DataModel }, IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED, + IsEnterpriseConfiguration = false, Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, Host = this.DataHost, HFInferenceProvider = this.HFInferenceProviderId, 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 System.Reflection; 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 @@ public partial class About : MSGComponentBase 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); 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); 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 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/Program.cs b/app/MindWork AI Studio/Program.cs index 1630a7a7..85667ac9 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -126,12 +126,14 @@ internal sealed class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); 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/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 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/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 sealed class SettingsManager /// /// 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. 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 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 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.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 partial class PluginFactory 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 partial class PluginFactory 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 diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 288bb9fc..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; @@ -58,8 +60,11 @@ public static partial class PluginFactory 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; @@ -113,6 +118,61 @@ public static partial class PluginFactory 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) @@ -158,11 +218,18 @@ public static partial class PluginFactory 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); - _ => new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.") - }; + case PluginType.CONFIGURATION: + var configPlug = new PluginConfiguration(isInternal, state, type); + await configPlug.InitializeAsync(); + return configPlug; + + 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 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.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 @@ public static partial class PluginFactory 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 @@ public static partial class PluginFactory 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; } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index 8dc83966..a5aaef37 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -6,11 +6,13 @@ 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; 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; @@ -28,6 +30,7 @@ public static partial class PluginFactory 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 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..76931c0b --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs @@ -0,0 +1,68 @@ +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 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. + /// + /// + /// 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 string.Empty; + } + + var serverUrl = await result.Content.ReadAsStringAsync(); + return string.IsNullOrWhiteSpace(serverUrl) ? string.Empty : serverUrl; + } +} \ No newline at end of file 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 @@ using AIStudio.Settings.DataModel; 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 @@ public class TemporaryChatService(ILogger logger, Settings 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 @@ public class TemporaryChatService(ILogger logger, Settings 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 @@ public class TemporaryChatService(ILogger logger, Settings 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; } 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..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,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. +- 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. - 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 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-result" -version = "0.3.2" +name = "windows-registry" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +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..06c9447c 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -1,5 +1,7 @@ +use std::env; use std::sync::OnceLock; -use rocket::get; +use log::info; +use rocket::{delete, get}; use sys_locale::get_locale; use crate::api_token::APIToken; @@ -43,4 +45,127 @@ 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) -> 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: + // - config_id + // + // The environment variable is: + // MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID + // + get_enterprise_configuration( + "config_id", + "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID", + ) +} + +#[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 { + // + // 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) -> 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 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); + val + }, + Err(_) => { + info!("Falling back to the environment variable '{}' was not successful. It appears that this is not an enterprise environment.", env_name); + "".to_string() + }, + } + }, + }; + + 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); + match env::var(env_name) { + Ok(val) => { + info!("Falling back to the environment variable '{}' was successful.", env_name); + val + }, + Err(_) => { + info!("Falling back to the environment variable '{}' was not successful. It appears that this is not an enterprise environment.", env_name); + "".to_string() + } + } + }, + } + } 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); + match env::var(env_name) { + Ok(val) => 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 diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 459fc936..eece5973 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -78,6 +78,9 @@ 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::delete_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,