Add enterprise config support via env vars & Windows registry (#491)

This commit is contained in:
Thorsten Sommer 2025-06-01 21:14:21 +02:00 committed by GitHub
parent b88b78a08e
commit e8cfce05fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1061 additions and 98 deletions

View File

@ -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] ~~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] ~~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] ~~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] ~~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)~~) - [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 - [ ] 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 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 - [ ] Add assistant plugins

View File

@ -1972,6 +1972,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T331371
-- Add LLM Provider -- Add LLM Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3346433704"] = "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 -- LLM Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3612415205"] = "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. -- 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." 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. -- 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." 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. -- 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." 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. -- 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." 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 -- Vision
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1892426825"] = "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. -- 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." 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. -- 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." 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 -- OK
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2246359087"] = "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. -- 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." 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. -- 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." 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 -- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3813932670"] = "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. -- 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." 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 -- Content Creation
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T914642375"] = "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. -- 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." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "The field IETF_TAG does not exist or is not a valid string."

View File

@ -13,7 +13,7 @@
<ConfigurationSelect OptionDescription="@T("Color theme")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreferredTheme)" Data="@ConfigurationSelectDataFactory.GetThemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreferredTheme = selectedValue)" OptionHelp="@T("Choose the color theme that best suits for you.")"/> <ConfigurationSelect OptionDescription="@T("Color theme")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreferredTheme)" Data="@ConfigurationSelectDataFactory.GetThemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreferredTheme = selectedValue)" OptionHelp="@T("Choose the color theme that best suits for you.")"/>
<ConfigurationOption OptionDescription="@T("Save energy?")" LabelOn="@T("Energy saving is enabled")" LabelOff="@T("Energy saving is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.IsSavingEnergy)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.IsSavingEnergy = updatedState)" OptionHelp="@T("When enabled, streamed content from the AI is updated once every third second. When disabled, streamed content will be updated as soon as it is available.")"/> <ConfigurationOption OptionDescription="@T("Save energy?")" LabelOn="@T("Energy saving is enabled")" LabelOff="@T("Energy saving is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.IsSavingEnergy)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.IsSavingEnergy = updatedState)" OptionHelp="@T("When enabled, streamed content from the AI is updated once every third second. When disabled, streamed content will be updated as soon as it is available.")"/>
<ConfigurationOption OptionDescription="@T("Enable spellchecking?")" LabelOn="@T("Spellchecking is enabled")" LabelOff="@T("Spellchecking is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="@T("When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections.")"/> <ConfigurationOption OptionDescription="@T("Enable spellchecking?")" LabelOn="@T("Spellchecking is enabled")" LabelOff="@T("Spellchecking is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="@T("When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections.")"/>
<ConfigurationSelect OptionDescription="@T("Check for updates")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateBehavior)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateBehavior = selectedValue)" OptionHelp="@T("How often should we check for app updates?")"/> <ConfigurationSelect OptionDescription="@T("Check for updates")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateBehavior)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateBehavior = selectedValue)" OptionHelp="@T("How often should we check for app updates?")" Disabled="() => this.SettingsLocker.IsLocked<DataApp>(x => x.UpdateBehavior)"/>
<ConfigurationSelect OptionDescription="@T("Navigation bar behavior")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="@T("Select the desired behavior for the navigation bar.")"/> <ConfigurationSelect OptionDescription="@T("Navigation bar behavior")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="@T("Select the desired behavior for the navigation bar.")"/>
<ConfigurationSelect OptionDescription="@T("Preview feature visibility")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreviewVisibility)" Data="@ConfigurationSelectDataFactory.GetPreviewVisibility()" SelectionUpdate="@this.UpdatePreviewFeatures" OptionHelp="@T("Do you want to show preview features in the app?")"/> <ConfigurationSelect OptionDescription="@T("Preview feature visibility")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreviewVisibility)" Data="@ConfigurationSelectDataFactory.GetPreviewVisibility()" SelectionUpdate="@this.UpdatePreviewFeatures" OptionHelp="@T("Do you want to show preview features in the app?")"/>

View File

@ -15,4 +15,7 @@ public abstract class SettingsPanelBase : MSGComponentBase
[Inject] [Inject]
protected RustService RustService { get; init; } = null!; protected RustService RustService { get; init; } = null!;
[Inject]
protected SettingsLocker SettingsLocker { get; init; } = null!;
} }

View File

