using System.Xml.XPath; using AIStudio.Tools.PluginSystem.Assistants.DataModel; using Lua; namespace AIStudio.Tools.PluginSystem.Assistants; public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType type) : PluginBase(isInternal, state, type) { 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 AssistantForm? RootComponent { get; set; } public string AssistantTitle { get; set; } = string.Empty; public string AssistantDescription { get; set; } = string.Empty; public string SystemPrompt { get; set; } = string.Empty; public string SubmitText { get; set; } = string.Empty; public bool AllowProfiles { get; set; } = true; public void TryLoad() { if(!this.TryProcessAssistant(out var issue)) this.pluginIssues.Add(issue); } /// /// 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; } if (!assistantTable.TryGetValue("SystemPrompt", out var assistantSystemPromptValue) || !assistantSystemPromptValue.TryRead(out var assistantSystemPrompt)) { message = TB("The ASSISTANT table does not contain a valid system prompt."); return false; } if (!assistantTable.TryGetValue("SubmitText", out var assistantSubmitTextValue) || !assistantSubmitTextValue.TryRead(out var assistantSubmitText)) { message = TB("The ASSISTANT table does not contain a valid system prompt."); return false; } if (!assistantTable.TryGetValue("AllowProfiles", out var assistantAllowProfilesValue) || !assistantAllowProfilesValue.TryRead(out var assistantAllowProfiles)) { message = TB("The ASSISTANT table does not contain a the boolean flag to control the allowance of profiles."); return false; } this.AssistantTitle = assistantTitle; this.AssistantDescription = assistantDescription; this.SystemPrompt = assistantSystemPrompt; this.SubmitText = assistantSubmitText; this.AllowProfiles = assistantAllowProfiles; // 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; } if (val.TryRead(out var table) && this.TryParseDropdownItem(table, out var item)) { result = item; return true; } if (val.TryRead(out var listTable) && this.TryParseDropdownItemList(listTable, out var itemList)) { result = itemList; return true; } if (val.TryRead(out var listItemListTable) && this.TryParseListItemList(listItemListTable, out var listItemList)) { result = listItemList; return true; } result = null!; return false; } private bool TryParseDropdownItem(LuaTable table, out AssistantDropdownItem item) { item = new AssistantDropdownItem(); if (!table.TryGetValue("Value", out var valueVal) || !valueVal.TryRead(out var value)) return false; if (!table.TryGetValue("Display", out var displayVal) || !displayVal.TryRead(out var display)) return false; item.Value = value; item.Display = display; return true; } private bool TryParseDropdownItemList(LuaTable table, out List items) { items = new List(); var length = table.ArrayLength; for (var i = 1; i <= length; i++) { var value = table[i]; if (value.TryRead(out var subTable) && this.TryParseDropdownItem(subTable, out var item)) { items.Add(item); } else { items = null!; return false; } } return true; } private bool TryParseListItem(LuaTable table, out AssistantListItem item) { item = new AssistantListItem(); if (!table.TryGetValue("Text", out var textVal) || !textVal.TryRead(out var text)) return false; if (!table.TryGetValue("Type", out var typeVal) || !typeVal.TryRead(out var type)) return false; item.Text = text; item.Type = type; if (table.TryGetValue("Href", out var hrefVal) && hrefVal.TryRead(out var href)) { item.Href = href; } return true; } private bool TryParseListItemList(LuaTable table, out List items) { items = new List(); var length = table.ArrayLength; for (var i = 1; i <= length; i++) { var value = table[i]; if (value.TryRead(out var subTable) && this.TryParseListItem(subTable, out var item)) { items.Add(item); } else { items = null!; return false; } } return true; } }