diff --git a/app/MindWork AI Studio/Plugins/assistants/plugin.lua b/app/MindWork AI Studio/Plugins/assistants/plugin.lua index 3146f75d..c1af0050 100644 --- a/app/MindWork AI Studio/Plugins/assistants/plugin.lua +++ b/app/MindWork AI Studio/Plugins/assistants/plugin.lua @@ -40,45 +40,45 @@ IS_MAINTAINED = true DEPRECATION_MESSAGE = "" ASSISTANT = { - Name = "Grammatik- und Rechtschreibprüfung", - Description = "Grammatik und Rechtschreibung eines Textes überprüfen.", - UI = { - Type = "Form", - Children = { + ["Title"] = "Grammatik- und Rechtschreibprüfung", + ["Description"] = "Grammatik und Rechtschreibung eines Textes überprüfen.", + ["UI"] = { + ["Type"] = "FORM", + ["Children"] = { { - Type = "TextArea", - Props = { - Name = "input", - Label = "Ihre Eingabe zur Überprüfung" + ["Type"] = "TEXT_AREA", + ["Props"] = { + ["Name"] = "input", + ["Label"] = "Ihre Eingabe zur Überprüfung" } }, { - Type = "Dropdown", - ValueType = "string", - Default = { Value = "", Display = "Sprache nicht angeben."} - Items = { - { Value = "de-DE", Display = "Deutsch" }, - { Value = "en-UK", Display = "Englisch (UK)" }, - { Value = "en-US", Display = "Englisch (US)" }, + ["Type"] = "DROPDOWN", + ["ValueType"] = "string", + ["Default"] = { ["Value"] = "", ["Display"] = "Sprache nicht angeben." }, + ["Items"] = { + { ["Value"] = "de-DE", ["Display"] = "Deutsch" }, + { ["Value"] = "en-UK", ["Display"] = "Englisch (UK)" }, + { ["Value"] = "en-US", ["Display"] = "Englisch (US)" }, }, - Props = { - Name = "language", - Label = "Sprache", + ["Props"] = { + ["Name"] = "language", + ["Label"] = "Sprache", } }, { - Type = "ProviderSelection", - Props = { - Name = "Anbieter", - Label = "LLM auswählen" + ["Type"] = "PROVIDER_SELECTION", + ["Props"] = { + ["Name"] = "Anbieter", + ["Label"] = "LLM auswählen" } }, { - Type = "Button", - Props = { - Name = "submit", - Text = "Korrekturlesen", - Action = "OnSubmit" + ["Type"] = "BUTTON", + ["Props"] = { + ["Name"] = "submit", + ["Text"] = "Korrekturlesen", + ["Action"] = "OnSubmit" } }, } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs new file mode 100644 index 00000000..b9104c05 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs @@ -0,0 +1,29 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public static class ComponentPropSpecs +{ + public static readonly IReadOnlyDictionary SPECS = + new Dictionary + { + [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: [] + ), + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/PropSpec.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/PropSpec.cs new file mode 100644 index 00000000..7aa5e7b1 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/PropSpec.cs @@ -0,0 +1,7 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class PropSpec(IEnumerable required, IEnumerable optional) +{ + public IReadOnlyList Required { get; } = required.ToArray(); + public IReadOnlyList Optional { get; } = optional.ToArray(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs index c85de500..25a43491 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs @@ -1,19 +1,243 @@ -using Lua; +using System.Xml.XPath; +using AIStudio.Tools.PluginSystem.Assistants.DataModel; +using Lua; namespace AIStudio.Tools.PluginSystem.Assistants; 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 LOGGER = Program.LOGGER_FACTORY.CreateLogger(); - public string AssistantTitle { get; set;} = string.Empty; - private string AssistantDescription {get; set;} = string.Empty; + public AssistantForm RootComponent { get; set; } + 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) { + } + + /// + /// Tries to parse the assistant table into our internal assistant render tree data model. It follows this process: + /// + /// ASSISTANT → Title/Description → UI + /// UI: Root element → required Children → Components + /// Components: Type → Props → Children (recursively) + /// + /// + /// The error message, when parameters from the table could not be read. + /// True, when the assistant could be read successfully indicating the data model is populated. + 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(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(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(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(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; + } + + /// + /// Parses the root FORM component and start to parse its required children (main ui components) + /// + /// The LuaTable containing all UI components + /// Outputs the root FORM component, if the parsing is successful. + /// True, when the UI table could be read successfully. + private bool TryReadRenderTree(LuaTable uiTable, out IAssistantComponent root) + { + root = null!; + + if (!uiTable.TryGetValue("Type", out var typeVal) + || !typeVal.TryRead(out var typeText) + || !Enum.TryParse(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(out var childrenTable)) + { + LOGGER.LogWarning("Form has no valid Children table."); + return false; + } + + var children = new List(); + var count = childrenTable.ArrayLength; + for (var idx = 1; idx <= count; idx++) + { + var childVal = childrenTable[idx]; + if (!childVal.TryRead(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(), + children); + return true; + } + + /// + /// Parses the components' table containing all members and properties. + /// Recursively calls itself, if the component has a children table + /// + /// Current index inside the FORM children + /// The LuaTable containing all component properties + /// Outputs the component if the parsing is successful + /// True, when the component table could be read successfully. + private bool TryReadComponentTable(int idx, LuaTable componentTable, out IAssistantComponent component) + { + component = null!; + + if (!componentTable.TryGetValue("Type", out var typeVal) + || !typeVal.TryRead(out var typeText) + || !Enum.TryParse(typeText, true, out var type)) + { + LOGGER.LogWarning($"Component #{idx} missing valid Type."); + return false; + } + + Dictionary props = new(); + if (componentTable.TryGetValue("Props", out var propsVal) + && propsVal.TryRead(out var propsTable)) + { + if (!this.TryReadComponentProps(type, propsTable, out props)) + LOGGER.LogWarning($"Component #{idx} Props could not be fully read."); + } + + var children = new List(); + if (componentTable.TryGetValue("Children", out var childVal) + && childVal.TryRead(out var childTable)) + { + var cnt = childTable.ArrayLength; + for (var i = 1; i <= cnt; i++) + { + var cv = childTable[i]; + if (cv.TryRead(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 props) + { + props = new Dictionary(); + + 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(out var s)) + { + result = s; + return true; + } + if (val.TryRead(out var b)) + { + result = b; + return true; + } + if (val.TryRead(out var d)) + { + result = d; + return true; + } + + result = null!; + return false; + } } \ No newline at end of file