@ -45,6 +45,14 @@
</MudTd> </MudTd>
<MudTd> <MudTd>
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap"> <MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
@if (context.IsEnterpriseConfiguration)
{
<MudTooltip Text="@T("This provider is managed by your organization.")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
</MudTooltip>
}
else
{
<MudTooltip Text="@T("Open Dashboard")"> <MudTooltip Text="@T("Open Dashboard")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.OpenInBrowser" Href="@context.UsedLLMProvider.GetDashboardURL()" Target="_blank" Disabled="@(!context.UsedLLMProvider.HasDashboard())"/> <MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.OpenInBrowser" Href="@context.UsedLLMProvider.GetDashboardURL()" Target="_blank" Disabled="@(!context.UsedLLMProvider.HasDashboard())"/>
</MudTooltip> </MudTooltip>
@ -54,6 +62,7 @@
<MudTooltip Text="@T("Delete")"> <MudTooltip Text="@T("Delete")">
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteLLMProvider(context)"/> <MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteLLMProvider(context)"/>
</MudTooltip> </MudTooltip>
}
</MudStack> </MudStack>
</MudTd> </MudTd>
</RowTemplate> </RowTemplate>

View File

@ -133,6 +133,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
_ => this.DataModel _ => this.DataModel
}, },
IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED, IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED,
IsEnterpriseConfiguration = false,
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
Host = this.DataHost, Host = this.DataHost,
HFInferenceProvider = this.HFInferenceProviderId, HFInferenceProvider = this.HFInferenceProviderId,

View File

