using System.Linq.Expressions; using AIStudio.Settings; using AIStudio.Settings.DataModel; using Lua; namespace AIStudio.Tools.PluginSystem; /// /// Represents metadata for a configuration object from a configuration plugin. These are /// complex objects such as configured LLM providers, chat templates, etc. /// public sealed record PluginConfigurationObject { private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(); /// /// The id of the configuration plugin to which this configuration object belongs. /// public required Guid ConfigPluginId { get; init; } = Guid.NewGuid(); /// /// The id of the configuration object, e.g., the id of a chat template. /// public required Guid Id { get; init; } = Guid.NewGuid(); /// /// The type of the configuration object. /// public required PluginConfigurationObjectType Type { get; init; } = PluginConfigurationObjectType.NONE; /// /// Parses Lua table entries into configuration objects of the specified type, populating the /// provided list with results. /// /// The type of configuration object to parse, which must /// inherit from . /// The type of configuration object to process, as specified /// in . /// An expression to retrieve existing configuration objects from /// the main configuration data. /// An expression to retrieve the next available configuration /// object number from the main configuration data. /// The Lua table containing entries to parse into configuration objects. /// The unique identifier of the plugin associated with the configuration /// objects being parsed. /// The list to populate with the parsed configuration objects. /// This parameter is passed by reference. /// Specifies whether to perform the operation as a dry run, where changes /// are not persisted. /// Returns true if parsing succeeds and configuration objects are added /// to the list; otherwise, false. public static bool TryParse( PluginConfigurationObjectType configObjectType, Expression>> configObjectSelection, Expression> nextConfigObjectNumSelection, LuaTable mainTable, Guid configPluginId, ref List configObjects, bool dryRun ) 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.PROFILE => "PROFILES", _ => null, }; if (luaTableName is null) { LOG.LogError($"The configuration object type '{configObjectType}' is not supported yet."); return false; } if (!mainTable.TryGetValue(luaTableName, out var luaValue) || !luaValue.TryRead(out var luaTable)) { LOG.LogWarning($"The {luaTableName} table does not exist or is not a valid table."); return false; } var storedObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData); var numberObjects = luaTable.ArrayLength; ThreadSafeRandom? random = null; for (var i = 1; i <= numberObjects; i++) { var luaObjectTableValue = luaTable[i]; if (!luaObjectTableValue.TryRead(out var luaObjectTable)) { LOG.LogWarning($"The {luaObjectTable} table at index {i} is not a valid table."); continue; } var (wasParsingSuccessful, configObject) = configObjectType switch { PluginConfigurationObjectType.LLM_PROVIDER => (Settings.Provider.TryParseProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Settings.Provider.NONE, configurationObject), PluginConfigurationObjectType.CHAT_TEMPLATE => (ChatTemplate.TryParseChatTemplateTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != ChatTemplate.NO_CHAT_TEMPLATE, configurationObject), _ => (false, NoConfigurationObject.INSTANCE) }; if (wasParsingSuccessful) { // Store it in the config object list: configObjects.Add(new() { ConfigPluginId = configPluginId, Id = Guid.Parse(configObject.Id), Type = configObjectType, }); if (dryRun) continue; var objectIndex = storedObjects.FindIndex(t => t.Id == configObject.Id); // Case: The object already exists, we update it: if (objectIndex > -1) { var existingObject = storedObjects[objectIndex]; configObject = configObject with { Num = existingObject.Num }; storedObjects[objectIndex] = (TClass)configObject; } // Case: The object does not exist, we have to add it else { if (nextConfigObjectNumSelection.TryIncrement(SETTINGS_MANAGER.ConfigurationData, IncrementType.POST) is { Success: true, UpdatedValue: var nextNum }) { // Case: Increment the next number was successful configObject = configObject with { Num = nextNum }; storedObjects.Add((TClass)configObject); } else { // Case: The next number could not be incremented, we use a random number random ??= new ThreadSafeRandom(); configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) }; storedObjects.Add((TClass)configObject); LOG.LogWarning($"The next number for the configuration object '{configObject.Name}' (id={configObject.Id}) could not be incremented. Using a random number instead."); } } } else LOG.LogWarning($"The {luaObjectTable} table at index {i} does not contain a valid chat template configuration."); } return true; } /// /// Cleans up configuration objects of a specified type that are no longer associated with any available plugin. /// /// The type of configuration object to clean up. /// The type of configuration object to process. /// A selection expression to retrieve the configuration objects from the main configuration. /// A list of currently available plugins. /// A list of all existing configuration objects. /// Returns true if the configuration was altered during cleanup; otherwise, false. public static bool CleanLeftOverConfigurationObjects( PluginConfigurationObjectType configObjectType, Expression>> configObjectSelection, IList availablePlugins, IList configObjectList) where TClass : IConfigurationObject { var configuredObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData); var leftOverObjects = new List(); foreach (var configuredObject in configuredObjects) { if(!configuredObject.IsEnterpriseConfiguration) continue; var configObjectSourcePluginId = configuredObject.EnterpriseConfigurationPluginId; if(configObjectSourcePluginId == Guid.Empty) continue; var templateSourcePlugin = availablePlugins.FirstOrDefault(plugin => plugin.Id == configObjectSourcePluginId); if(templateSourcePlugin is null) { LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is based on a plugin that is not available anymore. Removing the chat template from the settings."); leftOverObjects.Add(configuredObject); } if(!configObjectList.Any(configObject => configObject.Type == configObjectType && configObject.ConfigPluginId == configObjectSourcePluginId && configObject.Id.ToString() == configuredObject.Id)) { LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is not present in the configuration plugin anymore. Removing the chat template from the settings."); leftOverObjects.Add(configuredObject); } } // Remove collected items after enumeration to avoid modifying the collection during iteration: var wasConfigurationChanged = leftOverObjects.Count > 0; foreach (var item in leftOverObjects.Distinct()) configuredObjects.Remove(item); return wasConfigurationChanged; } }