mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-18 16:38:00 +00:00
Fixed settings which configuration plugins could not manage (#1080)
This commit is contained in:
1 parent
055cce80a0
commit
8642ce2d81
8 files changed
+260
-17
No files matched your search
@@ -12,7 +12,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.")"/>
|
||||
<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.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.IsSavingEnergy, out var meta) && meta.IsLocked"/>
|
||||
<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("Show details when suspicious content was removed?")" LabelOn="@T("A dialog lists what was removed and explains the attack pattern")" LabelOff="@T("Only a short notification is shown")" State="@(() => this.SettingsManager.ConfigurationData.App.ShowPromptInjectionAlert)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.ShowPromptInjectionAlert = updatedState)" OptionHelp="@T("Shows a dialog listing the removed passages, together with an explanation and an external reference.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShowPromptInjectionAlert, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSlider T="int" OptionDescription="@T("Request timeout")" Min="@ExternalHttpClientTimeout.MIN_HTTP_CLIENT_TIMEOUT_SECONDS" Max="@ExternalHttpClientTimeout.MAX_HTTP_CLIENT_TIMEOUT_SECONDS" Step="60" Unit="@T("seconds")" Value="@(() => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds = updatedValue)" OptionHelp="@T("How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.HttpClientTimeoutSeconds, out var meta) && meta.IsLocked"/>
|
||||
|
||||
@@ -10,20 +10,20 @@
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<ConfigurationSelect OptionDescription="@T("Default target language")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesOptionalData()" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedTargetLanguage = value)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Default target language")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesOptionalData()" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedTargetLanguage = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.VisualBriefing, x => x.PreselectedTargetLanguage, out var meta) && meta.IsLocked"/>
|
||||
@if (this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedTargetLanguage is CommonLanguages.OTHER)
|
||||
{
|
||||
<ConfigurationText OptionDescription="@T("Default custom target language")" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedOtherLanguage)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedOtherLanguage = value)"/>
|
||||
<ConfigurationText OptionDescription="@T("Default custom target language")" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedOtherLanguage)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedOtherLanguage = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.VisualBriefing, x => x.PreselectedOtherLanguage, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
<ConfigurationSelect OptionDescription="@T("Default audience profile")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceProfile)" Data="@ConfigurationSelectDataFactory.GetSlideBuilderAudienceProfileData()" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceProfile = value)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Default audience age group")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceAgeGroup)" Data="@ConfigurationSelectDataFactory.GetSlideBuilderAudienceAgeGroupData()" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceAgeGroup = value)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Default audience organizational level")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceOrganizationalLevel)" Data="@ConfigurationSelectDataFactory.GetSlideBuilderAudienceOrganizationalLevelData()" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceOrganizationalLevel = value)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Default audience expertise")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceExpertise)" Data="@ConfigurationSelectDataFactory.GetSlideBuilderAudienceExpertiseData()" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceExpertise = value)"/>
|
||||
<ConfigurationOption OptionDescription="@T("Show source references by default?")" LabelOn="@T("Source references are visible")" LabelOff="@T("Source references are hidden")" State="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.ShowSourceReferences)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.ShowSourceReferences = value)"/>
|
||||
<ConfigurationOption OptionDescription="@T("Optimize large visual assets by default?")" LabelOn="@T("Large visual assets are optimized")" LabelOff="@T("Visual assets keep their original size")" State="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.OptimizeImages)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.OptimizeImages = value)"/>
|
||||
<ConfigurationMinConfidenceSelection RestrictToGlobalMinimumConfidence="true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence = value)"/>
|
||||
<ConfigurationProviderSelection Component="Components.VISUAL_BRIEFING_ASSISTANT" Data="@this.AvailableLLMProviders" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProvider)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProvider = value)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Default profile")" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProfile = value)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Default audience profile")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceProfile)" Data="@ConfigurationSelectDataFactory.GetSlideBuilderAudienceProfileData()" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceProfile = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.VisualBriefing, x => x.PreselectedAudienceProfile, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Default audience age group")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceAgeGroup)" Data="@ConfigurationSelectDataFactory.GetSlideBuilderAudienceAgeGroupData()" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceAgeGroup = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.VisualBriefing, x => x.PreselectedAudienceAgeGroup, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Default audience organizational level")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceOrganizationalLevel)" Data="@ConfigurationSelectDataFactory.GetSlideBuilderAudienceOrganizationalLevelData()" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceOrganizationalLevel = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.VisualBriefing, x => x.PreselectedAudienceOrganizationalLevel, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Default audience expertise")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceExpertise)" Data="@ConfigurationSelectDataFactory.GetSlideBuilderAudienceExpertiseData()" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedAudienceExpertise = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.VisualBriefing, x => x.PreselectedAudienceExpertise, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationOption OptionDescription="@T("Show source references by default?")" LabelOn="@T("Source references are visible")" LabelOff="@T("Source references are hidden")" State="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.ShowSourceReferences)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.ShowSourceReferences = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.VisualBriefing, x => x.ShowSourceReferences, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationOption OptionDescription="@T("Optimize large visual assets by default?")" LabelOn="@T("Large visual assets are optimized")" LabelOff="@T("Visual assets keep their original size")" State="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.OptimizeImages)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.OptimizeImages = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.VisualBriefing, x => x.OptimizeImages, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationMinConfidenceSelection RestrictToGlobalMinimumConfidence="true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.VisualBriefing, x => x.MinimumProviderConfidence, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationProviderSelection Component="Components.VISUAL_BRIEFING_ASSISTANT" Data="@this.AvailableLLMProviders" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProvider)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProvider = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.VisualBriefing, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Default profile")" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProfile = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.VisualBriefing, x => x.PreselectedProfile, out var meta) && meta.IsLocked"/>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -406,6 +406,13 @@ CONFIG["SETTINGS"] = {}
|
||||
-- but users can still choose another start page in the app settings.
|
||||
-- CONFIG["SETTINGS"]["DataApp.StartPage.AllowUserOverride"] = true
|
||||
|
||||
-- Configure whether AI Studio saves energy while the AI writes its answers.
|
||||
-- Allowed values are: true, false
|
||||
-- When set to true, streamed answers are updated once every third second instead of as soon
|
||||
-- as new text arrives. This helps on battery-powered devices and on virtual desktops, where
|
||||
-- every redraw of the screen is costly.
|
||||
-- CONFIG["SETTINGS"]["DataApp.IsSavingEnergy"] = true
|
||||
|
||||
-- Configure whether the quick start guide is shown on the welcome page.
|
||||
-- CONFIG["SETTINGS"]["DataApp.ShowQuickStartGuide"] = false
|
||||
|
||||
|
||||
@@ -14,6 +14,15 @@ public static partial class ManagedConfiguration
|
||||
|
||||
private static ILogger Log => Program.LOGGER_FACTORY.CreateLogger(nameof(ManagedConfiguration));
|
||||
|
||||
/// <summary>
|
||||
/// All registered settings, in no particular order.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The data classes register their settings when the settings data is created. Once it exists,
|
||||
/// this covers every managed setting, whatever its type.
|
||||
/// </remarks>
|
||||
internal static IEnumerable<ConfigMetaBase> RegisteredSettings => METADATA.Values.OfType<ConfigMetaBase>();
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the configuration metadata for a given configuration selection and
|
||||
/// property expression (enum-based).
|
||||
@@ -325,11 +334,8 @@ public static partial class ManagedConfiguration
|
||||
var wasChanged = false;
|
||||
var registeredSettingNames = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var config in METADATA.Values)
|
||||
foreach (var configMeta in RegisteredSettings)
|
||||
{
|
||||
if (config is not ConfigMetaBase configMeta)
|
||||
continue;
|
||||
|
||||
registeredSettingNames.Add(configMeta.SettingName);
|
||||
|
||||
//
|
||||
|
||||
@@ -37,7 +37,7 @@ Configuration plugins provide three kinds of values:
|
||||
- **Live plugin content:** complex Lua tables that implement `ILivePluginContent` and are read live from running plugins instead of being persisted to `ConfigurationData`. Examples include `MANDATORY_INFOS` and `INTRODUCTIONS`. If live plugin content creates persistent side data, add a dedicated cleanup path for that side data, like mandatory-info acceptances.
|
||||
|
||||
When adding configuration plugin capabilities:
|
||||
- For managed settings, update the corresponding data class in `app/MindWork AI Studio/Settings/DataModel/` to call `ManagedConfiguration.Register(...)` and process the setting in `PluginConfiguration.TryProcessConfiguration`. Cleaning up the setting when its configuration plugin was removed needs no extra step: `ManagedConfiguration.CleanupLeftOverManagedConfigurations(...)` iterates all registered settings. Do not add per-setting cleanup calls to `PluginFactory.Loading.LoadAll`.
|
||||
- For managed settings, update the corresponding data class in `app/MindWork AI Studio/Settings/DataModel/` to call `ManagedConfiguration.Register(...)` and process the setting in `PluginConfiguration.TryProcessConfiguration`. Cleaning up the setting when its configuration plugin was removed needs no extra step: `ManagedConfiguration.CleanupLeftOverManagedConfigurations(...)` iterates all registered settings. Do not add per-setting cleanup calls to `PluginFactory.Loading.LoadAll`. `ManagedSettingsCoverageTests` fails for a registered setting which no configuration plugin can manage; a setting which must stay out of their reach goes into its list `EXEMPT_SETTINGS`, together with the reason.
|
||||
- For managed configuration objects, add the type to `PluginConfigurationObjectType.cs`, persist the objects in the appropriate `ConfigurationData` collection, and add a descriptor to `PluginConfigurationObjectDescriptors` and its list `ALL`: its Lua table, its parser, the collection, the next number, and where secrets are kept. Parsing and cleanup need no extra call; `PluginConfigurationObjectDescriptorsTests` fails for a type without a descriptor.
|
||||
- For live plugin content, add a data type implementing `ILivePluginContent`, parse it in `PluginConfiguration`, expose it through `PluginFactory`, and add any required cleanup only for persistent side data.
|
||||
- Always document the new capability in `app/MindWork AI Studio/Plugins/configuration/plugin.lua`.
|
||||
|
||||
@@ -233,6 +233,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
// Config: what should be the start page?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.StartPage, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: save energy while the AI streams its answers?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.IsSavingEnergy, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: show prompt-injection alert dialogs?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowPromptInjectionAlert, this.Id, settingsTable, dryRun);
|
||||
|
||||
@@ -447,6 +450,19 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.MinimumProviderConfidence, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: Visual Briefing Assistant defaults?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.PreselectedProfile, Guid.Empty, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.PreselectedTargetLanguage, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.PreselectedOtherLanguage, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.PreselectedAudienceProfile, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.PreselectedAudienceAgeGroup, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.PreselectedAudienceOrganizationalLevel, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.PreselectedAudienceExpertise, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.ShowSourceReferences, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.OptimizeImages, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.MinimumProviderConfidence, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: transcription provider?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UseTranscriptionProvider, Guid.Empty, this.Id, settingsTable, dryRun);
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
- Improved the Batch Processing assistant, which now leaves out hidden files and folders as well as build output and downloaded dependencies, such as the folder `node_modules`. You can change both in the expert settings of the assistant.
|
||||
- Improved local data sources for folders and the Batch Processing assistant, which now leave out files larger than 1 GB, such as images of virtual machines. You can change this limit.
|
||||
- Improved the organization-wide management of the Batch Processing assistant. IT departments can now preset whether it includes hidden files and folders, which files and folders it leaves out, and how large a file may be, either fixed or as a default which users may change.
|
||||
- Improved the organization-wide management of the app settings. IT departments can now also set whether AI Studio saves energy while the AI writes its answers, which helps on virtual desktops, for example.
|
||||
- Improved attaching files to a chat. You can now attach any file that contains readable text, for example, program code or configuration files of a kind AI Studio did not know before.
|
||||
- Changed the banner on the start page to a fresh design that shows what AI Studio can do today.
|
||||
- Fixed a warning about the confidence level that stayed in the dialog for adding a local data source, even after you chose an embedding provider that meets it. The dialog now checks again whenever you change either of them, so a warning also appears when you switch to a provider that falls short.
|
||||
@@ -77,6 +78,7 @@
|
||||
- Fixed the security notice about removed instructions (prompt injection) appearing only minutes later while AI Studio updated a data source in the background. The notice also mixed your own attachments with files of the data source. Now each notice appears as soon as AI Studio has read the content, for example, the attachments of your message, and it names only that content.
|
||||
- Fixed the security notice about removed instructions (prompt injection) appearing several times for one answer when AI Studio checks which passages of your data sources fit your question: once for every passage it checked, and once more for all of them together. You now get one notice per answer, and a file counts once, however many of its passages were affected.
|
||||
- Fixed the example configuration for IT departments, which spelled llama.cpp in a way AI Studio does not accept. AI Studio ignored a self-hosted provider set up with that spelling. The example now also names only the self-hosted servers which can create embeddings or transcribe audio.
|
||||
- Fixed the configuration of your organization not applying its defaults for the Visual Briefing assistant, such as the provider, the language, or the audience. The settings of the assistant now also show which defaults your organization has locked.
|
||||
- Fixed exporting an LLM provider or an embedding provider with a tokenizer. On Windows, the export broke the configuration plugin, and on every system, the plugin ignored the tokenizer because the export named a folder on your computer. The export now names the place in your configuration plugin where the tokenizer file belongs.
|
||||
- Fixed building AI Studio from source on Windows, where the checks that run before every build failed because two of them expected the line breaks and file paths of macOS and Linux. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution.
|
||||
- Fixed a problem on Windows where AI Studio could fail to read the configuration of your organization when the name of an environment variable on your computer contained umlauts or other special characters.
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
using System.Collections;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace AIStudio.Tests.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that a configuration plugin can manage every setting registered as a managed one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A data class registers a managed setting with ManagedConfiguration.Register, and
|
||||
/// PluginConfiguration.TryProcessConfiguration reads it from a configuration plugin. Nothing ties the
|
||||
/// two together: a setting registered but never read can be locked in the user interface and appears
|
||||
/// in the documentation, yet no configuration plugin ever reaches it. The defaults of the Visual
|
||||
/// Briefing assistant stayed out of reach of every organization that way.<br/><br/>
|
||||
/// The test therefore configures every registered setting in one plugin and runs it for real. A dry
|
||||
/// run would not do: it reads the values but applies none of them, so it cannot tell which settings
|
||||
/// were read. The settings are reached through Program.SERVICE_PROVIDER, and the managed state of each
|
||||
/// setting is static, which is why this fixture does not run alongside others.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
[NonParallelizable]
|
||||
public sealed class ManagedSettingsCoverageTests
|
||||
{
|
||||
private static readonly Guid PLUGIN_ID = Guid.Parse("cd426cd3-bcff-4d6f-87bd-2c3c0756aa7d");
|
||||
|
||||
/// <summary>
|
||||
/// The value configured for every setting holding a string.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Free text accepts any value, and the settings which hold the ID of a provider or a profile
|
||||
/// accept nothing but a GUID, so a GUID suits them all.
|
||||
/// </remarks>
|
||||
private const string SAMPLE_TEXT = "e2f4bebe-1a3a-4f98-bbb9-e40e0b9daa14";
|
||||
|
||||
/// <summary>
|
||||
/// The registered settings this test does not expect a configuration plugin to manage, each with
|
||||
/// the reason.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A setting which must stay out of reach of configuration plugins belongs here as well. Neither
|
||||
/// kind of entry is a way to postpone reading a setting: a plugin which declares a setting that
|
||||
/// AI Studio silently ignores is worse than a setting no plugin can declare.
|
||||
/// </remarks>
|
||||
private static readonly IReadOnlyDictionary<string, string> EXEMPT_SETTINGS = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["DataAssistantPluginAudit.EnterpriseApprovedPlugins"] = "PluginConfiguration reads the approvals with a parser of its own, and only from a configuration plugin of an organization. Whether a plugin is one depends on the directory it lives in, which only the plugin factory sets up.",
|
||||
};
|
||||
|
||||
private RustService rustService = null!;
|
||||
private ServiceProvider serviceProvider = null!;
|
||||
private IServiceProvider previousServiceProvider = null!;
|
||||
|
||||
[SetUp]
|
||||
public void CreateSettings()
|
||||
{
|
||||
// Only builds its HTTP clients. Nothing connects, because the plugin of this test defines no provider:
|
||||
this.rustService = new RustService("1", "unused");
|
||||
|
||||
// Creating the settings data registers every managed setting:
|
||||
var settingsManager = new SettingsManager(NullLogger<SettingsManager>.Instance, this.rustService);
|
||||
|
||||
this.previousServiceProvider = Program.SERVICE_PROVIDER;
|
||||
this.serviceProvider = new ServiceCollection().AddSingleton(settingsManager).AddSingleton(this.rustService).BuildServiceProvider();
|
||||
Program.SERVICE_PROVIDER = this.serviceProvider;
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void RestoreApplicationState()
|
||||
{
|
||||
//
|
||||
// The managed state of a setting outlives this fixture, so we release everything the plugin
|
||||
// took over. The clean-up AI Studio runs after a plugin was removed does exactly that, as
|
||||
// long as it still reaches the settings of this test:
|
||||
//
|
||||
ManagedConfiguration.CleanupLeftOverManagedConfigurations([], new HashSet<Guid>());
|
||||
|
||||
Program.SERVICE_PROVIDER = this.previousServiceProvider;
|
||||
this.serviceProvider.Dispose();
|
||||
this.rustService.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task EveryRegisteredSettingCanBeManagedByAConfigurationPlugin()
|
||||
{
|
||||
using var plugin = await RunPluginWithEveryRegisteredSettingAsync();
|
||||
var registeredSettings = ManagedConfiguration.RegisteredSettings.ToList();
|
||||
var unmanagedSettingNames = registeredSettings
|
||||
.Where(setting => !EXEMPT_SETTINGS.ContainsKey(setting.SettingName) && !IsManagedByThePlugin(setting))
|
||||
.Select(setting => setting.SettingName)
|
||||
.Order(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(registeredSettings.Select(setting => setting.SettingName), Does.Contain("DataApp.UpdateInterval"), "No settings were registered, so this test would check nothing.");
|
||||
Assert.That(plugin.DeclaredSettingsCount, Is.EqualTo(registeredSettings.Count), "The plugin of this test does not declare every registered setting.");
|
||||
Assert.That(unmanagedSettingNames, Is.Empty, "These settings are registered with ManagedConfiguration.Register, but no configuration plugin can manage them. Read each one in PluginConfiguration.TryProcessConfiguration and document it in the example configuration plugin.lua, or add it to EXEMPT_SETTINGS with the reason it must stay out of reach of configuration plugins.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task EveryExemptSettingIsRegisteredAndStaysUnmanaged()
|
||||
{
|
||||
using var plugin = await RunPluginWithEveryRegisteredSettingAsync();
|
||||
var registeredSettings = ManagedConfiguration.RegisteredSettings.ToDictionary(setting => setting.SettingName, StringComparer.Ordinal);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var exemptSettingName in EXEMPT_SETTINGS.Keys)
|
||||
{
|
||||
Assert.That(registeredSettings.TryGetValue(exemptSettingName, out var exemptSetting), Is.True, $"The exempt setting '{exemptSettingName}' is not registered anymore. Remove it from EXEMPT_SETTINGS.");
|
||||
if (exemptSetting is not null)
|
||||
Assert.That(IsManagedByThePlugin(exemptSetting), Is.False, $"A configuration plugin can manage the exempt setting '{exemptSettingName}' by now. Remove it from EXEMPT_SETTINGS.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a configuration plugin which declares a valid value for every registered setting, and
|
||||
/// applies it the way AI Studio does when the plugin starts.
|
||||
/// </summary>
|
||||
/// <returns>The started configuration plugin.</returns>
|
||||
private static async Task<PluginConfiguration> RunPluginWithEveryRegisteredSettingAsync()
|
||||
{
|
||||
var code = new StringBuilder($$"""
|
||||
ID = "{{PLUGIN_ID}}"
|
||||
NAME = "Configuration for the tests"
|
||||
DESCRIPTION = "A configuration plugin which declares every managed setting."
|
||||
VERSION = "1.0.0"
|
||||
TYPE = "CONFIGURATION"
|
||||
AUTHORS = {"MindWork AI"}
|
||||
SUPPORT_CONTACT = "MindWork AI"
|
||||
SOURCE_URL = "https://github.com/MindWorkAI/AI-Studio"
|
||||
CATEGORIES = {"CORE"}
|
||||
TARGET_GROUPS = {"EVERYONE"}
|
||||
IS_MAINTAINED = true
|
||||
DEPRECATION_MESSAGE = ""
|
||||
CONFIG = {}
|
||||
CONFIG["SETTINGS"] = {}
|
||||
|
||||
""");
|
||||
|
||||
foreach (var setting in ManagedConfiguration.RegisteredSettings)
|
||||
code.AppendLine($"""CONFIG["SETTINGS"]["{setting.SettingName}"] = {ToLuaValue(setting)}""");
|
||||
|
||||
var plugin = await PluginFactory.Load(null, code.ToString());
|
||||
if (plugin is not PluginConfiguration { IsValid: true } configurationPlugin)
|
||||
{
|
||||
var issues = string.Join(" ", plugin.Issues);
|
||||
plugin.Dispose();
|
||||
throw new InvalidOperationException($"The code of this test did not produce a valid configuration plugin: {issues}");
|
||||
}
|
||||
|
||||
await configurationPlugin.InitializeAsync(false);
|
||||
return configurationPlugin;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a value which the setting accepts, in the Lua notation of a configuration plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Numbers take the default of the setting, because only the default is sure to pass the range
|
||||
/// a setting may check. Every collection accepts an empty table.
|
||||
/// </remarks>
|
||||
private static string ToLuaValue(ConfigMetaBase setting)
|
||||
{
|
||||
var valueType = GetValueType(setting);
|
||||
var defaultValue = setting.GetType().GetProperty(nameof(ConfigMeta<object, object>.Default))!.GetValue(setting);
|
||||
|
||||
if (valueType == typeof(bool))
|
||||
return "true";
|
||||
|
||||
if (valueType == typeof(string))
|
||||
return $"\"{SAMPLE_TEXT}\"";
|
||||
|
||||
if (valueType.IsEnum)
|
||||
return $"\"{Enum.GetName(valueType, defaultValue!)}\"";
|
||||
|
||||
if (valueType == typeof(int))
|
||||
return Convert.ToString(defaultValue, CultureInfo.InvariantCulture)!;
|
||||
|
||||
if (typeof(IEnumerable).IsAssignableFrom(valueType))
|
||||
return "{}";
|
||||
|
||||
throw new InvalidOperationException($"This test has no value for the setting '{setting.SettingName}' of type {valueType}. Extend ToLuaValue with a value which PluginConfiguration accepts for that type.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the type a setting holds.
|
||||
/// </summary>
|
||||
private static Type GetValueType(ConfigMetaBase setting)
|
||||
{
|
||||
for (var type = setting.GetType(); type is not null; type = type.BaseType)
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ConfigMeta<,>))
|
||||
return type.GetGenericArguments()[1];
|
||||
|
||||
throw new InvalidOperationException($"The setting '{setting.SettingName}' is no ConfigMeta, so this test cannot tell which type it holds.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the plugin of this test manages a setting: by locking it, or by contributing to it.
|
||||
/// </summary>
|
||||
private static bool IsManagedByThePlugin(ConfigMetaBase setting) => (setting.IsLocked && setting.LockedByConfigPluginId == PLUGIN_ID) || setting.ContributingConfigPluginIds.Contains(PLUGIN_ID);
|
||||
}
|
||||
Reference in new issue
Block a user