@ -23,6 +23,7 @@
<MudListItem T="string" Icon="@Icons.Material.Outlined.Widgets" Text="@MudBlazorVersion"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Widgets" Text="@MudBlazorVersion"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@TauriVersion"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@TauriVersion"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Translate" Text="@this.OSLanguage"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Translate" Text="@this.OSLanguage"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Business" Text="@this.GetEnterpriseEnvironment()"/>
</MudList> </MudList>
<MudStack Row="true"> <MudStack Row="true">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Update" OnClick="() => this.CheckForUpdate()"> <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Update" OnClick="() => this.CheckForUpdate()">
@ -106,7 +107,7 @@
<ThirdPartyComponent Name="Tauri" Developer="Daniel Thompson-Yvetot, Lucas Nogueira, Tensor, Boscop, Serge Zaitsev, George Burton & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tauri-apps/tauri/blob/dev/LICENSE_MIT" RepositoryUrl="https://github.com/tauri-apps/tauri" UseCase="@T("Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!")"/> <ThirdPartyComponent Name="Tauri" Developer="Daniel Thompson-Yvetot, Lucas Nogueira, Tensor, Boscop, Serge Zaitsev, George Burton & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tauri-apps/tauri/blob/dev/LICENSE_MIT" RepositoryUrl="https://github.com/tauri-apps/tauri" UseCase="@T("Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!")"/>
<ThirdPartyComponent Name="Rocket" Developer="Sergio Benitez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rwf2/Rocket/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rwf2/Rocket" UseCase="@T("We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust.")"/> <ThirdPartyComponent Name="Rocket" Developer="Sergio Benitez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rwf2/Rocket/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rwf2/Rocket" UseCase="@T("We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust.")"/>
<ThirdPartyComponent Name="serde" Developer="Erick Tryzelaar, David Tolnay & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/serde-rs/serde/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/serde-rs/serde" UseCase="@T("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.")"/> <ThirdPartyComponent Name="serde" Developer="Erick Tryzelaar, David Tolnay & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/serde-rs/serde/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/serde-rs/serde" UseCase="@T("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.")"/>
<ThirdPartyComponent Name="keyring" Developer="Walther Chen, Daniel Brotsky & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/hwchen/keyring-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/hwchen/keyring-rs" UseCase="@T("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.")"/> <ThirdPartyComponent Name="keyring" Developer="Walther Chen, Daniel Brotsky & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/hwchen/keyring-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/hwchen/keyring-rs" UseCase="@T("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.")"/>
<ThirdPartyComponent Name="arboard" Developer="Artur Kovacs, Avi Weinstock, 1Password & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/arboard/blob/master/LICENSE-MIT.txt" RepositoryUrl="https://github.com/1Password/arboard" UseCase="@T("To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system.")"/> <ThirdPartyComponent Name="arboard" Developer="Artur Kovacs, Avi Weinstock, 1Password & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/arboard/blob/master/LICENSE-MIT.txt" RepositoryUrl="https://github.com/1Password/arboard" UseCase="@T("To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system.")"/>
<ThirdPartyComponent Name="tokio" Developer="Alex Crichton, Carl Lerche, Alice Ryhl, Taiki Endo, Ivan Petkov, Eliza Weisman, Lucio Franco & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/tokio/blob/master/LICENSE" RepositoryUrl="https://github.com/tokio-rs/tokio" UseCase="@T("Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor.")"/> <ThirdPartyComponent Name="tokio" Developer="Alex Crichton, Carl Lerche, Alice Ryhl, Taiki Endo, Ivan Petkov, Eliza Weisman, Lucio Franco & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/tokio/blob/master/LICENSE" RepositoryUrl="https://github.com/tokio-rs/tokio" UseCase="@T("Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor.")"/>
<ThirdPartyComponent Name="futures" Developer="Alex Crichton, Taiki Endo, Taylor Cramer, Nemo157, Josef Brandl, Aaron Turon & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/futures-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/futures-rs" UseCase="@T("This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow.")"/> <ThirdPartyComponent Name="futures" Developer="Alex Crichton, Taiki Endo, Taylor Cramer, Nemo157, Josef Brandl, Aaron Turon & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/futures-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/futures-rs" UseCase="@T("This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow.")"/>
@ -116,6 +117,7 @@
<ThirdPartyComponent Name="base64" Developer="Marshall Pierce, Alice Maz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/marshallpierce/rust-base64/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/marshallpierce/rust-base64" UseCase="@T("For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.")"/> <ThirdPartyComponent Name="base64" Developer="Marshall Pierce, Alice Maz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/marshallpierce/rust-base64/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/marshallpierce/rust-base64" UseCase="@T("For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.")"/>
<ThirdPartyComponent Name="Rust Crypto" Developer="Artyom Pavlov, Tony Arcieri, Brian Warner, Arthur Gautier, Vlad Filippov, Friedel Ziegelmayer, Nicolas Stalder & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/RustCrypto/traits/blob/master/cipher/LICENSE-MIT" RepositoryUrl="https://github.com/RustCrypto" UseCase="@T("When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project.")"/> <ThirdPartyComponent Name="Rust Crypto" Developer="Artyom Pavlov, Tony Arcieri, Brian Warner, Arthur Gautier, Vlad Filippov, Friedel Ziegelmayer, Nicolas Stalder & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/RustCrypto/traits/blob/master/cipher/LICENSE-MIT" RepositoryUrl="https://github.com/RustCrypto" UseCase="@T("When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project.")"/>
<ThirdPartyComponent Name="rcgen" Developer="RustTLS developers, est31 & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rcgen/blob/main/LICENSE" RepositoryUrl="https://github.com/rustls/rcgen" UseCase="@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.")"/> <ThirdPartyComponent Name="rcgen" Developer="RustTLS developers, est31 & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rcgen/blob/main/LICENSE" RepositoryUrl="https://github.com/rustls/rcgen" UseCase="@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.")"/>
<ThirdPartyComponent Name="windows-registry" Developer="Microsoft, Kenny Kerr, Ryan Levick, Rafael Rivera, sivadeilra, Marijn Suijten & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/microsoft/windows-rs/blob/master/license-mit" RepositoryUrl="https://github.com/microsoft/windows-rs" UseCase="@T("This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration.")"/>
<ThirdPartyComponent Name="file-format" Developer="Mickaël Malécot & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mmalecot/file-format/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/mmalecot/file-format" UseCase="@T("This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file.")"/> <ThirdPartyComponent Name="file-format" Developer="Mickaël Malécot & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mmalecot/file-format/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/mmalecot/file-format" UseCase="@T("This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file.")"/>
<ThirdPartyComponent Name="calamine" Developer="Johann Tuffe, Joel Natividad, Eric Jolibois, Dmitriy & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tafia/calamine/blob/master/LICENSE-MIT.md" RepositoryUrl="https://github.com/tafia/calamine" UseCase="@T("This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.")"/> <ThirdPartyComponent Name="calamine" Developer="Johann Tuffe, Joel Natividad, Eric Jolibois, Dmitriy & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tafia/calamine/blob/master/LICENSE-MIT.md" RepositoryUrl="https://github.com/tafia/calamine" UseCase="@T("This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.")"/>
<ThirdPartyComponent Name="PDFium" Developer="Lei Zhang, Tom Sepez, Dan Sinclair, and Foxit, Google, Chromium, Collabora, Ada, DocsCorp, Dropbox, Microsoft, and PSPDFKit Teams & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://pdfium.googlesource.com/pdfium/+/refs/heads/main/LICENSE" RepositoryUrl="https://pdfium.googlesource.com/pdfium" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/> <ThirdPartyComponent Name="PDFium" Developer="Lei Zhang, Tom Sepez, Dan Sinclair, and Foxit, Google, Chromium, Collabora, Ada, DocsCorp, Dropbox, Microsoft, and PSPDFKit Teams & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://pdfium.googlesource.com/pdfium/+/refs/heads/main/LICENSE" RepositoryUrl="https://pdfium.googlesource.com/pdfium" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/>

View File

