mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 12:33:37 +00:00
Resolved 29 conflicting files. The notable decisions: Confidence: main's tool-calling gate (RequiredProviderConfidence) and this branch's local-RAG gate (DataConfidenceLevel) turned out to be the same rule on the same axis, so they are now one field. Both tool results and data sources raise it through RequireProviderConfidence(). The gate checks the level strictly and no longer exempts providers trusted by configuration: TrustedProviderIds is documented as applying to data-source security checks only, and organizations set confidence through DataConfidence .CustomConfidenceScheme instead. The security axis (DataSecurity, ERI, IsTrustedForDataSourceSecurityChecks) is unchanged. Provider creation: main's CreateProvider signature won (hfEndpointKind, capabilityOverrides, no model parameter); tokenizerPath was added to it and is set for every provider, including the new Hetzner, IONOS and LiteLLM. Provider and EmbeddingProvider combine the record parameters, Lua parsing and Lua serialization of both sides. File types: main's hierarchy (ODT leaf, WORD parent, PowerPoint without the legacy .ppt, TABULAR instead of DELIMITED_TABLE) plus this branch's SPREADSHEET parent with ODS and the xlsm/xlsb/xla/xlam extensions, which the runtime already reads. Both sides had added a conflicting HTML filter; the reading family keeps the name, and the export path uses a narrow HTML_DOCUMENT, following the existing LATEX/TEX split. Runtime: main's file_data.rs is the base, including the prompt-injection sanitizer and the extraction routes. Token counting and chunk segmentation moved into take_released, so they act on the text the filter has released rather than on text it is still holding. A failed count is logged and left out instead of ending the extraction, because the app counts such a segment itself. Data sources: the participating-provider checks of this branch are kept, and main's GetAllowedDataSources overload now builds on them. DirectChatService resolves the launched chat's data source options before the check, so filter and chat see the same options. .NET and Rust both build clean; I18N regenerated to 4060 keys.
566 lines
30 KiB
C#
566 lines
30 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq.Expressions;
|
|
|
|
using AIStudio.Settings;
|
|
using AIStudio.Settings.DataModel;
|
|
using AIStudio.Tools.Services;
|
|
|
|
using Lua;
|
|
|
|
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
|
|
{
|
|
private static RustService RustService => Program.SERVICE_PROVIDER.GetRequiredService<RustService>();
|
|
private static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
|
private static ThreadSafeRandom Rng => Program.SERVICE_PROVIDER.GetRequiredService<ThreadSafeRandom>();
|
|
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger<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;
|
|
|
|
/// <summary>
|
|
/// The name of the configuration object, e.g. the name of a provider.
|
|
/// </summary>
|
|
public string Name { get; init; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Where this configuration object sends data to: the host of a self-hosted provider or data
|
|
/// source, or the name of the cloud provider. Empty for objects without a destination, such as
|
|
/// chat templates or profiles.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// We keep this next to the object metadata so the import preview can tell users where a
|
|
/// configuration would send their prompts before its providers are stored.
|
|
/// </remarks>
|
|
public string Endpoint { get; private init; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Determines the destination of a configuration object for the import preview.
|
|
/// </summary>
|
|
private static string DescribeEndpoint(IConfigurationObject configObject) => configObject switch
|
|
{
|
|
Settings.Provider { IsSelfHosted: true } provider => provider.Hostname,
|
|
Settings.Provider provider => Provider.LLMProvidersExtensions.ToName(provider.UsedLLMProvider),
|
|
|
|
EmbeddingProvider { IsSelfHosted: true } embeddingProvider => embeddingProvider.Hostname,
|
|
EmbeddingProvider embeddingProvider => Provider.LLMProvidersExtensions.ToName(embeddingProvider.UsedLLMProvider),
|
|
|
|
TranscriptionProvider { IsSelfHosted: true } transcriptionProvider => transcriptionProvider.Hostname,
|
|
TranscriptionProvider transcriptionProvider => Provider.LLMProvidersExtensions.ToName(transcriptionProvider.UsedLLMProvider),
|
|
|
|
DataSourceERI_V1 dataSource => dataSource.Hostname,
|
|
|
|
_ => string.Empty,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Parses Lua table entries into configuration objects of the specified type, populating the
|
|
/// 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>
|
|
/// <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="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>
|
|
/// <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
|
|
{
|
|
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;
|
|
}
|
|
|
|
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);
|
|
return false;
|
|
}
|
|
|
|
var localSettingsManager = SettingsManagerAccess;
|
|
var storedObjects = configObjectSelection.Compile()(localSettingsManager.ConfigurationData);
|
|
var numberObjects = luaTable.ArrayLength;
|
|
ThreadSafeRandom? random = null;
|
|
for (var i = 1; i <= numberObjects; i++)
|
|
{
|
|
var luaObjectTableValue = luaTable[i];
|
|
if (!luaObjectTableValue.TryRead<LuaTable>(out var luaObjectTable))
|
|
{
|
|
LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", luaTableName, i, configPluginId);
|
|
continue;
|
|
}
|
|
|
|
var (wasParsingSuccessful, configObject) = configObjectType switch
|
|
{
|
|
PluginConfigurationObjectType.LLM_PROVIDER => (Settings.Provider.TryParseProviderTable(i, luaObjectTable, configPluginId, pluginPath, 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, out var configurationObject) && configurationObject != TranscriptionProvider.NONE, configurationObject),
|
|
PluginConfigurationObjectType.EMBEDDING_PROVIDER => (EmbeddingProvider.TryParseEmbeddingProviderTable(i, luaObjectTable, configPluginId, pluginPath, 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)
|
|
{
|
|
// Store it in the config object list:
|
|
configObjects.Add(new()
|
|
{
|
|
ConfigPluginId = configPluginId,
|
|
Id = Guid.Parse(configObject.Id),
|
|
Type = configObjectType,
|
|
Name = configObject.Name,
|
|
Endpoint = DescribeEndpoint(configObject),
|
|
});
|
|
|
|
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];
|
|
if (!MayReplaceConfigurationObject(existingObject, configPluginId))
|
|
continue;
|
|
|
|
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(localSettingsManager.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 ??= Rng;
|
|
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 '{ConfigObjectName}' (id={ConfigObjectId}) could not be incremented. Using a random number instead (config plugin id: {ConfigPluginId}).", configObject.Name, configObject.Id, configPluginId);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} does not contain a valid configuration object (type={ConfigObjectType}, config plugin id: {ConfigPluginId}).", luaTableName, i, configObjectType, configPluginId);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Tokenizer synchronization needs indexed access to update enterprise-managed providers in place.")]
|
|
public static async Task<bool> SyncManagedTokenizersAsync(Guid configPluginId, string pluginPath)
|
|
{
|
|
var wasConfigurationChanged = false;
|
|
var localSettingsManager = SettingsManagerAccess;
|
|
|
|
for (var i = 0; i < localSettingsManager.ConfigurationData.Providers.Count; i++)
|
|
{
|
|
var provider = localSettingsManager.ConfigurationData.Providers[i];
|
|
if (!provider.IsEnterpriseConfiguration || provider.EnterpriseConfigurationPluginId != configPluginId)
|
|
continue;
|
|
|
|
var syncedProvider = await SyncProviderTokenizerAsync(provider, pluginPath);
|
|
if (syncedProvider == provider)
|
|
continue;
|
|
|
|
localSettingsManager.ConfigurationData.Providers[i] = syncedProvider;
|
|
wasConfigurationChanged = true;
|
|
}
|
|
|
|
for (var i = 0; i < localSettingsManager.ConfigurationData.EmbeddingProviders.Count; i++)
|
|
{
|
|
var provider = localSettingsManager.ConfigurationData.EmbeddingProviders[i];
|
|
if (!provider.IsEnterpriseConfiguration || provider.EnterpriseConfigurationPluginId != configPluginId)
|
|
continue;
|
|
|
|
var syncedProvider = await SyncEmbeddingTokenizerAsync(provider, pluginPath);
|
|
if (syncedProvider == provider)
|
|
continue;
|
|
|
|
localSettingsManager.ConfigurationData.EmbeddingProviders[i] = syncedProvider;
|
|
wasConfigurationChanged = true;
|
|
}
|
|
|
|
return wasConfigurationChanged;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses configured data sources from a configuration plugin.
|
|
/// </summary>
|
|
/// <param name="mainTable">The Lua table containing entries to parse into data sources.</param>
|
|
/// <param name="configPluginId">The unique identifier of the plugin associated with the data sources.</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.</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)
|
|
{
|
|
const string LUA_TABLE_NAME = "DATA_SOURCES";
|
|
if (!mainTable.TryGetValue(LUA_TABLE_NAME, 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}).", LUA_TABLE_NAME, configPluginId);
|
|
return false;
|
|
}
|
|
|
|
var localSettingsManager = SettingsManagerAccess;
|
|
var storedObjects = localSettingsManager.ConfigurationData.DataSources;
|
|
var numberObjects = luaTable.ArrayLength;
|
|
ThreadSafeRandom? random = null;
|
|
for (var i = 1; i <= numberObjects; i++)
|
|
{
|
|
var luaObjectTableValue = luaTable[i];
|
|
if (!luaObjectTableValue.TryRead<LuaTable>(out var luaObjectTable))
|
|
{
|
|
LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, i, configPluginId);
|
|
continue;
|
|
}
|
|
|
|
if (!DataSourceERI_V1.TryParseConfiguration(i, luaObjectTable, configPluginId, out var configObject))
|
|
{
|
|
LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} does not contain a valid data source (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, i, configPluginId);
|
|
continue;
|
|
}
|
|
|
|
configObjects.Add(new()
|
|
{
|
|
ConfigPluginId = configPluginId,
|
|
Id = Guid.Parse(configObject.Id),
|
|
Type = PluginConfigurationObjectType.DATA_SOURCE,
|
|
Name = configObject.Name,
|
|
Endpoint = DescribeEndpoint(configObject),
|
|
});
|
|
|
|
if (dryRun)
|
|
continue;
|
|
|
|
var objectIndex = storedObjects.FindIndex(t => t.Id == configObject.Id);
|
|
if (objectIndex > -1)
|
|
{
|
|
var existingObject = storedObjects[objectIndex];
|
|
if (!MayReplaceConfigurationObject(existingObject, configPluginId))
|
|
continue;
|
|
|
|
configObject = configObject with { Num = existingObject.Num };
|
|
storedObjects[objectIndex] = configObject;
|
|
}
|
|
else
|
|
{
|
|
if (IncrementDataSourceNum(localSettingsManager.ConfigurationData) is { Success: true, UpdatedValue: var nextNum })
|
|
{
|
|
configObject = configObject with { Num = nextNum };
|
|
storedObjects.Add(configObject);
|
|
}
|
|
else
|
|
{
|
|
random ??= Rng;
|
|
configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) };
|
|
storedObjects.Add(configObject);
|
|
LOG.LogWarning("The next number for the data source '{ConfigObjectName}' (id={ConfigObjectId}) could not be incremented. Using a random number instead (config plugin id: {ConfigPluginId}).", configObject.Name, configObject.Id, configPluginId);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
|
|
static IncrementResult<uint> IncrementDataSourceNum(Data data)
|
|
{
|
|
return ((Expression<Func<Data, uint>>)(x => x.NextDataSourceNum)).TryIncrement(data, IncrementType.POST);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether a configuration plugin may replace a stored configuration object, or whether
|
|
/// that object belongs to the IT department of an organization.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Configuration objects are matched by their ID alone. Without this check, a local configuration
|
|
/// plugin could claim the ID of an object an organization deployed and replace it, e.g. to point
|
|
/// a self-hosted LLM provider at a different host.<br/><br/>
|
|
/// Between two configuration plugins of the same organization, we do not interfere: both belong
|
|
/// to the IT department, so the one processed later wins, as before.
|
|
/// </remarks>
|
|
/// <param name="existingObject">The configuration object which is stored already.</param>
|
|
/// <param name="configPluginId">The configuration plugin which wants to replace that object.</param>
|
|
/// <returns>True when the plugin may replace the object, otherwise false.</returns>
|
|
private static bool MayReplaceConfigurationObject(IConfigurationObject existingObject, Guid configPluginId)
|
|
{
|
|
if (!existingObject.IsEnterpriseConfiguration || existingObject.EnterpriseConfigurationPluginId == configPluginId)
|
|
return true;
|
|
|
|
if (!PluginFactory.IsOrganizationConfigurationPlugin(existingObject.EnterpriseConfigurationPluginId))
|
|
return true;
|
|
|
|
if (PluginFactory.IsOrganizationConfigurationPlugin(configPluginId))
|
|
return true;
|
|
|
|
LOG.LogWarning("The configuration plugin '{ConfigPluginId}' tried to replace the object '{ConfigObjectName}' (id={ConfigObjectId}), which belongs to the configuration plugin '{OwningConfigPluginId}' of your organization. Ignoring the attempt: configurations deployed by your organization's IT take precedence.", configPluginId, existingObject.Name, existingObject.Id, existingObject.EnterpriseConfigurationPluginId);
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleans up configuration objects of a specified type that are no longer associated with any available plugin.
|
|
/// </summary>
|
|
/// <typeparam name="TClass">The type of configuration object to clean up.</typeparam>
|
|
/// <param name="configObjectType">The type of configuration object to process.</param>
|
|
/// <param name="configObjectSelection">A selection expression to retrieve the configuration objects from the main configuration.</param>
|
|
/// <param name="availablePlugins">A list of currently available plugins.</param>
|
|
/// <param name="deployedEnterpriseConfigPluginIds">
|
|
/// The IDs of the configuration plugins which an organization deployed on this machine, including
|
|
/// those which could not be loaded. Objects of a deployed plugin are never removed, because the
|
|
/// plugin was not removed either.
|
|
/// </param>
|
|
/// <param name="configObjectList">A list of all existing configuration objects.</param>
|
|
/// <param name="secretStoreType">An optional parameter specifying the type of secret store to use for deleting associated API keys from the OS keyring, if applicable.</param>
|
|
/// <param name="deleteSecret">When true, delete the associated non-API-key secret from the OS keyring.</param>
|
|
/// <returns>Returns true if the configuration was altered during cleanup; otherwise, false.</returns>
|
|
public static async Task<bool> CleanLeftOverConfigurationObjects<TClass>(
|
|
PluginConfigurationObjectType configObjectType,
|
|
Expression<Func<Data, List<TClass>>> configObjectSelection,
|
|
IList<IAvailablePlugin> availablePlugins,
|
|
IReadOnlySet<Guid> deployedEnterpriseConfigPluginIds,
|
|
IList<PluginConfigurationObject> configObjectList,
|
|
SecretStoreType? secretStoreType = null,
|
|
bool deleteSecret = false) where TClass : IConfigurationObject
|
|
{
|
|
var localSettingsManager = SettingsManagerAccess;
|
|
var configuredObjects = configObjectSelection.Compile()(localSettingsManager.ConfigurationData);
|
|
var leftOverObjects = new List<TClass>();
|
|
foreach (var configuredObject in configuredObjects)
|
|
{
|
|
// Only process objects that are based on enterprise configuration plugins (aka configuration plugins),
|
|
// as only those can be left over after a plugin was removed:
|
|
if(!configuredObject.IsEnterpriseConfiguration)
|
|
continue;
|
|
|
|
// From what plugin is this configuration object coming from?
|
|
var configObjectSourcePluginId = configuredObject.EnterpriseConfigurationPluginId;
|
|
if(configObjectSourcePluginId == Guid.Empty)
|
|
continue;
|
|
|
|
//
|
|
// Is the source plugin deployed, but could not be loaded? Then we must not touch any of
|
|
// its objects. The plugin was not removed, it is broken: it might be invalid Lua code,
|
|
// a missing `plugin.lua`, or an incomplete download. Removing the objects would delete
|
|
// the organization's providers and data sources, including their secrets, although the
|
|
// organization still manages this AI Studio instance:
|
|
//
|
|
if(deployedEnterpriseConfigPluginIds.Contains(configObjectSourcePluginId) && availablePlugins.All(plugin => plugin.Id != configObjectSourcePluginId))
|
|
continue;
|
|
|
|
// Is the source plugin still available? If not, we can be pretty sure that this configuration object is left
|
|
// over and should be removed:
|
|
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 this object from the settings.");
|
|
leftOverObjects.Add(configuredObject);
|
|
}
|
|
|
|
// Is the configuration object still present in the configuration plugin? If not, it is also left over and should be removed:
|
|
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 object 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())
|
|
{
|
|
if (item is Settings.Provider provider)
|
|
{
|
|
var deleteTokenizerResult = await RustService.DeleteTokenizer(TokenizerModelId.ForProvider(provider));
|
|
if (!deleteTokenizerResult.Success)
|
|
LOG.LogWarning("Failed to delete tokenizer for removed enterprise provider '{ProviderName}': {Issue}", provider.InstanceName, deleteTokenizerResult.Message);
|
|
}
|
|
else if (item is EmbeddingProvider embeddingProvider)
|
|
{
|
|
var deleteTokenizerResult = await RustService.DeleteTokenizer(TokenizerModelId.ForEmbeddingProvider(embeddingProvider));
|
|
if (!deleteTokenizerResult.Success)
|
|
LOG.LogWarning("Failed to delete tokenizer for removed enterprise embedding provider '{ProviderName}': {Issue}", embeddingProvider.Name, deleteTokenizerResult.Message);
|
|
}
|
|
|
|
configuredObjects.Remove(item);
|
|
|
|
// Delete the API key from the OS keyring if the removed object has one:
|
|
if(deleteSecret && item is ISecretId regularSecretId)
|
|
{
|
|
var deleteResult = await RustService.DeleteSecret(regularSecretId, secretStoreType ?? SecretStoreType.DATA_SOURCE);
|
|
if (deleteResult.Success)
|
|
LOG.LogInformation($"Successfully deleted secret for removed enterprise object '{item.Name}' from the OS keyring.");
|
|
else
|
|
LOG.LogWarning($"Failed to delete secret for removed enterprise object '{item.Name}' from the OS keyring: {deleteResult.Issue}");
|
|
}
|
|
else if(item is IUserProvidedAPIKey { AllowUserProvidedAPIKey: true })
|
|
{
|
|
// The user manages their own key for this provider. Keep it in the OS keyring
|
|
// in case the organization's configuration comes back later, instead of forcing
|
|
// the user to re-enter it:
|
|
LOG.LogInformation($"Preserving the user-provided API key for removed enterprise provider '{item.Name}' in the OS keyring.");
|
|
}
|
|
else if(secretStoreType is not null && item is ISecretId secretId)
|
|
{
|
|
var deleteResult = await RustService.DeleteAPIKey(secretId, secretStoreType.Value);
|
|
if (deleteResult.Success)
|
|
LOG.LogInformation($"Successfully deleted API key for removed enterprise provider '{item.Name}' from the OS keyring.");
|
|
else
|
|
LOG.LogWarning($"Failed to delete API key for removed enterprise provider '{item.Name}' from the OS keyring: {deleteResult.Issue}");
|
|
}
|
|
}
|
|
|
|
return wasConfigurationChanged;
|
|
}
|
|
|
|
private static async Task<Settings.Provider> SyncProviderTokenizerAsync(Settings.Provider provider, string pluginPath)
|
|
{
|
|
var syncedTokenizerPath = await SyncTokenizerAsync(
|
|
provider.TokenizerPath,
|
|
pluginPath,
|
|
TokenizerModelId.ForProvider(provider),
|
|
$"provider '{provider.InstanceName}'");
|
|
|
|
return provider with { TokenizerPath = syncedTokenizerPath };
|
|
}
|
|
|
|
private static async Task<EmbeddingProvider> SyncEmbeddingTokenizerAsync(EmbeddingProvider provider, string pluginPath)
|
|
{
|
|
var syncedTokenizerPath = await SyncTokenizerAsync(
|
|
provider.TokenizerPath,
|
|
pluginPath,
|
|
TokenizerModelId.ForEmbeddingProvider(provider),
|
|
$"embedding provider '{provider.Name}'");
|
|
|
|
return provider with { TokenizerPath = syncedTokenizerPath };
|
|
}
|
|
|
|
private static async Task<string> SyncTokenizerAsync(string configuredTokenizerPath, string pluginPath, string modelId, string logName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(configuredTokenizerPath))
|
|
{
|
|
var deleteResult = await RustService.DeleteTokenizer(modelId);
|
|
if (!deleteResult.Success)
|
|
LOG.LogWarning("Failed to delete tokenizer for {LogName}: {Issue}", logName, deleteResult.Message);
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
var resolvedPath = ResolvePluginTokenizerPath(configuredTokenizerPath, pluginPath);
|
|
if (resolvedPath is null)
|
|
{
|
|
var deleteResult = await RustService.DeleteTokenizer(modelId);
|
|
if (!deleteResult.Success)
|
|
LOG.LogWarning("Failed to delete tokenizer after invalid path for {LogName}: {Issue}", logName, deleteResult.Message);
|
|
|
|
LOG.LogWarning("The configured tokenizer path '{TokenizerPath}' for {LogName} is invalid. The tokenizer path must stay within the plugin directory '{PluginPath}'.", configuredTokenizerPath, logName, pluginPath);
|
|
return string.Empty;
|
|
}
|
|
|
|
var validateResult = await RustService.ValidateTokenizer(resolvedPath);
|
|
if (!validateResult.Success)
|
|
{
|
|
var deleteResult = await RustService.DeleteTokenizer(modelId);
|
|
if (!deleteResult.Success)
|
|
LOG.LogWarning("Failed to delete tokenizer after validation failure for {LogName}: {Issue}", logName, deleteResult.Message);
|
|
|
|
LOG.LogWarning("The configured tokenizer for {LogName} is invalid. Path='{TokenizerPath}', issue='{Issue}'", logName, resolvedPath, validateResult.Message);
|
|
return string.Empty;
|
|
}
|
|
|
|
var storeResult = await RustService.StoreTokenizer(modelId, resolvedPath);
|
|
if (!storeResult.Success)
|
|
{
|
|
LOG.LogWarning("Failed to store tokenizer for {LogName}. Path='{TokenizerPath}', issue='{Issue}'", logName, resolvedPath, storeResult.Message);
|
|
return string.Empty;
|
|
}
|
|
|
|
return storeResult.StoredPath;
|
|
}
|
|
|
|
private static string? ResolvePluginTokenizerPath(string configuredTokenizerPath, string pluginPath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(pluginPath))
|
|
return null;
|
|
|
|
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;
|
|
}
|
|
}
|