Add configuration object management for providers and chat templates

This commit is contained in:
Thorsten Sommer 2025-08-18 20:34:04 +02:00
parent f88fdabfbe
commit 18828fbe85
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
5 changed files with 131 additions and 72 deletions

View File

@ -14,7 +14,14 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfiguration).Namespace, nameof(PluginConfiguration)); 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 ILogger<PluginConfiguration> LOGGER = Program.LOGGER_FACTORY.CreateLogger<PluginConfiguration>();
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>(); private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
private readonly List<PluginConfigurationObject> configObjects = [];
/// <summary>
/// The list of configuration objects. Configuration objects are, e.g., providers or chat templates.
/// </summary>
public IEnumerable<PluginConfigurationObject> ConfigObjects => this.configObjects;
public async Task InitializeAsync(bool dryRun) public async Task InitializeAsync(bool dryRun)
{ {
if(!this.TryProcessConfiguration(dryRun, out var issue)) if(!this.TryProcessConfiguration(dryRun, out var issue))
@ -30,11 +37,13 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
/// <summary> /// <summary>
/// Tries to initialize the UI text content of the plugin. /// Tries to initialize the UI text content of the plugin.
/// </summary> /// </summary>
/// <param name="dryRun">When true, the method will not apply any changes, but only check if the configuration can be read.</param> /// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
/// <param name="message">The error message, when the UI text content could not be read.</param> /// <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> /// <returns>True, when the UI text content could be read successfully.</returns>
private bool TryProcessConfiguration(bool dryRun, out string message) private bool TryProcessConfiguration(bool dryRun, out string message)
{ {
this.configObjects.Clear();
// Ensure that the main CONFIG table exists and is a valid Lua table: // Ensure that the main CONFIG table exists and is a valid Lua table:
if (!this.state.Environment["CONFIG"].TryRead<LuaTable>(out var mainTable)) if (!this.state.Environment["CONFIG"].TryRead<LuaTable>(out var mainTable))
{ {
@ -90,29 +99,38 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Apply the configured providers to the system settings: // Apply the configured providers to the system settings:
// //
#pragma warning disable MWAIS0001 #pragma warning disable MWAIS0001
if (!dryRun) foreach (var configuredProvider in configuredProviders)
{ {
foreach (var configuredProvider in configuredProviders) // The iterating variable is immutable, so we need to create a local copy:
{ var provider = configuredProvider;
// 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); // Store this provider in the config object list:
if (providerIndex > -1) this.configObjects.Add(new()
{ {
// Case: The provider already exists, we update it: ConfigPluginId = this.Id,
var existingProvider = SETTINGS_MANAGER.ConfigurationData.Providers[providerIndex]; Id = Guid.Parse(provider.Id),
provider = provider with { Num = existingProvider.Num }; // Keep the original number Type = PluginConfigurationObjectType.LLM_PROVIDER,
SETTINGS_MANAGER.ConfigurationData.Providers[providerIndex] = provider; });
}
else if (dryRun)
{ continue;
// Case: The provider does not exist, we add it:
provider = provider with { Num = SETTINGS_MANAGER.ConfigurationData.NextProviderNum++ }; var providerIndex = SETTINGS_MANAGER.ConfigurationData.Providers.FindIndex(p => p.Id == provider.Id);
SETTINGS_MANAGER.ConfigurationData.Providers.Add(provider); 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 #pragma warning restore MWAIS0001
// //
@ -138,26 +156,35 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
} }
// Apply configured chat templates to the system settings: // Apply configured chat templates to the system settings:
if (!dryRun) foreach (var configuredTemplate in configuredTemplates)
{ {
foreach (var configuredTemplate in configuredTemplates) // The iterating variable is immutable, so we need to create a local copy:
var template = configuredTemplate;
// Store this provider in the config object list:
this.configObjects.Add(new()
{ {
// The iterating variable is immutable, so we need to create a local copy: ConfigPluginId = this.Id,
var template = configuredTemplate; Id = Guid.Parse(template.Id),
var tplIndex = SETTINGS_MANAGER.ConfigurationData.ChatTemplates.FindIndex(t => t.Id == template.Id); Type = PluginConfigurationObjectType.CHAT_TEMPLATE,
if (tplIndex > -1) });
{
// Case: The template already exists, we update it: if (dryRun)
var existingTemplate = SETTINGS_MANAGER.ConfigurationData.ChatTemplates[tplIndex]; continue;
template = template with { Num = existingTemplate.Num };
SETTINGS_MANAGER.ConfigurationData.ChatTemplates[tplIndex] = template; var tplIndex = SETTINGS_MANAGER.ConfigurationData.ChatTemplates.FindIndex(t => t.Id == template.Id);
} if (tplIndex > -1)
else {
{ // Case: The template already exists, we update it:
// Case: The template does not exist, we add it: var existingTemplate = SETTINGS_MANAGER.ConfigurationData.ChatTemplates[tplIndex];
template = template with { Num = SETTINGS_MANAGER.ConfigurationData.NextChatTemplateNum++ }; template = template with { Num = existingTemplate.Num };
SETTINGS_MANAGER.ConfigurationData.ChatTemplates.Add(template); SETTINGS_MANAGER.ConfigurationData.ChatTemplates[tplIndex] = template;
} }
else
{
// Case: The template does not exist, we add it:
template = template with { Num = SETTINGS_MANAGER.ConfigurationData.NextChatTemplateNum++ };
SETTINGS_MANAGER.ConfigurationData.ChatTemplates.Add(template);
} }
} }
} }

View File

@ -0,0 +1,23 @@
namespace AIStudio.Tools.PluginSystem;
/// <summary>
/// Represents metadata for a configuration object from a configuration plugin. These are
/// complex objects such as configured LLM providers, chat templates, etc.
/// </summary>
public sealed record PluginConfigurationObject
{
/// <summary>
/// The id of the configuration plugin to which this configuration object belongs.
/// </summary>
public required Guid ConfigPluginId { get; init; } = Guid.NewGuid();
/// <summary>
/// The id of the configuration object, e.g., the id of a chat template.
/// </summary>
public required Guid Id { get; init; } = Guid.NewGuid();
/// <summary>
/// The type of the configuration object.
/// </summary>
public required PluginConfigurationObjectType Type { get; init; } = PluginConfigurationObjectType.NONE;
}

View File

@ -0,0 +1,13 @@
namespace AIStudio.Tools.PluginSystem;
public enum PluginConfigurationObjectType
{
NONE,
UNKNOWN,
PROFILE,
DATA_SOURCE,
LLM_PROVIDER,
CHAT_TEMPLATE,
EMBEDDING_PROVIDER,
}

View File

@ -40,6 +40,8 @@ public static partial class PluginFactory
if (!await PLUGIN_LOAD_SEMAPHORE.WaitAsync(0, cancellationToken)) if (!await PLUGIN_LOAD_SEMAPHORE.WaitAsync(0, cancellationToken))
return; return;
var configObjectList = new List<PluginConfigurationObject>();
try try
{ {
LOG.LogInformation("Start loading plugins."); LOG.LogInformation("Start loading plugins.");
@ -112,7 +114,8 @@ public static partial class PluginFactory
} }
// Start or restart all plugins: // Start or restart all plugins:
await RestartAllPlugins(cancellationToken); var configObjects = await RestartAllPlugins(cancellationToken);
configObjectList.AddRange(configObjects);
} }
finally finally
{ {
@ -149,6 +152,16 @@ public static partial class PluginFactory
SETTINGS_MANAGER.ConfigurationData.Providers.Remove(configuredProvider); SETTINGS_MANAGER.ConfigurationData.Providers.Remove(configuredProvider);
wasConfigurationChanged = true; wasConfigurationChanged = true;
} }
if(!configObjectList.Any(configObject =>
configObject.Type is PluginConfigurationObjectType.LLM_PROVIDER &&
configObject.ConfigPluginId == providerSourcePluginId &&
configObject.Id.ToString() == configuredProvider.Id))
{
LOG.LogWarning($"The configured LLM provider '{configuredProvider.InstanceName}' (id={configuredProvider.Id}) is not present in the configuration plugin anymore. Removing the provider from the settings.");
SETTINGS_MANAGER.ConfigurationData.Providers.Remove(configuredProvider);
wasConfigurationChanged = true;
}
} }
#pragma warning restore MWAIS0001 #pragma warning restore MWAIS0001
@ -172,41 +185,17 @@ public static partial class PluginFactory
SETTINGS_MANAGER.ConfigurationData.ChatTemplates.Remove(configuredTemplate); SETTINGS_MANAGER.ConfigurationData.ChatTemplates.Remove(configuredTemplate);
wasConfigurationChanged = true; wasConfigurationChanged = true;
} }
}
if(!configObjectList.Any(configObject =>
// configObject.Type is PluginConfigurationObjectType.CHAT_TEMPLATE &&
// Before checking simple settings, validate that still-present configuration plugins haven't removed individual configObject.ConfigPluginId == templateSourcePluginId &&
// providers or chat templates they previously managed. If so, remove those items from our settings as well: configObject.Id.ToString() == configuredTemplate.Id))
//
#pragma warning disable MWAIS0001
foreach (var runningPlugin in RUNNING_PLUGINS.OfType<PluginConfiguration>())
{
var (providerIds, templateIds) = runningPlugin.GetManagedObjectIds();
var cfgPluginId = runningPlugin.Id;
// Providers managed by this plugin but no longer present in plugin config
var providersToRemove = SETTINGS_MANAGER.ConfigurationData.Providers
.Where(p => p.IsEnterpriseConfiguration && p.EnterpriseConfigurationPluginId == cfgPluginId && !providerIds.Contains(p.Id))
.ToList();
foreach (var p in providersToRemove)
{ {
LOG.LogWarning($"The configured LLM provider '{p.InstanceName}' (id={p.Id}) was removed from its configuration plugin (id={cfgPluginId}). Removing the provider from the settings."); LOG.LogWarning($"The configured chat template '{configuredTemplate.Name}' (id={configuredTemplate.Id}) is not present in the configuration plugin anymore. Removing the chat template from the settings.");
SETTINGS_MANAGER.ConfigurationData.Providers.Remove(p); SETTINGS_MANAGER.ConfigurationData.ChatTemplates.Remove(configuredTemplate);
wasConfigurationChanged = true;
}
// Chat templates managed by this plugin but no longer present in plugin config
var templatesToRemove = SETTINGS_MANAGER.ConfigurationData.ChatTemplates
.Where(t => t.IsEnterpriseConfiguration && t.EnterpriseConfigurationPluginId == cfgPluginId && !templateIds.Contains(t.Id))
.ToList();
foreach (var t in templatesToRemove)
{
LOG.LogWarning($"The configured chat template '{t.Name}' (id={t.Id}) was removed from its configuration plugin (id={cfgPluginId}). Removing the chat template from the settings.");
SETTINGS_MANAGER.ConfigurationData.ChatTemplates.Remove(t);
wasConfigurationChanged = true; wasConfigurationChanged = true;
} }
} }
#pragma warning restore MWAIS0001
// //
// ========================================================== // ==========================================================

View File

@ -11,9 +11,10 @@ public static partial class PluginFactory
/// </summary> /// </summary>
public static IReadOnlyCollection<PluginBase> RunningPlugins => RUNNING_PLUGINS; public static IReadOnlyCollection<PluginBase> RunningPlugins => RUNNING_PLUGINS;
private static async Task RestartAllPlugins(CancellationToken cancellationToken = default) private static async Task<List<PluginConfigurationObject>> RestartAllPlugins(CancellationToken cancellationToken = default)
{ {
LOG.LogInformation("Try to start or restart all plugins."); LOG.LogInformation("Try to start or restart all plugins.");
var configObjects = new List<PluginConfigurationObject>();
RUNNING_PLUGINS.Clear(); RUNNING_PLUGINS.Clear();
// //
@ -65,7 +66,12 @@ public static partial class PluginFactory
{ {
if (availablePlugin.IsInternal || SETTINGS_MANAGER.IsPluginEnabled(availablePlugin) || availablePlugin.Type == PluginType.CONFIGURATION) 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)
{
if (plugin is PluginConfiguration configPlugin)
configObjects.AddRange(configPlugin.ConfigObjects);
RUNNING_PLUGINS.Add(plugin); RUNNING_PLUGINS.Add(plugin);
}
} }
catch (Exception e) catch (Exception e)
{ {
@ -75,6 +81,7 @@ public static partial class PluginFactory
// 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);
return configObjects;
} }
private static async Task<PluginBase> Start(IAvailablePlugin meta, CancellationToken cancellationToken = default) private static async Task<PluginBase> Start(IAvailablePlugin meta, CancellationToken cancellationToken = default)