@ -3,6 +3,7 @@ using System.Reflection;
using AIStudio.Components; using AIStudio.Components;
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Tools.Metadata; using AIStudio.Tools.Metadata;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust; using AIStudio.Tools.Rust;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
@ -30,7 +31,7 @@ public partial class About : MSGComponentBase
private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute<MetaDataArchitectureAttribute>()!; private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute<MetaDataArchitectureAttribute>()!;
private static readonly MetaDataLibrariesAttribute META_DATA_LIBRARIES = ASSEMBLY.GetCustomAttribute<MetaDataLibrariesAttribute>()!; private static readonly MetaDataLibrariesAttribute META_DATA_LIBRARIES = ASSEMBLY.GetCustomAttribute<MetaDataLibrariesAttribute>()!;
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; private string osLanguage = string.Empty;
@ -116,6 +117,27 @@ public partial class About : MSGComponentBase
await this.DeterminePandocVersion(); 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() private async Task CopyStartupLogPath()
{ {
await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath); await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath);

View File

@ -63,7 +63,7 @@
</MudStack> </MudStack>
</MudTd> </MudTd>
<MudTd> <MudTd>
@if (!context.IsInternal) @if (context is { IsInternal: false, Type: not PluginType.CONFIGURATION })
{ {
var isEnabled = this.SettingsManager.IsPluginEnabled(context); var isEnabled = this.SettingsManager.IsPluginEnabled(context);
<MudTooltip Text="@(isEnabled ? T("Disable plugin") : T("Enable plugin"))"> <MudTooltip Text="@(isEnabled ? T("Disable plugin") : T("Enable plugin"))">

View File

@ -0,0 +1 @@
SVG = [[<svg enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><g><path d="M0,0h24v24H0V0z" fill="none"/><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></g></svg>]]

View File

@ -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 = "<Company Name> - Configuration for <Department Name>"
-- The description of the plugin:
DESCRIPTION = "This is a pre-defined configuration of <Company Name>"
-- The version of the plugin:
VERSION = "1.0.0"
-- The type of the plugin:
TYPE = "CONFIGURATION"
-- The authors of the plugin:
AUTHORS = {"<Company Name>"}
-- The support contact for the plugin:
SUPPORT_CONTACT = "<IT Department of Company Name>"
-- The source URL for the plugin:
SOURCE_URL = "<Any internal Git repository>"
-- 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"] = "<user-friendly name for the combination of server and model>",
["UsedLLMProvider"] = "SELF_HOSTED",
["Host"] = "OLLAMA",
["Hostname"] = "<https address of the ollama server>",
["Model"] = {
["Id"] = "<the ollama model ID>",
["DisplayName"] = "<user-friendly name of the model>",
}
}
CONFIG["SETTINGS"] = {}
-- Configure the update behavior:
-- Allowed values are: NO_CHECK, ONCE_STARTUP, HOURLY, DAILY, WEEKLY
-- CONFIG["SETTINGS"]["DataApp.UpdateBehavior"] = "NO_CHECK"

View File

@ -1974,6 +1974,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T331371
-- Add LLM Provider -- Add LLM Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3346433704"] = "LLM-Anbieter hinzufügen" 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 -- LLM Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3612415205"] = "LLM-Anbieter" 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. -- 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." 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. -- 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." 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. -- 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." 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. -- 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." 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 -- Vision
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1892426825"] = "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. -- 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." 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. -- 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." 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 -- OK
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2246359087"] = "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. -- 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." 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. -- 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." 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 -- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3813932670"] = "diese Version erfüllt die Anforderungen nicht" 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. -- 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." 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 -- Content Creation
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T914642375"] = "Inhalte erstellen" 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. -- 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." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "Das Feld IETF_TAG existiert nicht oder ist keine gültige Zeichenkette."

View File

@ -1974,6 +1974,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T331371
-- Add LLM Provider -- Add LLM Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3346433704"] = "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 -- LLM Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3612415205"] = "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. -- 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." 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. -- 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." 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. -- 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." 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. -- 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." 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 -- Vision
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1892426825"] = "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. -- 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." 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. -- 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." 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 -- OK
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2246359087"] = "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. -- 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." 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. -- 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." 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 -- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3813932670"] = "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. -- 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." 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 -- Content Creation
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T914642375"] = "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. -- 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." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "The field IETF_TAG does not exist or is not a valid string."

View File

