WIP: Implementing a parser for the lua data structure

This commit is contained in:
krut_ni 2025-07-22 20:15:36 +02:00
parent f5c475f354
commit cef643cd7f
4 changed files with 293 additions and 33 deletions

View File

@ -40,45 +40,45 @@ IS_MAINTAINED = true
DEPRECATION_MESSAGE = "" DEPRECATION_MESSAGE = ""
ASSISTANT = { ASSISTANT = {
Name = "Grammatik- und Rechtschreibprüfung", ["Title"] = "Grammatik- und Rechtschreibprüfung",
Description = "Grammatik und Rechtschreibung eines Textes überprüfen.", ["Description"] = "Grammatik und Rechtschreibung eines Textes überprüfen.",
UI = { ["UI"] = {
Type = "Form", ["Type"] = "FORM",
Children = { ["Children"] = {
{ {
Type = "TextArea", ["Type"] = "TEXT_AREA",
Props = { ["Props"] = {
Name = "input", ["Name"] = "input",
Label = "Ihre Eingabe zur Überprüfung" ["Label"] = "Ihre Eingabe zur Überprüfung"
} }
}, },
{ {
Type = "Dropdown", ["Type"] = "DROPDOWN",
ValueType = "string", ["ValueType"] = "string",
Default = { Value = "", Display = "Sprache nicht angeben."} ["Default"] = { ["Value"] = "", ["Display"] = "Sprache nicht angeben." },
Items = { ["Items"] = {
{ Value = "de-DE", Display = "Deutsch" }, { ["Value"] = "de-DE", ["Display"] = "Deutsch" },
{ Value = "en-UK", Display = "Englisch (UK)" }, { ["Value"] = "en-UK", ["Display"] = "Englisch (UK)" },
{ Value = "en-US", Display = "Englisch (US)" }, { ["Value"] = "en-US", ["Display"] = "Englisch (US)" },
}, },
Props = { ["Props"] = {
Name = "language", ["Name"] = "language",
Label = "Sprache", ["Label"] = "Sprache",
} }
}, },
{ {
Type = "ProviderSelection", ["Type"] = "PROVIDER_SELECTION",
Props = { ["Props"] = {
Name = "Anbieter", ["Name"] = "Anbieter",
Label = "LLM auswählen" ["Label"] = "LLM auswählen"
} }
}, },
{ {
Type = "Button", ["Type"] = "BUTTON",
Props = { ["Props"] = {
Name = "submit", ["Name"] = "submit",
Text = "Korrekturlesen", ["Text"] = "Korrekturlesen",
Action = "OnSubmit" ["Action"] = "OnSubmit"
} }
}, },
} }

View File

@ -0,0 +1,29 @@
namespace AIStudio.Tools.PluginSystem.Assistants.DataModel;
public static class ComponentPropSpecs
{
public static readonly IReadOnlyDictionary<AssistantUiCompontentType, PropSpec> SPECS =
new Dictionary<AssistantUiCompontentType, PropSpec>
{
[AssistantUiCompontentType.FORM] = new(
required: ["Children"],
optional: []
),
[AssistantUiCompontentType.TEXT_AREA] = new(
required: ["Name", "Label"],
optional: []
),
[AssistantUiCompontentType.BUTTON] = new(
required: ["Name", "Text", "Action"],
optional: []
),
[AssistantUiCompontentType.DROPDOWN] = new(
required: ["Name", "Label", "Default", "Items"],
optional: []
),
[AssistantUiCompontentType.PROVIDER_SELECTION] = new(
required: ["Name", "Label"],
optional: []
),
};
}

View File

@ -0,0 +1,7 @@
namespace AIStudio.Tools.PluginSystem.Assistants.DataModel;
public class PropSpec(IEnumerable<string> required, IEnumerable<string> optional)
{
public IReadOnlyList<string> Required { get; } = required.ToArray();
public IReadOnlyList<string> Optional { get; } = optional.ToArray();
}

View File

@ -1,19 +1,243 @@
using Lua; using System.Xml.XPath;
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
using Lua;
namespace AIStudio.Tools.PluginSystem.Assistants; namespace AIStudio.Tools.PluginSystem.Assistants;
public sealed class PluginAssistants : PluginBase public sealed class PluginAssistants : PluginBase
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginAssistants).Namespace, nameof(PluginAssistants)); private static string TB(string fallbackEN) =>
I18N.I.T(fallbackEN, typeof(PluginAssistants).Namespace, nameof(PluginAssistants));
private static readonly ILogger<PluginAssistants> LOGGER = Program.LOGGER_FACTORY.CreateLogger<PluginAssistants>(); private static readonly ILogger<PluginAssistants> LOGGER = Program.LOGGER_FACTORY.CreateLogger<PluginAssistants>();
public string AssistantTitle { get; set;} = string.Empty; public AssistantForm RootComponent { get; set; }
private string AssistantDescription {get; set;} = string.Empty; public string AssistantTitle { get; set; } = string.Empty;
private string AssistantDescription { get; set; } = string.Empty;
public PluginAssistants(bool isInternal, LuaState state, PluginType type) : base(isInternal, state, type) public PluginAssistants(bool isInternal, LuaState state, PluginType type) : base(isInternal, state, type)
{ {
}
/// <summary>
/// Tries to parse the assistant table into our internal assistant render tree data model. It follows this process:
/// <list type="number">
/// <item><description>ASSISTANT → Title/Description → UI</description></item>
/// <item><description>UI: Root element → required Children → Components</description></item>
/// <item><description>Components: Type → Props → Children (recursively)</description></item>
/// </list>
/// </summary>
/// <param name="message">The error message, when parameters from the table could not be read.</param>
/// <returns>True, when the assistant could be read successfully indicating the data model is populated.</returns>
private bool TryProcessAssistant(out string message)
{
message = string.Empty;
// Ensure that the main ASSISTANT table exists and is a valid Lua table:
if (!this.state.Environment["ASSISTANT"].TryRead<LuaTable>(out var assistantTable))
{
message = TB("The ASSISTANT table does not exist or is not a valid table.");
return false;
}
if (!assistantTable.TryGetValue("Title", out var assistantTitleValue) ||
!assistantTitleValue.TryRead<string>(out var assistantTitle))
{
message = TB("The ASSISTANT table does not contain a valid title.");
return false;
}
if (!assistantTable.TryGetValue("Description", out var assistantDescriptionValue) ||
!assistantDescriptionValue.TryRead<string>(out var assistantDescription))
{
message = TB("The ASSISTANT table does not contain a valid description.");
return false;
}
this.AssistantTitle = assistantTitle;
this.AssistantDescription = assistantDescription;
// Ensure that the UI table exists nested in the ASSISTANT table and is a valid Lua table:
if (!assistantTable.TryGetValue("UI", out var uiVal) || !uiVal.TryRead<LuaTable>(out var uiTable))
{
message = TB("The ASSISTANT table does not contain a valid UI section.");
return false;
}
if (!this.TryReadRenderTree(uiTable, out var rootComponent))
{
message = TB("Failed to parse the UI render tree.");
return false;
}
this.RootComponent = (AssistantForm)rootComponent;
return true;
}
/// <summary>
/// Parses the root <c>FORM</c> component and start to parse its required children (main ui components)
/// </summary>
/// <param name="uiTable">The <c>LuaTable</c> containing all UI components</param>
/// <param name="root">Outputs the root <c>FORM</c> component, if the parsing is successful. </param>
/// <returns>True, when the UI table could be read successfully.</returns>
private bool TryReadRenderTree(LuaTable uiTable, out IAssistantComponent root)
{
root = null!;
if (!uiTable.TryGetValue("Type", out var typeVal)
|| !typeVal.TryRead<string>(out var typeText)
|| !Enum.TryParse<AssistantUiCompontentType>(typeText, true, out var type)
|| type != AssistantUiCompontentType.FORM)
{
LOGGER.LogWarning("UI table of the ASSISTANT table has no valid Form type.");
return false;
}
if (!uiTable.TryGetValue("Children", out var childrenVal) ||
!childrenVal.TryRead<LuaTable>(out var childrenTable))
{
LOGGER.LogWarning("Form has no valid Children table.");
return false;
}
var children = new List<IAssistantComponent>();
var count = childrenTable.ArrayLength;
for (var idx = 1; idx <= count; idx++)
{
var childVal = childrenTable[idx];
if (!childVal.TryRead<LuaTable>(out var childTable))
{
LOGGER.LogWarning($"Child #{idx} is not a table.");
continue;
}
if (!this.TryReadComponentTable(idx, childTable, out var comp))
{
LOGGER.LogWarning($"Child #{idx} could not be parsed.");
continue;
}
children.Add(comp);
}
root = AssistantComponentFactory.CreateComponent(
AssistantUiCompontentType.FORM,
new Dictionary<string, object>(),
children);
return true;
}
/// <summary>
/// Parses the components' table containing all members and properties.
/// Recursively calls itself, if the component has a children table
/// </summary>
/// <param name="idx">Current index inside the <c>FORM</c> children</param>
/// <param name="componentTable">The <c>LuaTable</c> containing all component properties</param>
/// <param name="component">Outputs the component if the parsing is successful</param>
/// <returns>True, when the component table could be read successfully.</returns>
private bool TryReadComponentTable(int idx, LuaTable componentTable, out IAssistantComponent component)
{
component = null!;
if (!componentTable.TryGetValue("Type", out var typeVal)
|| !typeVal.TryRead<string>(out var typeText)
|| !Enum.TryParse<AssistantUiCompontentType>(typeText, true, out var type))
{
LOGGER.LogWarning($"Component #{idx} missing valid Type.");
return false;
}
Dictionary<string, object> props = new();
if (componentTable.TryGetValue("Props", out var propsVal)
&& propsVal.TryRead<LuaTable>(out var propsTable))
{
if (!this.TryReadComponentProps(type, propsTable, out props))
LOGGER.LogWarning($"Component #{idx} Props could not be fully read.");
}
var children = new List<IAssistantComponent>();
if (componentTable.TryGetValue("Children", out var childVal)
&& childVal.TryRead<LuaTable>(out var childTable))
{
var cnt = childTable.ArrayLength;
for (var i = 1; i <= cnt; i++)
{
var cv = childTable[i];
if (cv.TryRead<LuaTable>(out var ct)
&& this.TryReadComponentTable(i, ct, out var childComp))
{
children.Add(childComp);
}
}
}
component = AssistantComponentFactory.CreateComponent(type, props, children);
return true;
}
private bool TryReadComponentProps(
AssistantUiCompontentType type,
LuaTable propsTable,
out Dictionary<string, object> props)
{
props = new Dictionary<string, object>();
if (!ComponentPropSpecs.SPECS.TryGetValue(type, out var spec))
{
LOGGER.LogWarning($"No PropSpec defined for component type {type}");
return false;
}
foreach (var key in spec.Required)
{
if (!propsTable.TryGetValue(key, out var luaVal))
{
LOGGER.LogWarning($"Component {type} missing required prop '{key}'.");
return false;
}
if (!this.TryConvertLuaValue(luaVal, out var dotNetVal))
{
LOGGER.LogWarning($"Component {type}: prop '{key}' has wrong type.");
return false;
}
props[key] = dotNetVal;
}
foreach (var key in spec.Optional)
{
if (!propsTable.TryGetValue(key, out var luaVal))
continue;
if (!this.TryConvertLuaValue(luaVal, out var dotNetVal))
{
LOGGER.LogWarning($"Component {type}: optional prop '{key}' has wrong type, skipping.");
continue;
}
props[key] = dotNetVal;
}
return true;
} }
private bool TryConvertLuaValue(LuaValue val, out object result)
{
if (val.TryRead<string>(out var s))
{
result = s;
return true;
}
if (val.TryRead<bool>(out var b))
{
result = b;
return true;
}
if (val.TryRead<double>(out var d))
{
result = d;
return true;
}
result = null!;
return false;
}
} }