Merge branch 'main' into custom-tokenizer

This commit is contained in:
PaulKoudelka committed 2026-04-24 12:07:22 +02:00
commit e3cb7e9734
165 files changed
+4739 -1738

No files matched your search

@@ -121,6 +121,26 @@ internal sealed class AssistantDropdown : StatefulAssistantComponentBase
#endregion
internal string ResolveDisplayText(string value)
{
if (string.IsNullOrWhiteSpace(value))
return this.Default.Display;
var item = this.GetRenderedItems().FirstOrDefault(item => string.Equals(item.Value, value, StringComparison.Ordinal));
return item?.Display ?? value;
}
private List<AssistantDropdownItem> GetRenderedItems()
{
if (string.IsNullOrWhiteSpace(this.Default.Value))
return this.Items;
if (this.Items.Any(item => string.Equals(item.Value, this.Default.Value, StringComparison.Ordinal)))
return this.Items;
return [this.Default, .. this.Items];
}
public IEnumerable<object> GetParsedDropdownValues()
{
foreach (var item in this.Items)
@@ -10,6 +10,11 @@ internal static class AssistantLuaConversion
/// </summary>
public static LuaTable CreateLuaArray(IEnumerable values) => CreateLuaArrayCore(values);
/// <summary>
/// Creates a readable string representation of a Lua table for debugging and inspection.
/// </summary>
public static string InspectTable(LuaTable table) => InspectTableCore(table, 0);
/// <summary>
/// Reads a Lua value into either a scalar .NET value or one of the structured assistant data model types.
/// Lua itself only exposes scalars and tables, so structured assistant types such as dropdown/list items
@@ -268,4 +273,47 @@ internal static class AssistantLuaConversion
return luaArray;
}
private static string InspectTableCore(LuaTable table, int depth)
{
if (depth > 8)
return "{ ... }";
var indent = new string(' ', depth * 2);
var childIndent = new string(' ', (depth + 1) * 2);
var builder = new System.Text.StringBuilder();
builder.AppendLine("{");
foreach (var entry in table)
{
builder.Append(childIndent);
builder.Append(FormatLuaValue(entry.Key));
builder.Append(" = ");
builder.AppendLine(FormatLuaValue(entry.Value, depth + 1));
}
builder.Append(indent);
builder.Append('}');
return builder.ToString();
}
private static string FormatLuaValue(LuaValue value, int depth = 0)
{
if (value.Type is LuaValueType.Nil)
return "nil";
if (value.TryRead<string>(out var stringValue))
return $"\"{stringValue.Replace("\\", "\\\\").Replace("\"", "\\\"")}\"";
if (value.TryRead<bool>(out var boolValue))
return boolValue ? "true" : "false";
if (value.TryRead<double>(out var doubleValue))
return doubleValue.ToString(System.Globalization.CultureInfo.InvariantCulture);
if (value.TryRead<LuaTable>(out var tableValue))
return InspectTableCore(tableValue, depth);
return value.ToString();
}
}
@@ -156,12 +156,17 @@ public sealed class AssistantState
{
if (component is INamedAssistantComponent named)
{
target[named.Name] = new LuaTable
var componentEntry = new LuaTable
{
["Type"] = Enum.GetName(component.Type) ?? string.Empty,
["Value"] = component is IStatefulAssistantComponent ? this.ReadValueForLua(named.Name) : LuaValue.Nil,
["Props"] = this.CreatePropsTable(component),
};
if (component is AssistantDropdown dropdown)
this.AddDropdownDisplay(componentEntry, dropdown, named.Name);
target[named.Name] = componentEntry;
}
if (component.Children.Count > 0)
@@ -218,6 +223,27 @@ public sealed class AssistantState
return table;
}
private void AddDropdownDisplay(LuaTable componentEntry, AssistantDropdown dropdown, string name)
{
if (dropdown.IsMultiselect)
{
if (!this.MultiSelect.TryGetValue(name, out var selectedValues))
return;
componentEntry["Display"] = AssistantLuaConversion.CreateLuaArray(
selectedValues
.OrderBy(static value => value, StringComparer.Ordinal)
.Select(dropdown.ResolveDisplayText));
return;
}
if (!this.SingleSelect.TryGetValue(name, out var selectedValue))
return;
componentEntry["Display"] = dropdown.ResolveDisplayText(selectedValue);
}
private static HashSet<string> ReadStringValues(LuaTable values)
{
var parsedValues = new HashSet<string>(StringComparer.Ordinal);
@@ -73,6 +73,8 @@ public static class PluginAssistantSecurityResolver
public static PluginAssistantSecurityState Resolve(SettingsManager settingsManager, PluginAssistants plugin)
{
var auditSettings = settingsManager.ConfigurationData.AssistantPluginAudit;
var enforceAuditBeforeActivation = auditSettings.RequireAuditBeforeActivation;
var isEnforcementDisabled = !enforceAuditBeforeActivation;
var currentHash = plugin.ComputeAuditHash();
var audit = settingsManager.ConfigurationData.AssistantPluginAudits.FirstOrDefault(x => x.PluginId == plugin.Id);
var hasAudit = audit is not null && audit.Level is not AssistantAuditLevel.UNKNOWN;
@@ -80,9 +82,9 @@ public static class PluginAssistantSecurityResolver
var hasHashMismatch = hasAudit && !hashMatches;
var isBelowMinimum = hashMatches && audit is not null && audit.Level < auditSettings.MinimumLevel;
var meetsMinimum = hashMatches && audit is not null && audit.Level >= auditSettings.MinimumLevel;
var requiresAudit = hasHashMismatch || auditSettings.RequireAuditBeforeActivation && !hasAudit;
var isBlocked = requiresAudit || isBelowMinimum && auditSettings.BlockActivationBelowMinimum;
var canOverride = isBelowMinimum && !auditSettings.BlockActivationBelowMinimum;
var requiresAudit = enforceAuditBeforeActivation && (hasHashMismatch || !hasAudit);
var isBlocked = requiresAudit || enforceAuditBeforeActivation && isBelowMinimum && auditSettings.BlockActivationBelowMinimum;
var canOverride = isBelowMinimum && (!auditSettings.BlockActivationBelowMinimum || isEnforcementDisabled);
var canUsePlugin = !isBlocked;
if (!hasAudit)
@@ -132,30 +134,32 @@ public static class PluginAssistantSecurityResolver
HasHashMismatch = true,
IsBelowMinimum = false,
MeetsMinimumLevel = false,
RequiresAudit = true,
IsBlocked = true,
RequiresAudit = requiresAudit,
IsBlocked = isBlocked,
CanOverride = false,
CanActivatePlugin = false,
CanStartAssistant = false,
CanActivatePlugin = !isBlocked,
CanStartAssistant = !isBlocked,
AuditLabel = TB("Unknown"),
AuditColor = AssistantAuditLevel.UNKNOWN.GetColor(),
AuditIcon = AssistantAuditLevel.UNKNOWN.GetIcon(),
AvailabilityLabel = GetAvailabilityLabel(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
AvailabilityColor = GetAvailabilityColor(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
AvailabilityIcon = GetAvailabilityIcon(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
StatusLabel = GetAvailabilityLabel(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
BadgeIcon = GetSecurityBadgeIcon(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
Headline = TB("This assistant is locked until it is audited again."),
Description = TB("The plugin code changed after the last security audit. The stored result no longer matches the current code, so this assistant plugin must be audited again before it may be enabled or used."),
StatusColor = GetAvailabilityColor(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
StatusIcon = GetAvailabilityIcon(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
AvailabilityLabel = GetAvailabilityLabel(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
AvailabilityColor = GetAvailabilityColor(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
AvailabilityIcon = GetAvailabilityIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
StatusLabel = GetAvailabilityLabel(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
BadgeIcon = GetSecurityBadgeIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
Headline = requiresAudit ? TB("This assistant is locked until it is audited again.") : TB("This assistant changed after its last audit."),
Description = requiresAudit
? TB("The plugin code changed after the last security audit. The stored result no longer matches the current code, so this assistant plugin must be audited again before it may be enabled or used.")
: TB("The plugin code changed after the last security audit. Audit enforcement is currently disabled, so this assistant plugin can still be enabled or used."),
StatusColor = GetAvailabilityColor(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
StatusIcon = GetAvailabilityIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
ActionLabel = TB("Run Security Check Again"),
};
}
if (isBelowMinimum)
{
var isBlockedByMinimum = auditSettings.BlockActivationBelowMinimum;
var isBlockedByMinimum = enforceAuditBeforeActivation && auditSettings.BlockActivationBelowMinimum;
var auditLevel = audit!.Level;
return new PluginAssistantSecurityState
@@ -181,10 +185,16 @@ public static class PluginAssistantSecurityResolver
AvailabilityIcon = GetAvailabilityIcon(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
StatusLabel = GetAvailabilityLabel(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
BadgeIcon = GetSecurityBadgeIcon(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
Headline = isBlockedByMinimum ? TB("This assistant is currently locked.") : TB("This assistant can still be used because your settings allow it."),
Headline = isBlockedByMinimum
? TB("This assistant is currently locked.")
: isEnforcementDisabled
? TB("This assistant can still be used because audit enforcement is disabled.")
: TB("This assistant can still be used because your settings allow it."),
Description = isBlockedByMinimum
? string.Format(TB("The current audit result '{0}' is below your required minimum level '{1}'. Your security settings therefore block this assistant plugin."), auditLevel.GetName(), auditSettings.MinimumLevel.GetName())
: string.Format(TB("The current audit result is '{0}', which is below your required minimum level '{1}'. Your settings still allow manual activation, but the assistant keeps this security status and should be reviewed carefully."), auditLevel.GetName(), auditSettings.MinimumLevel.GetName()),
: isEnforcementDisabled
? string.Format(TB("The current audit result is '{0}', which is below your required minimum level '{1}'. Audit enforcement is currently disabled, so this assistant plugin can still be enabled or used."), auditLevel.GetName(), auditSettings.MinimumLevel.GetName())
: string.Format(TB("The current audit result is '{0}', which is below your required minimum level '{1}'. Your settings still allow manual activation, but the assistant keeps this security status and should be reviewed carefully."), auditLevel.GetName(), auditSettings.MinimumLevel.GetName()),
StatusColor = GetAvailabilityColor(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
StatusIcon = GetAvailabilityIcon(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
ActionLabel = TB("Open Security Check"),
@@ -497,7 +497,6 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
private void RegisterLuaHelpers()
{
this.State.Environment["LogInfo"] = new LuaFunction((context, _) =>
{
if (context.ArgumentCount == 0) return new(0);
@@ -559,6 +558,15 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
var timestamp = DateTime.UtcNow.ToString("o");
return new(context.Return(timestamp));
});
this.State.Environment["InspectTable"] = new LuaFunction((context, _) =>
{
if (context.ArgumentCount == 0)
return new(context.Return("{}"));
var table = context.GetArgument<LuaTable>(0);
return new(context.Return(AssistantLuaConversion.InspectTable(table)));
});
}
private static void InitializeState(IEnumerable<IAssistantComponent> components, AssistantState state)
@@ -1,4 +1,5 @@
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
using Lua;
@@ -12,12 +13,18 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginConfiguration));
private List<PluginConfigurationObject> configObjects = [];
private List<DataMandatoryInfo> mandatoryInfos = [];
/// <summary>
/// The list of configuration objects. Configuration objects are, e.g., providers or chat templates.
/// </summary>
public IEnumerable<PluginConfigurationObject> ConfigObjects => this.configObjects;
/// <summary>
/// The list of mandatory infos provided by this configuration plugin.
/// </summary>
public IReadOnlyList<DataMandatoryInfo> MandatoryInfos => this.mandatoryInfos;
/// <summary>
/// True/false when explicitly configured in the plugin, otherwise null.
/// </summary>
@@ -93,6 +100,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
private bool TryProcessConfiguration(bool dryRun, out string message)
{
this.configObjects.Clear();
this.mandatoryInfos.Clear();
// Ensure that the main CONFIG table exists and is a valid Lua table:
if (!this.State.Environment["CONFIG"].TryRead<LuaTable>(out var mainTable))
@@ -152,6 +160,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// 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 mandatory infos:
this.TryReadMandatoryInfos(mainTable);
// Config: preselected provider?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
@@ -165,4 +176,25 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
message = string.Empty;
return true;
}
private void TryReadMandatoryInfos(LuaTable mainTable)
{
if (!mainTable.TryGetValue("MANDATORY_INFOS", out var mandatoryInfosValue) || !mandatoryInfosValue.TryRead<LuaTable>(out var mandatoryInfosTable))
return;
for (var i = 1; i <= mandatoryInfosTable.ArrayLength; i++)
{
var luaMandatoryInfoValue = mandatoryInfosTable[i];
if (!luaMandatoryInfoValue.TryRead<LuaTable>(out var luaMandatoryInfoTable))
{
LOG.LogWarning("The table 'MANDATORY_INFOS' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", i, this.Id);
continue;
}
if (DataMandatoryInfo.TryParseConfiguration(i, luaMandatoryInfoTable, this.Id, out var mandatoryInfo))
this.mandatoryInfos.Add(mandatoryInfo);
else
LOG.LogWarning("The table 'MANDATORY_INFOS' entry at index {Index} does not contain a valid mandatory info (config plugin id: {ConfigPluginId}).", i, this.Id);
}
}
}
@@ -185,6 +185,10 @@ public static partial class PluginFactory
// Check document analysis policies:
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, configObjectList))
wasConfigurationChanged = true;
// Check left-over mandatory info acceptances:
if (SETTINGS_MANAGER.ConfigurationData.MandatoryInformation.RemoveLeftOverAcceptances(GetMandatoryInfos()))
wasConfigurationChanged = true;
// Check for a preselected provider:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreselectedProvider, AVAILABLE_PLUGINS))
@@ -1,4 +1,5 @@
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
namespace AIStudio.Tools.PluginSystem;
@@ -127,4 +128,12 @@ public static partial class PluginFactory
HOT_RELOAD_WATCHER.Dispose();
}
public static IReadOnlyList<DataMandatoryInfo> GetMandatoryInfos()
{
return RUNNING_PLUGINS
.OfType<PluginConfiguration>()
.SelectMany(plugin => plugin.MandatoryInfos)
.ToList();
}
}