@ -126,12 +126,14 @@ internal sealed class Program
builder.Services.AddSingleton<SettingsManager>(); builder.Services.AddSingleton<SettingsManager>();
builder.Services.AddSingleton<ThreadSafeRandom>(); builder.Services.AddSingleton<ThreadSafeRandom>();
builder.Services.AddSingleton<DataSourceService>(); builder.Services.AddSingleton<DataSourceService>();
builder.Services.AddSingleton<SettingsLocker>();
builder.Services.AddTransient<HTMLParser>(); builder.Services.AddTransient<HTMLParser>();
builder.Services.AddTransient<AgentDataSourceSelection>(); builder.Services.AddTransient<AgentDataSourceSelection>();
builder.Services.AddTransient<AgentRetrievalContextValidation>(); builder.Services.AddTransient<AgentRetrievalContextValidation>();
builder.Services.AddTransient<AgentTextContentCleaner>(); builder.Services.AddTransient<AgentTextContentCleaner>();
builder.Services.AddHostedService<UpdateService>(); builder.Services.AddHostedService<UpdateService>();
builder.Services.AddHostedService<TemporaryChatService>(); builder.Services.AddHostedService<TemporaryChatService>();
builder.Services.AddHostedService<EnterpriseEnvironmentService>();
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()
.AddInteractiveServerComponents() .AddInteractiveServerComponents()
.AddHubOptions(options => .AddHubOptions(options =>

View File

@ -23,6 +23,8 @@ public readonly record struct Provider(
LLMProviders UsedLLMProvider, LLMProviders UsedLLMProvider,
Model Model, Model Model,
bool IsSelfHosted = false, bool IsSelfHosted = false,
bool IsEnterpriseConfiguration = false,
Guid EnterpriseConfigurationPluginId = default,
string Hostname = "http://localhost:1234", string Hostname = "http://localhost:1234",
Host Host = Host.NONE, Host Host = Host.NONE,
HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE) : ISecretId HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE) : ISecretId

View File

@ -0,0 +1,78 @@
using System.Linq.Expressions;
namespace AIStudio.Settings;
public sealed class SettingsLocker
{
private static readonly ILogger<SettingsLocker> LOGGER = Program.LOGGER_FACTORY.CreateLogger<SettingsLocker>();
private readonly Dictionary<string, Dictionary<string, Guid>> lockedProperties = new();
public void Register<T>(Expression<Func<T, object>> 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<T>(Expression<Func<T, object>> 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<T>(Expression<Func<T, object>> 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<T>(Expression<Func<T, object>> 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<T>(Expression<Func<T, object>> 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));
}
}
}

View File

@ -159,7 +159,7 @@ public sealed class SettingsManager
/// </summary> /// </summary>
/// <param name="plugin">The plugin to check.</param> /// <param name="plugin">The plugin to check.</param>
/// <returns>True, when the plugin is enabled, false otherwise.</returns> /// <returns>True, when the plugin is enabled, false otherwise.</returns>
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);
/// <summary> /// <summary>
/// Returns the active language plugin. /// Returns the active language plugin.

View File

@ -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;
}

View File

