mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-17 06:18:00 +00:00
Added backups of all data, restorable on another computer (#1008)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
ab45580473
commit
936043319c
144 files changed
+10417
-424
No files matched your search
@@ -33,11 +33,11 @@ Plugins can configure:
|
||||
|
||||
Configuration plugins provide three kinds of values:
|
||||
- **Managed settings:** simple values such as booleans, numbers, strings, enums, lists, or sets handled through `ManagedConfiguration`. These values may be locked or used as organization defaults. Which configuration plugin owns a locked setting is persisted in `Data.ManagedLockedConfigurations`, and organization defaults are tracked in `Data.ManagedEditableDefaults`. Both are cleaned up generically by `ManagedConfiguration.CleanupLeftOverManagedConfigurations(...)` when the owning plugin is gone. The value a setting had before a configuration plugin took it over is kept in `Data.ManagedUserValueSnapshots` and restored by that same clean-up, so removing a plugin hands the user's own value back instead of the app default.
|
||||
- **Managed configuration objects:** complex Lua tables that are persisted into `SettingsManager.ConfigurationData`, implement `IConfigurationObject`, and are cleaned up through `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`. Examples include providers, profiles, chat templates, data sources, and document analysis policies.
|
||||
- **Managed configuration objects:** complex Lua tables that are persisted into `SettingsManager.ConfigurationData`, implement `IConfigurationObject`, and are described by a `PluginConfigurationObjectDescriptor` in `PluginConfigurationObjectDescriptors`. Reading them from a plugin and cleaning up the left-over ones go through these descriptors. Examples include providers, profiles, chat templates, data sources, and document analysis policies.
|
||||
- **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 configuration objects, update `PluginConfigurationObject.cs` and `PluginConfigurationObjectType.cs`, persist them in the appropriate `ConfigurationData` collection, and add cleanup via `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`.
|
||||
- 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`.
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a type of configuration object which derives from ConfigurationBaseObject.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the configuration objects.</param>
|
||||
/// <param name="luaTableName">The Lua table in which a configuration plugin defines these objects.</param>
|
||||
/// <param name="selection">Where the settings keep these objects.</param>
|
||||
/// <param name="nextNumSelection">The number which the next of these objects gets.</param>
|
||||
/// <param name="parser">Reads one of these objects from its Lua table.</param>
|
||||
/// <param name="secretStoreType">Where the API keys of these objects are kept, or null for objects without one.</param>
|
||||
/// <typeparam name="TClass">The class of the configuration objects.</typeparam>
|
||||
public sealed class ConfigurationBaseObjectDescriptor<TClass>(PluginConfigurationObjectType type, string luaTableName, Expression<Func<Data, List<TClass>>> selection, Expression<Func<Data, uint>> nextNumSelection, ConfigurationObjectParser parser, SecretStoreType? secretStoreType = null) : PluginConfigurationObjectDescriptor where TClass : ConfigurationBaseObject
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override PluginConfigurationObjectType Type => type;
|
||||
|
||||
/// <summary>
|
||||
/// The Lua table in which a configuration plugin defines these objects.
|
||||
/// </summary>
|
||||
public string LuaTableName => luaTableName;
|
||||
|
||||
/// <summary>
|
||||
/// Where the settings keep these objects.
|
||||
/// </summary>
|
||||
public Expression<Func<Data, List<TClass>>> Selection => selection;
|
||||
|
||||
/// <summary>
|
||||
/// The number which the next of these objects gets.
|
||||
/// </summary>
|
||||
public Expression<Func<Data, uint>> NextNumSelection => nextNumSelection;
|
||||
|
||||
/// <summary>
|
||||
/// Reads one of these objects from its Lua table.
|
||||
/// </summary>
|
||||
public ConfigurationObjectParser Parser => parser;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool TryParse(LuaTable mainTable, Guid configPluginId, List<PluginConfigurationObject> configObjects, bool dryRun, string pluginPath) => PluginConfigurationObject.TryParse(this, mainTable, configPluginId, configObjects, dryRun, pluginPath);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<bool> CleanLeftOverAsync(IList<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedEnterpriseConfigPluginIds, IReadOnlySet<Guid> notStartedConfigPluginIds, IList<PluginConfigurationObject> configObjectList) => PluginConfigurationObject.CleanLeftOverConfigurationObjects(type, selection, availablePlugins, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList, secretStoreType);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int RemovePluginObjects(Data settings) => selection.Compile()(settings).RemoveAll(configObject => configObject.IsEnterpriseConfiguration);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Reads one configuration object from the Lua table in which a configuration plugin defines it.
|
||||
/// </summary>
|
||||
/// <param name="index">The position of the entry within its Lua table, which the log names.</param>
|
||||
/// <param name="table">The entry to read.</param>
|
||||
/// <param name="configPluginId">The configuration plugin which defines the object.</param>
|
||||
/// <param name="pluginPath">The directory of the plugin, which paths in the entry are relative to.</param>
|
||||
/// <param name="dryRun">Whether the plugin is only checked, e.g., before an import. Nothing must be stored then.</param>
|
||||
/// <param name="configObject">The configuration object read from the entry.</param>
|
||||
/// <returns>True when the entry defines a valid configuration object.</returns>
|
||||
public delegate bool ConfigurationObjectParser(int index, LuaTable table, Guid configPluginId, string pluginPath, bool dryRun, out ConfigurationBaseObject configObject);
|
||||
@@ -0,0 +1,32 @@
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the data sources which configuration plugins provide.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The settings keep every data source in a list of IDataSource, and a data source of a configuration
|
||||
/// plugin is a record struct rather than a ConfigurationBaseObject. That is why data sources are read
|
||||
/// in a way of their own.
|
||||
/// </remarks>
|
||||
public sealed class DataSourceConfigurationObjectDescriptor : PluginConfigurationObjectDescriptor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override PluginConfigurationObjectType Type => PluginConfigurationObjectType.DATA_SOURCE;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool TryParse(LuaTable mainTable, Guid configPluginId, List<PluginConfigurationObject> configObjects, bool dryRun, string pluginPath) => PluginConfigurationObject.TryParseDataSources(mainTable, configPluginId, configObjects, dryRun);
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// The secret of a data source is not necessarily an API key, e.g., it is a password, so it is
|
||||
/// deleted as a secret.
|
||||
/// </remarks>
|
||||
public override Task<bool> CleanLeftOverAsync(IList<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedEnterpriseConfigPluginIds, IReadOnlySet<Guid> notStartedConfigPluginIds, IList<PluginConfigurationObject> configObjectList) => PluginConfigurationObject.CleanLeftOverConfigurationObjects(this.Type, x => x.DataSources, availablePlugins, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList, SecretStoreType.DATA_SOURCE, deleteSecret: true);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int RemovePluginObjects(Data settings) => settings.DataSources.RemoveAll(dataSource => dataSource.IsEnterpriseConfiguration);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public sealed record NoConfigurationObject : ConfigurationBaseObject
|
||||
{
|
||||
public static readonly NoConfigurationObject INSTANCE = new();
|
||||
|
||||
private NoConfigurationObject()
|
||||
{
|
||||
this.Id = Guid.Empty.ToString();
|
||||
this.Name = "No Configuration";
|
||||
}
|
||||
|
||||
#region Overrides of ConfigurationBaseObject
|
||||
|
||||
public override string Id { get; init; }
|
||||
|
||||
public override uint Num { get; init; }
|
||||
|
||||
public override string Name { get; init; }
|
||||
|
||||
public override bool IsEnterpriseConfiguration { get; init; }
|
||||
|
||||
public override Guid EnterpriseConfigurationPluginId { get; init; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -22,16 +22,12 @@ public static class PluginArchive
|
||||
using var archive = ZipFile.OpenRead(sourceArchiveFileName);
|
||||
Directory.CreateDirectory(destinationDirectory);
|
||||
|
||||
var destinationDirectoryFullPath = Path.GetFullPath(destinationDirectory);
|
||||
if (!destinationDirectoryFullPath.EndsWith(Path.DirectorySeparatorChar))
|
||||
destinationDirectoryFullPath += Path.DirectorySeparatorChar;
|
||||
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
var normalizedEntryName = NormalizeEntryName(entry.FullName);
|
||||
var destinationPath = GetEntryDestinationPath(destinationDirectoryFullPath, normalizedEntryName);
|
||||
var normalizedEntryName = ZipEntryNames.Normalize(entry.FullName);
|
||||
var destinationPath = ZipEntryNames.GetDestinationPath(destinationDirectory, normalizedEntryName);
|
||||
|
||||
if (normalizedEntryName.EndsWith('/'))
|
||||
if (normalizedEntryName.EndsWith(ZipEntryNames.SEPARATOR))
|
||||
{
|
||||
if (entry.Length != 0)
|
||||
throw new InvalidDataException($"The plugin archive contains a directory entry with data: '{entry.FullName}'.");
|
||||
@@ -44,37 +40,4 @@ public static class PluginArchive
|
||||
entry.ExtractToFile(destinationPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeEntryName(string entryName)
|
||||
{
|
||||
var normalizedEntryName = entryName.Replace('\\', '/');
|
||||
if (string.IsNullOrWhiteSpace(normalizedEntryName))
|
||||
throw new InvalidDataException("The plugin archive contains an empty entry name.");
|
||||
|
||||
if (normalizedEntryName.Contains('\0'))
|
||||
throw new InvalidDataException($"The plugin archive contains an invalid entry name: '{entryName}'.");
|
||||
|
||||
if (normalizedEntryName.StartsWith('/'))
|
||||
throw new InvalidDataException($"The plugin archive contains a rooted entry name: '{entryName}'.");
|
||||
|
||||
if (normalizedEntryName is [_, ':', ..])
|
||||
throw new InvalidDataException($"The plugin archive contains a drive-qualified entry name: '{entryName}'.");
|
||||
|
||||
var pathSegments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (pathSegments.Length == 0 || pathSegments.Any(segment => segment is "." or ".."))
|
||||
throw new InvalidDataException($"The plugin archive contains an unsafe entry name: '{entryName}'.");
|
||||
|
||||
return normalizedEntryName;
|
||||
}
|
||||
|
||||
private static string GetEntryDestinationPath(string destinationDirectoryFullPath, string normalizedEntryName)
|
||||
{
|
||||
var pathSegments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
var relativePath = Path.Combine(pathSegments);
|
||||
var destinationPath = Path.GetFullPath(Path.Combine(destinationDirectoryFullPath, relativePath));
|
||||
if (!destinationPath.StartsWith(destinationDirectoryFullPath, StringComparison.Ordinal))
|
||||
throw new InvalidDataException($"The plugin archive contains an entry outside the destination directory: '{normalizedEntryName}'.");
|
||||
|
||||
return destinationPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,6 +272,12 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
// Config: allow the user to share or export plugins?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToSharePlugins, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: allow the user to back up the data of AI Studio?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToBackUpData, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: allow the user to restore the data of AI Studio from a backup?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToRestoreData, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: show administration settings?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowAdminSettings, this.Id, settingsTable, dryRun);
|
||||
|
||||
@@ -349,26 +355,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
// Config: enterprise-managed approvals for assistant plugins
|
||||
this.TryProcessEnterpriseApprovedAssistantPlugins(settingsTable, dryRun);
|
||||
|
||||
// Handle configured LLM providers:
|
||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun, this.PluginPath);
|
||||
|
||||
// Handle configured transcription providers:
|
||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER, x => x.TranscriptionProviders, x => x.NextTranscriptionNum, mainTable, this.Id, ref this.configObjects, dryRun, this.PluginPath);
|
||||
|
||||
// Handle configured embedding providers:
|
||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, x => x.NextEmbeddingNum, mainTable, this.Id, ref this.configObjects, dryRun, this.PluginPath);
|
||||
|
||||
// Handle configured chat templates:
|
||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, x => x.NextChatTemplateNum, mainTable, this.Id, ref this.configObjects, dryRun, this.PluginPath);
|
||||
|
||||
// Handle configured data sources:
|
||||
PluginConfigurationObject.TryParseDataSources(mainTable, this.Id, ref this.configObjects, dryRun);
|
||||
|
||||
// Handle configured profiles:
|
||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.PROFILE, x => x.Profiles, x => x.NextProfileNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
||||
|
||||
// Handle configured document analysis policies:
|
||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, x => x.NextDocumentAnalysisPolicyNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
||||
// Handle configured objects such as LLM providers, chat templates, or data sources:
|
||||
foreach (var descriptor in PluginConfigurationObjectDescriptors.ALL)
|
||||
descriptor.TryParse(mainTable, this.Id, this.configObjects, dryRun, this.PluginPath);
|
||||
|
||||
// Handle configured mandatory infos:
|
||||
this.TryReadMandatoryInfos(mainTable);
|
||||
|
||||
@@ -125,53 +125,22 @@ public sealed record PluginConfigurationObject
|
||||
/// provided list with results.
|
||||
/// </summary>
|
||||
/// <typeparam name="TClass">The type of configuration object to parse, which must
|
||||
/// inherit from <see cref="ConfigurationBaseObject"/>.</typeparam>
|
||||
/// <param name="configObjectType">The type of configuration object to process, as specified
|
||||
/// in <see cref="PluginConfigurationObjectType"/>.</param>
|
||||
/// <param name="configObjectSelection">An expression to retrieve existing configuration objects from
|
||||
/// the main configuration data.</param>
|
||||
/// <param name="nextConfigObjectNumSelection">An expression to retrieve the next available configuration
|
||||
/// object number from the main configuration data.</param>
|
||||
/// inherit from ConfigurationBaseObject.</typeparam>
|
||||
/// <param name="descriptor">Describes the type of configuration object: its Lua table, how to
|
||||
/// read one entry, and where the settings keep these objects.</param>
|
||||
/// <param name="mainTable">The Lua table containing entries to parse into configuration objects.</param>
|
||||
/// <param name="configPluginId">The unique identifier of the plugin associated with the configuration
|
||||
/// objects being parsed.</param>
|
||||
/// <param name="configObjects">The list to populate with the parsed configuration objects.
|
||||
/// This parameter is passed by reference.</param>
|
||||
/// <param name="configObjects">The list to populate with the parsed configuration objects.</param>
|
||||
/// <param name="dryRun">Specifies whether to perform the operation as a dry run, where changes
|
||||
/// are not persisted.</param>
|
||||
/// <param name="pluginPath">An optional parameter specifying the file path of the plugin, used for relative paths in the Lua table.</param>
|
||||
/// <param name="pluginPath">The directory of the plugin, used for relative paths in the Lua table.</param>
|
||||
/// <returns>Returns true if parsing succeeds and configuration objects are added
|
||||
/// to the list; otherwise, false.</returns>
|
||||
public static bool TryParse<TClass>(
|
||||
PluginConfigurationObjectType configObjectType,
|
||||
Expression<Func<Data, List<TClass>>> configObjectSelection,
|
||||
Expression<Func<Data, uint>> nextConfigObjectNumSelection,
|
||||
LuaTable mainTable,
|
||||
Guid configPluginId,
|
||||
ref List<PluginConfigurationObject> configObjects,
|
||||
bool dryRun,
|
||||
string pluginPath = ""
|
||||
) where TClass : ConfigurationBaseObject
|
||||
public static bool TryParse<TClass>(ConfigurationBaseObjectDescriptor<TClass> descriptor, LuaTable mainTable, Guid configPluginId, List<PluginConfigurationObject> configObjects, bool dryRun, string pluginPath) where TClass : ConfigurationBaseObject
|
||||
{
|
||||
var luaTableName = configObjectType switch
|
||||
{
|
||||
PluginConfigurationObjectType.LLM_PROVIDER => "LLM_PROVIDERS",
|
||||
PluginConfigurationObjectType.CHAT_TEMPLATE => "CHAT_TEMPLATES",
|
||||
PluginConfigurationObjectType.DATA_SOURCE => "DATA_SOURCES",
|
||||
PluginConfigurationObjectType.EMBEDDING_PROVIDER => "EMBEDDING_PROVIDERS",
|
||||
PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => "TRANSCRIPTION_PROVIDERS",
|
||||
PluginConfigurationObjectType.PROFILE => "PROFILES",
|
||||
PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY => "DOCUMENT_ANALYSIS_POLICIES",
|
||||
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (luaTableName is null)
|
||||
{
|
||||
LOG.LogError("The configuration object type '{ConfigObjectType}' is not supported yet (config plugin id: {ConfigPluginId}).", configObjectType, configPluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var configObjectType = descriptor.Type;
|
||||
var luaTableName = descriptor.LuaTableName;
|
||||
if (!mainTable.TryGetValue(luaTableName, out var luaValue) || !luaValue.TryRead<LuaTable>(out var luaTable))
|
||||
{
|
||||
LOG.LogWarning("The table '{LuaTableName}' does not exist or is not a valid table (config plugin id: {ConfigPluginId}).", luaTableName, configPluginId);
|
||||
@@ -179,7 +148,7 @@ public sealed record PluginConfigurationObject
|
||||
}
|
||||
|
||||
var localSettingsManager = SettingsManagerAccess;
|
||||
var storedObjects = configObjectSelection.Compile()(localSettingsManager.ConfigurationData);
|
||||
var storedObjects = descriptor.Selection.Compile()(localSettingsManager.ConfigurationData);
|
||||
var numberObjects = luaTable.ArrayLength;
|
||||
ThreadSafeRandom? random = null;
|
||||
for (var i = 1; i <= numberObjects; i++)
|
||||
@@ -191,19 +160,7 @@ public sealed record PluginConfigurationObject
|
||||
continue;
|
||||
}
|
||||
|
||||
var (wasParsingSuccessful, configObject) = configObjectType switch
|
||||
{
|
||||
PluginConfigurationObjectType.LLM_PROVIDER => (Settings.Provider.TryParseProviderTable(i, luaObjectTable, configPluginId, pluginPath, dryRun, out var configurationObject) && configurationObject != Settings.Provider.NONE, configurationObject),
|
||||
PluginConfigurationObjectType.CHAT_TEMPLATE => (ChatTemplate.TryParseChatTemplateTable(i, luaObjectTable, configPluginId, pluginPath, out var configurationObject) && configurationObject != ChatTemplate.NO_CHAT_TEMPLATE, configurationObject),
|
||||
PluginConfigurationObjectType.PROFILE => (Profile.TryParseProfileTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Profile.NO_PROFILE, configurationObject),
|
||||
PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => (TranscriptionProvider.TryParseTranscriptionProviderTable(i, luaObjectTable, configPluginId, pluginPath, dryRun, out var configurationObject) && configurationObject != TranscriptionProvider.NONE, configurationObject),
|
||||
PluginConfigurationObjectType.EMBEDDING_PROVIDER => (EmbeddingProvider.TryParseEmbeddingProviderTable(i, luaObjectTable, configPluginId, pluginPath, dryRun, out var configurationObject) && configurationObject != EmbeddingProvider.NONE, configurationObject),
|
||||
PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY => (DataDocumentAnalysisPolicy.TryProcessConfiguration(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject is DataDocumentAnalysisPolicy, configurationObject),
|
||||
|
||||
_ => (false, NoConfigurationObject.INSTANCE)
|
||||
};
|
||||
|
||||
if (wasParsingSuccessful)
|
||||
if (descriptor.Parser(i, luaObjectTable, configPluginId, pluginPath, dryRun, out var configObject))
|
||||
{
|
||||
// Store it in the config object list:
|
||||
configObjects.Add(new()
|
||||
@@ -236,7 +193,7 @@ public sealed record PluginConfigurationObject
|
||||
else
|
||||
{
|
||||
configObject = KeepSynchronizedTokenizer(configObject, null);
|
||||
if (nextConfigObjectNumSelection.TryIncrement(localSettingsManager.ConfigurationData, IncrementType.POST) is { Success: true, UpdatedValue: var nextNum })
|
||||
if (descriptor.NextNumSelection.TryIncrement(localSettingsManager.ConfigurationData, IncrementType.POST) is { Success: true, UpdatedValue: var nextNum })
|
||||
{
|
||||
// Case: Increment the next number was successful
|
||||
configObject = configObject with { Num = nextNum };
|
||||
@@ -330,11 +287,7 @@ public sealed record PluginConfigurationObject
|
||||
/// <param name="configObjects">The list to populate with the parsed configuration objects.</param>
|
||||
/// <param name="dryRun">Specifies whether to perform the operation as a dry run.</param>
|
||||
/// <returns>True if the table was present and processed; otherwise false.</returns>
|
||||
public static bool TryParseDataSources(
|
||||
LuaTable mainTable,
|
||||
Guid configPluginId,
|
||||
ref List<PluginConfigurationObject> configObjects,
|
||||
bool dryRun)
|
||||
public static bool TryParseDataSources(LuaTable mainTable, Guid configPluginId, List<PluginConfigurationObject> configObjects, bool dryRun)
|
||||
{
|
||||
const string LUA_TABLE_NAME = "DATA_SOURCES";
|
||||
if (!mainTable.TryGetValue(LUA_TABLE_NAME, out var luaValue) || !luaValue.TryRead<LuaTable>(out var luaTable))
|
||||
@@ -698,16 +651,6 @@ public sealed record PluginConfigurationObject
|
||||
|
||||
var fullPluginPath = Path.GetFullPath(pluginPath);
|
||||
var candidatePath = Path.GetFullPath(Path.Combine(fullPluginPath, configuredTokenizerPath));
|
||||
|
||||
if (candidatePath.Equals(fullPluginPath, StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
var pluginPrefix = fullPluginPath.EndsWith(Path.DirectorySeparatorChar)
|
||||
? fullPluginPath
|
||||
: fullPluginPath + Path.DirectorySeparatorChar;
|
||||
|
||||
return candidatePath.StartsWith(pluginPrefix, StringComparison.OrdinalIgnoreCase)
|
||||
? candidatePath
|
||||
: null;
|
||||
return PathTools.IsInsideDirectory(fullPluginPath, candidatePath) ? candidatePath : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Describes one type of configuration object which configuration plugins provide, such as LLM
|
||||
/// providers or chat templates.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Whatever treats every type of configuration object alike goes through these descriptors, so that
|
||||
/// none of these places can forget a type: reading the objects from a configuration plugin, removing
|
||||
/// those a plugin no longer defines, and leaving all of them out of a backup. A new type of
|
||||
/// configuration object needs a descriptor in PluginConfigurationObjectDescriptors, and every such place
|
||||
/// handles it.
|
||||
/// </remarks>
|
||||
public abstract class PluginConfigurationObjectDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the configuration objects.
|
||||
/// </summary>
|
||||
public abstract PluginConfigurationObjectType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Reads the configuration objects of this type which a configuration plugin defines, and stores
|
||||
/// them in the settings unless the plugin is only checked.
|
||||
/// </summary>
|
||||
/// <param name="mainTable">The main table of the configuration plugin.</param>
|
||||
/// <param name="configPluginId">The configuration plugin.</param>
|
||||
/// <param name="configObjects">The list to which every object read is added.</param>
|
||||
/// <param name="dryRun">Whether the plugin is only checked, e.g., before an import. Nothing is stored then.</param>
|
||||
/// <param name="pluginPath">The directory of the plugin, which paths in the objects are relative to.</param>
|
||||
/// <returns>True when the plugin defines a table for this type.</returns>
|
||||
public abstract bool TryParse(LuaTable mainTable, Guid configPluginId, List<PluginConfigurationObject> configObjects, bool dryRun, string pluginPath);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the configuration objects of this type which belong to a configuration plugin that is
|
||||
/// gone or no longer defines them, along with their secrets and tokenizers.
|
||||
/// </summary>
|
||||
/// <param name="availablePlugins">The plugins AI Studio found.</param>
|
||||
/// <param name="deployedEnterpriseConfigPluginIds">The configuration plugins an organization deployed, including those which could not be loaded.</param>
|
||||
/// <param name="notStartedConfigPluginIds">The configuration plugins which were loaded, but did not start.</param>
|
||||
/// <param name="configObjectList">The configuration objects all running configuration plugins define.</param>
|
||||
/// <returns>True when the settings changed.</returns>
|
||||
public abstract Task<bool> CleanLeftOverAsync(IList<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedEnterpriseConfigPluginIds, IReadOnlySet<Guid> notStartedConfigPluginIds, IList<PluginConfigurationObject> configObjectList);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the configuration objects of this type which a configuration plugin manages.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings to change, e.g., a copy for a backup. Never the settings AI Studio runs with.</param>
|
||||
/// <returns>How many objects were removed.</returns>
|
||||
public abstract int RemovePluginObjects(Data settings);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// The descriptors of every type of configuration object which configuration plugins provide.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A new type of configuration object gets its descriptor here and joins ALL. Reading the objects from
|
||||
/// a configuration plugin and removing the left-over ones then handle it, and a test checks that no
|
||||
/// type of PluginConfigurationObjectType is missing.
|
||||
/// </remarks>
|
||||
public static class PluginConfigurationObjectDescriptors
|
||||
{
|
||||
public static readonly ConfigurationBaseObjectDescriptor<Settings.Provider> LLM_PROVIDER = new(PluginConfigurationObjectType.LLM_PROVIDER, "LLM_PROVIDERS", x => x.Providers, x => x.NextProviderNum, ParseLLMProvider, SecretStoreType.LLM_PROVIDER);
|
||||
|
||||
public static readonly ConfigurationBaseObjectDescriptor<TranscriptionProvider> TRANSCRIPTION_PROVIDER = new(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER, "TRANSCRIPTION_PROVIDERS", x => x.TranscriptionProviders, x => x.NextTranscriptionNum, ParseTranscriptionProvider, SecretStoreType.TRANSCRIPTION_PROVIDER);
|
||||
|
||||
public static readonly ConfigurationBaseObjectDescriptor<EmbeddingProvider> EMBEDDING_PROVIDER = new(PluginConfigurationObjectType.EMBEDDING_PROVIDER, "EMBEDDING_PROVIDERS", x => x.EmbeddingProviders, x => x.NextEmbeddingNum, ParseEmbeddingProvider, SecretStoreType.EMBEDDING_PROVIDER);
|
||||
|
||||
public static readonly ConfigurationBaseObjectDescriptor<ChatTemplate> CHAT_TEMPLATE = new(PluginConfigurationObjectType.CHAT_TEMPLATE, "CHAT_TEMPLATES", x => x.ChatTemplates, x => x.NextChatTemplateNum, ParseChatTemplate);
|
||||
|
||||
public static readonly DataSourceConfigurationObjectDescriptor DATA_SOURCE = new();
|
||||
|
||||
public static readonly ConfigurationBaseObjectDescriptor<Profile> PROFILE = new(PluginConfigurationObjectType.PROFILE, "PROFILES", x => x.Profiles, x => x.NextProfileNum, ParseProfile);
|
||||
|
||||
public static readonly ConfigurationBaseObjectDescriptor<DataDocumentAnalysisPolicy> DOCUMENT_ANALYSIS_POLICY = new(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, "DOCUMENT_ANALYSIS_POLICIES", x => x.DocumentAnalysis.Policies, x => x.NextDocumentAnalysisPolicyNum, ParseDocumentAnalysisPolicy);
|
||||
|
||||
/// <summary>
|
||||
/// Every descriptor, in the order in which configuration plugins are read.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyList<PluginConfigurationObjectDescriptor> ALL =
|
||||
[
|
||||
LLM_PROVIDER,
|
||||
TRANSCRIPTION_PROVIDER,
|
||||
EMBEDDING_PROVIDER,
|
||||
CHAT_TEMPLATE,
|
||||
DATA_SOURCE,
|
||||
PROFILE,
|
||||
DOCUMENT_ANALYSIS_POLICY,
|
||||
];
|
||||
|
||||
private static bool ParseLLMProvider(int index, LuaTable table, Guid configPluginId, string pluginPath, bool dryRun, out ConfigurationBaseObject configObject) =>
|
||||
Settings.Provider.TryParseProviderTable(index, table, configPluginId, pluginPath, dryRun, out configObject) && configObject != Settings.Provider.NONE;
|
||||
|
||||
private static bool ParseTranscriptionProvider(int index, LuaTable table, Guid configPluginId, string pluginPath, bool dryRun, out ConfigurationBaseObject configObject) =>
|
||||
TranscriptionProvider.TryParseTranscriptionProviderTable(index, table, configPluginId, pluginPath, dryRun, out configObject) && configObject != TranscriptionProvider.NONE;
|
||||
|
||||
private static bool ParseEmbeddingProvider(int index, LuaTable table, Guid configPluginId, string pluginPath, bool dryRun, out ConfigurationBaseObject configObject) =>
|
||||
EmbeddingProvider.TryParseEmbeddingProviderTable(index, table, configPluginId, pluginPath, dryRun, out configObject) && configObject != EmbeddingProvider.NONE;
|
||||
|
||||
private static bool ParseChatTemplate(int index, LuaTable table, Guid configPluginId, string pluginPath, bool dryRun, out ConfigurationBaseObject configObject) =>
|
||||
ChatTemplate.TryParseChatTemplateTable(index, table, configPluginId, pluginPath, out configObject) && configObject != ChatTemplate.NO_CHAT_TEMPLATE;
|
||||
|
||||
private static bool ParseProfile(int index, LuaTable table, Guid configPluginId, string pluginPath, bool dryRun, out ConfigurationBaseObject configObject) =>
|
||||
Profile.TryParseProfileTable(index, table, configPluginId, out configObject) && configObject != Profile.NO_PROFILE;
|
||||
|
||||
private static bool ParseDocumentAnalysisPolicy(int index, LuaTable table, Guid configPluginId, string pluginPath, bool dryRun, out ConfigurationBaseObject configObject) =>
|
||||
DataDocumentAnalysisPolicy.TryProcessConfiguration(index, table, configPluginId, out configObject) && configObject is DataDocumentAnalysisPolicy;
|
||||
}
|
||||
@@ -14,7 +14,19 @@ public static partial class PluginFactory
|
||||
{
|
||||
private static readonly List<IAvailablePlugin> AVAILABLE_PLUGINS = [];
|
||||
private static readonly SemaphoreSlim PLUGIN_LOAD_SEMAPHORE = new(1, 1);
|
||||
|
||||
private static readonly TaskCompletionSource FIRST_LOAD_COMPLETED = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
/// <summary>
|
||||
/// Completes once LoadAll got through for the first time, the clean-up of the configuration included.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Until then, the settings may lack whatever the configuration of an organization provides, such
|
||||
/// as its embedding providers: on a new computer, or after restoring a backup, which leaves them
|
||||
/// out. Whoever acts on such objects while AI Studio starts waits for this, or takes them for
|
||||
/// objects which are gone. It never completes when loading failed, so wait with a limit.
|
||||
/// </remarks>
|
||||
public static Task FirstLoadCompleted => FIRST_LOAD_COMPLETED.Task;
|
||||
|
||||
/// <summary>
|
||||
/// How long a single plugin may take to load, and again to start.
|
||||
/// </summary>
|
||||
@@ -303,33 +315,10 @@ public static partial class PluginFactory
|
||||
|
||||
var wasConfigurationChanged = hasStartedConfigurationPlugins;
|
||||
|
||||
// Check LLM providers:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList, SecretStoreType.LLM_PROVIDER))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check transcription providers:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER, x => x.TranscriptionProviders, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList, SecretStoreType.TRANSCRIPTION_PROVIDER))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check embedding providers:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList, SecretStoreType.EMBEDDING_PROVIDER))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check data sources:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DATA_SOURCE, x => x.DataSources, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList, SecretStoreType.DATA_SOURCE, deleteSecret: true))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check chat templates:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check profiles:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.PROFILE, x => x.Profiles, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check document analysis policies:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
// Check configured objects such as LLM providers, chat templates, or data sources:
|
||||
foreach (var descriptor in PluginConfigurationObjectDescriptors.ALL)
|
||||
if(await descriptor.CleanLeftOverAsync(AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
//
|
||||
// Check left-over mandatory info acceptances. The mandatory infos come from the running
|
||||
@@ -372,6 +361,8 @@ public static partial class PluginFactory
|
||||
await SettingsManagerAccess.StoreSettings();
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
FIRST_LOAD_COMPLETED.TrySetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in new issue
Block a user