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