@ -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<PluginConfiguration> LOGGER = Program.LOGGER_FACTORY.CreateLogger<PluginConfiguration>();
private static readonly SettingsLocker SETTINGS_LOCKER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsLocker>();
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
public async Task InitializeAsync()
{
if(!this.TryProcessConfiguration(out var issue))
this.pluginIssues.Add(issue);
await SETTINGS_MANAGER.StoreSettings();
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.CONFIGURATION_CHANGED);
}
/// <summary>
/// Tries to initialize the UI text content of the plugin.
/// </summary>
/// <param name="message">The error message, when the UI text content could not be read.</param>
/// <returns>True, when the UI text content could be read successfully.</returns>
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<LuaTable>(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<LuaTable>(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<string>(out var updateBehaviorText) && Enum.TryParse<UpdateBehavior>(updateBehaviorText, true, out var updateBehavior))
{
SETTINGS_LOCKER.Register<DataApp>(x => x.UpdateBehavior, this.Id);
SETTINGS_MANAGER.ConfigurationData.App.UpdateBehavior = updateBehavior;
}
//
// Configured providers
//
if (!mainTable.TryGetValue("LLM_PROVIDERS", out var providersValue) || !providersValue.TryRead<LuaTable>(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<Settings.Provider>(numberProviders);
for (var i = 1; i <= numberProviders; i++)
{
var providerLuaTableValue = providersTable[i];
if (!providerLuaTableValue.TryRead<LuaTable>(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<string>(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<string>(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<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(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<string>(out var hostText) || !Enum.TryParse<Host>(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<string>(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<LuaTable>(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<string>(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<string>(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;
}
}

View File

@ -0,0 +1,56 @@
using System.IO.Compression;
namespace AIStudio.Tools.PluginSystem;
public static partial class PluginFactory
{
public static async Task<bool> 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;
}
}

View File

@ -15,12 +15,26 @@ public static partial class PluginFactory
LOG.LogInformation($"Start hot reloading plugins for path '{HOT_RELOAD_WATCHER.Path}'."); LOG.LogInformation($"Start hot reloading plugins for path '{HOT_RELOAD_WATCHER.Path}'.");
try try
{ {
var messageBus = Program.SERVICE_PROVIDER.GetRequiredService<MessageBus>();
HOT_RELOAD_WATCHER.IncludeSubdirectories = true; HOT_RELOAD_WATCHER.IncludeSubdirectories = true;
HOT_RELOAD_WATCHER.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName; HOT_RELOAD_WATCHER.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;
HOT_RELOAD_WATCHER.Filter = "*.lua"; HOT_RELOAD_WATCHER.Filter = "*.lua";
HOT_RELOAD_WATCHER.Changed += async (_, args) => HOT_RELOAD_WATCHER.Changed += HotReloadEventHandler;
HOT_RELOAD_WATCHER.Deleted += HotReloadEventHandler;
HOT_RELOAD_WATCHER.EnableRaisingEvents = true;
}
catch (Exception e)
{
LOG.LogError(e, "Error while setting up hot reloading.");
}
finally
{
LOG.LogInformation("Hot reloading plugins set up.");
}
}
private static async void HotReloadEventHandler(object _, FileSystemEventArgs args)
{
try
{ {
var changeType = args.ChangeType.ToString().ToLowerInvariant(); var changeType = args.ChangeType.ToString().ToLowerInvariant();
if (!await HOT_RELOAD_SEMAPHORE.WaitAsync(0)) if (!await HOT_RELOAD_SEMAPHORE.WaitAsync(0))
@ -33,23 +47,16 @@ public static partial class PluginFactory
{ {
LOG.LogInformation($"File changed ({changeType}): {args.FullPath}. Reloading plugins..."); LOG.LogInformation($"File changed ({changeType}): {args.FullPath}. Reloading plugins...");
await LoadAll(); await LoadAll();
await messageBus.SendMessage<bool>(null, Event.PLUGINS_RELOADED); await MessageBus.INSTANCE.SendMessage<bool>(null, Event.PLUGINS_RELOADED);
} }
finally finally
{ {
HOT_RELOAD_SEMAPHORE.Release(); HOT_RELOAD_SEMAPHORE.Release();
} }
};
HOT_RELOAD_WATCHER.EnableRaisingEvents = true;
} }
catch (Exception e) catch (Exception e)
{ {
LOG.LogError(e, "Error while setting up hot reloading."); LOG.LogError(e, $"Error while handling hot reload event for file '{args.FullPath}' with change type '{args.ChangeType}'.");
}
finally
{
LOG.LogInformation("Hot reloading plugins set up.");
} }
} }
} }

View File

@ -1,5 +1,7 @@
using System.Text; using System.Text;
using AIStudio.Settings.DataModel;
using Lua; using Lua;
using Lua.Standard; using Lua.Standard;
@ -58,7 +60,10 @@ public static partial class PluginFactory
try try
{ {
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
{
LOG.LogWarning("Was not able to load all plugins, because the operation was cancelled. It seems to be a timeout.");
break; break;
}
LOG.LogInformation($"Try to load plugin: {pluginMainFile}"); LOG.LogInformation($"Try to load plugin: {pluginMainFile}");
var fileInfo = new FileInfo(pluginMainFile); var fileInfo = new FileInfo(pluginMainFile);
@ -113,6 +118,61 @@ public static partial class PluginFactory
PLUGIN_LOAD_SEMAPHORE.Release(); PLUGIN_LOAD_SEMAPHORE.Release();
LOG.LogInformation("Finished loading plugins."); 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<DataApp>(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<DataApp>(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<bool>(null, Event.CONFIGURATION_CHANGED);
}
} }
public static async Task<PluginBase> Load(string? pluginPath, string code, CancellationToken cancellationToken = default) public static async Task<PluginBase> 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<PluginType>()}"); return new NoPlugin($"TYPE is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues<PluginType>()}");
var isInternal = !string.IsNullOrWhiteSpace(pluginPath) && pluginPath.StartsWith(INTERNAL_PLUGINS_ROOT, StringComparison.OrdinalIgnoreCase); 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.");
}
} }
} }

View File

@ -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.");
}
}

View File

@ -22,17 +22,14 @@ public static partial class PluginFactory
var baseLanguagePluginId = InternalPlugin.LANGUAGE_EN_US.MetaData().Id; var baseLanguagePluginId = InternalPlugin.LANGUAGE_EN_US.MetaData().Id;
var baseLanguagePluginMetaData = AVAILABLE_PLUGINS.FirstOrDefault(p => p.Id == baseLanguagePluginId); var baseLanguagePluginMetaData = AVAILABLE_PLUGINS.FirstOrDefault(p => p.Id == baseLanguagePluginId);
if (baseLanguagePluginMetaData is null) if (baseLanguagePluginMetaData is null)
{
LOG.LogError($"Was not able to find the base language plugin: Id='{baseLanguagePluginId}'. Please check your installation."); LOG.LogError($"Was not able to find the base language plugin: Id='{baseLanguagePluginId}'. Please check your installation.");
return; else
} {
try
{
var startedBasePlugin = await Start(baseLanguagePluginMetaData, cancellationToken); var startedBasePlugin = await Start(baseLanguagePluginMetaData, cancellationToken);
if (startedBasePlugin is NoPlugin noPlugin) if (startedBasePlugin is NoPlugin noPlugin)
{
LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {noPlugin.Issues.First()}"); LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {noPlugin.Issues.First()}");
return;
}
if (startedBasePlugin is PluginLanguage languagePlugin) if (startedBasePlugin is PluginLanguage languagePlugin)
{ {
@ -41,9 +38,13 @@ public static partial class PluginFactory
LOG.LogInformation($"Successfully started the base language plugin: Id='{languagePlugin.Id}', Type='{languagePlugin.Type}', Name='{languagePlugin.Name}', Version='{languagePlugin.Version}'"); LOG.LogInformation($"Successfully started the base language plugin: Id='{languagePlugin.Id}', Type='{languagePlugin.Type}', Name='{languagePlugin.Name}', Version='{languagePlugin.Version}'");
} }
else else
{
LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {string.Join("; ", startedBasePlugin.Issues)}"); LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {string.Join("; ", startedBasePlugin.Issues)}");
return; }
catch (Exception e)
{
LOG.LogError(e, $"An error occurred while starting the base language plugin: Id='{baseLanguagePluginId}'.");
BASE_LANGUAGE_PLUGIN = NoPluginLanguage.INSTANCE;
}
} }
// //
@ -52,19 +53,29 @@ public static partial class PluginFactory
foreach (var availablePlugin in AVAILABLE_PLUGINS) foreach (var availablePlugin in AVAILABLE_PLUGINS)
{ {
if(cancellationToken.IsCancellationRequested) if(cancellationToken.IsCancellationRequested)
{
LOG.LogWarning("Cancellation requested while starting plugins. Stopping the plugin startup process. Probably due to a timeout.");
break; break;
}
if (availablePlugin.Id == baseLanguagePluginId) if (availablePlugin.Id == baseLanguagePluginId)
continue; continue;
if (availablePlugin.IsInternal || SETTINGS_MANAGER.IsPluginEnabled(availablePlugin)) try
{
if (availablePlugin.IsInternal || SETTINGS_MANAGER.IsPluginEnabled(availablePlugin) || availablePlugin.Type == PluginType.CONFIGURATION)
if(await Start(availablePlugin, cancellationToken) is { IsValid: true } plugin) if(await Start(availablePlugin, cancellationToken) is { IsValid: true } plugin)
RUNNING_PLUGINS.Add(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: // Inform all components that the plugins have been reloaded or started:
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.PLUGINS_RELOADED); await MessageBus.INSTANCE.SendMessage<bool>(null, Event.PLUGINS_RELOADED);
} }
}
private static async Task<PluginBase> Start(IAvailablePlugin meta, CancellationToken cancellationToken = default) private static async Task<PluginBase> 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) if (plugin is PluginLanguage languagePlugin && BASE_LANGUAGE_PLUGIN != NoPluginLanguage.INSTANCE)
languagePlugin.SetBaseLanguage(BASE_LANGUAGE_PLUGIN); 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}'"); LOG.LogInformation($"Successfully started plugin: Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}'");
return plugin; return plugin;
} }

View File

@ -6,11 +6,13 @@ public static partial class PluginFactory
{ {
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginFactory)); private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginFactory));
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>(); private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
private static readonly SettingsLocker SETTINGS_LOCKER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsLocker>();
private static bool IS_INITIALIZED; private static bool IS_INITIALIZED;
private static string DATA_DIR = string.Empty; private static string DATA_DIR = string.Empty;
private static string PLUGINS_ROOT = string.Empty; private static string PLUGINS_ROOT = string.Empty;
private static string INTERNAL_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 FileSystemWatcher HOT_RELOAD_WATCHER = null!;
private static ILanguagePlugin BASE_LANGUAGE_PLUGIN = NoPluginLanguage.INSTANCE; private static ILanguagePlugin BASE_LANGUAGE_PLUGIN = NoPluginLanguage.INSTANCE;
@ -28,6 +30,7 @@ public static partial class PluginFactory
DATA_DIR = SettingsManager.DataDirectory!; DATA_DIR = SettingsManager.DataDirectory!;
PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins"); PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins");
INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal"); INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal");
CONFIGURATION_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".config");
if (!Directory.Exists(PLUGINS_ROOT)) if (!Directory.Exists(PLUGINS_ROOT))
Directory.CreateDirectory(PLUGINS_ROOT); Directory.CreateDirectory(PLUGINS_ROOT);

View File

@ -0,0 +1,75 @@
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Tools.Services;
public sealed class EnterpriseEnvironmentService(ILogger<TemporaryChatService> 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.");
}
}
}

View File

@ -0,0 +1,68 @@
namespace AIStudio.Tools.Services;
public sealed partial class RustService
{
/// <summary>
/// Tries to read the enterprise environment for the current user's configuration ID.
/// </summary>
/// <returns>
/// Returns the empty Guid when the environment is not set or the request fails.
/// Otherwise, the configuration ID.
/// </returns>
public async Task<Guid> 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;
}
/// <summary>
/// Tries to read the enterprise environment for a configuration ID, which must be removed.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>
/// Returns the empty Guid when the environment is not set or the request fails.
/// Otherwise, the configuration ID.
/// </returns>
public async Task<Guid> 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;
}
/// <summary>
/// Tries to read the enterprise environment for the current user's configuration server URL.
/// </summary>
/// <returns>
/// Returns null when the environment is not set or the request fails.
/// Otherwise, the configuration server URL.
/// </returns>
public async Task<string> 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;
}
}

View File

@ -3,13 +3,11 @@ using AIStudio.Settings.DataModel;
namespace AIStudio.Tools.Services; namespace AIStudio.Tools.Services;
public class TemporaryChatService(ILogger<TemporaryChatService> logger, SettingsManager settingsManager) : BackgroundService public sealed class TemporaryChatService(ILogger<TemporaryChatService> logger, SettingsManager settingsManager) : BackgroundService
{ {
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromDays(1); private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromDays(1);
private static bool IS_INITIALIZED; private static bool IS_INITIALIZED;
private readonly ILogger<TemporaryChatService> logger = logger;
#region Overrides of BackgroundService #region Overrides of BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@ -17,12 +15,12 @@ public class TemporaryChatService(ILogger<TemporaryChatService> logger, Settings
while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED) while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED)
await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken); 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(); await settingsManager.LoadSettings();
if(settingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy is WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE) 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; return;
} }
@ -38,11 +36,11 @@ public class TemporaryChatService(ILogger<TemporaryChatService> logger, Settings
private Task StartMaintenance() 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"); var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats");
if(!Directory.Exists(temporaryDirectories)) 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; return Task.CompletedTask;
} }
@ -67,12 +65,12 @@ public class TemporaryChatService(ILogger<TemporaryChatService> logger, Settings
if(deleteChat) 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); Directory.Delete(tempChatDirPath, true);
} }
} }
this.logger.LogInformation("Finished maintenance of temporary chat storage."); logger.LogInformation("Finished maintenance of temporary chat storage.");
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -1,6 +1,6 @@
# v0.9.46, build 221 (2025-06-xx xx:xx UTC) # 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. Youll 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. Were 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, youll 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. Were 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. - 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 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. - 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.

32
runtime/Cargo.lock generated
View File

@ -2632,6 +2632,7 @@ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"calamine", "calamine",
"cbc", "cbc",
"cfg-if",
"cipher", "cipher",
"crossbeam-channel", "crossbeam-channel",
"file-format", "file-format",
@ -2660,6 +2661,7 @@ dependencies = [
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"url", "url",
"windows-registry 0.5.2",
] ]
[[package]] [[package]]
@ -3978,7 +3980,7 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"windows-registry", "windows-registry 0.4.0",
] ]
[[package]] [[package]]
@ -5949,15 +5951,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [ dependencies = [
"windows-result", "windows-result",
"windows-strings", "windows-strings 0.3.1",
"windows-targets 0.53.0", "windows-targets 0.53.0",
] ]
[[package]] [[package]]
name = "windows-result" name = "windows-registry"
version = "0.3.2" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"windows-link", "windows-link",
] ]
@ -5971,6 +5984,15 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.42.0" version = "0.42.0"

View File

@ -37,6 +37,7 @@ file-format = "0.27.0"
calamine = "0.27.0" calamine = "0.27.0"
pdfium-render = "0.8.31" pdfium-render = "0.8.31"
sys-locale = "0.3.2" sys-locale = "0.3.2"
cfg-if = "1.0.0"
# Fixes security vulnerability downstream, where the upstream is not fixed yet: # Fixes security vulnerability downstream, where the upstream is not fixed yet:
url = "2.5" 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: # Fixes security vulnerability downstream, where the upstream is not fixed yet:
openssl = "0.10.72" openssl = "0.10.72"
[target.'cfg(target_os = "windows")'.dependencies]
windows-registry = "0.5.2"
[features] [features]
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]

View File

@ -1,5 +1,7 @@
use std::env;
use std::sync::OnceLock; use std::sync::OnceLock;
use rocket::get; use log::info;
use rocket::{delete, get};
use sys_locale::get_locale; use sys_locale::get_locale;
use crate::api_token::APIToken; use crate::api_token::APIToken;
@ -44,3 +46,126 @@ pub fn read_user_language(_token: APIToken) -> String {
String::from("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()
}
}
}
}
}

View File

@ -78,6 +78,9 @@ pub fn start_runtime_api() {
crate::environment::get_data_directory, crate::environment::get_data_directory,
crate::environment::get_config_directory, crate::environment::get_config_directory,
crate::environment::read_user_language, 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::extract_data,
crate::file_data::read_pdf, crate::file_data::read_pdf,
crate::log::get_log_paths, crate::log::get_log_paths,