2025-07-22 18:15:36 +00:00
|
|
|
|
using System.Xml.XPath;
|
|
|
|
|
|
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
|
|
|
|
|
|
using Lua;
|
2025-07-21 12:38:08 +00:00
|
|
|
|
|
2025-07-21 13:10:40 +00:00
|
|
|
|
namespace AIStudio.Tools.PluginSystem.Assistants;
|
2025-07-21 12:38:08 +00:00
|
|
|
|
|
2025-09-29 19:04:16 +00:00
|
|
|
|
public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType type) : PluginBase(isInternal, state, type)
|
2025-07-21 12:38:08 +00:00
|
|
|
|
{
|
2025-09-29 19:04:16 +00:00
|
|
|
|
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginAssistants).Namespace, nameof(PluginAssistants));
|
2025-07-22 18:15:36 +00:00
|
|
|
|
|
2025-07-21 12:38:08 +00:00
|
|
|
|
private static readonly ILogger<PluginAssistants> LOGGER = Program.LOGGER_FACTORY.CreateLogger<PluginAssistants>();
|
|
|
|
|
|
|
2025-09-29 19:04:16 +00:00
|
|
|
|
public AssistantForm? RootComponent { get; set; }
|
2025-07-22 18:15:36 +00:00
|
|
|
|
public string AssistantTitle { get; set; } = string.Empty;
|
2025-09-30 19:54:24 +00:00
|
|
|
|
public string AssistantDescription { get; set; } = string.Empty;
|
|
|
|
|
|
public string SystemPrompt { get; set; } = string.Empty;
|
2025-11-10 16:01:49 +00:00
|
|
|
|
public string SubmitText { get; set; } = string.Empty;
|
2025-09-30 19:54:24 +00:00
|
|
|
|
public bool AllowProfiles { get; set; } = true;
|
2025-07-21 12:38:08 +00:00
|
|
|
|
|
2025-09-29 19:04:16 +00:00
|
|
|
|
public void TryLoad()
|
2025-07-21 12:38:08 +00:00
|
|
|
|
{
|
2025-09-29 19:04:16 +00:00
|
|
|
|
if(!this.TryProcessAssistant(out var issue))
|
|
|
|
|
|
this.pluginIssues.Add(issue);
|
2025-07-22 18:15:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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;
|
|
|
|
|
|
}
|
2025-07-21 12:38:08 +00:00
|
|
|
|
|
2025-07-22 18:15:36 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-09-30 19:54:24 +00:00
|
|
|
|
|
|
|
|
|
|
if (!assistantTable.TryGetValue("SystemPrompt", out var assistantSystemPromptValue) ||
|
|
|
|
|
|
!assistantSystemPromptValue.TryRead<string>(out var assistantSystemPrompt))
|
|
|
|
|
|
{
|
|
|
|
|
|
message = TB("The ASSISTANT table does not contain a valid system prompt.");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-10 16:01:49 +00:00
|
|
|
|
if (!assistantTable.TryGetValue("SubmitText", out var assistantSubmitTextValue) ||
|
|
|
|
|
|
!assistantSubmitTextValue.TryRead<string>(out var assistantSubmitText))
|
|
|
|
|
|
{
|
|
|
|
|
|
message = TB("The ASSISTANT table does not contain a valid system prompt.");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 19:54:24 +00:00
|
|
|
|
if (!assistantTable.TryGetValue("AllowProfiles", out var assistantAllowProfilesValue) ||
|
|
|
|
|
|
!assistantAllowProfilesValue.TryRead<bool>(out var assistantAllowProfiles))
|
|
|
|
|
|
{
|
|
|
|
|
|
message = TB("The ASSISTANT table does not contain a the boolean flag to control the allowance of profiles.");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-07-22 18:15:36 +00:00
|
|
|
|
|
|
|
|
|
|
this.AssistantTitle = assistantTitle;
|
|
|
|
|
|
this.AssistantDescription = assistantDescription;
|
2025-09-30 19:54:24 +00:00
|
|
|
|
this.SystemPrompt = assistantSystemPrompt;
|
2025-11-10 16:01:49 +00:00
|
|
|
|
this.SubmitText = assistantSubmitText;
|
2025-09-30 19:54:24 +00:00
|
|
|
|
this.AllowProfiles = assistantAllowProfiles;
|
2025-07-22 18:15:36 +00:00
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 19:04:16 +00:00
|
|
|
|
root = AssistantComponentFactory.CreateComponent(AssistantUiCompontentType.FORM, new Dictionary<string, object>(), children);
|
2025-07-22 18:15:36 +00:00
|
|
|
|
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.");
|
|
|
|
|
|
}
|
2025-09-30 21:12:16 +00:00
|
|
|
|
|
2025-07-22 18:15:36 +00:00
|
|
|
|
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;
|
2025-07-21 12:38:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-22 18:15:36 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 21:12:16 +00:00
|
|
|
|
// AssistantDropdownItem
|
|
|
|
|
|
if (val.TryRead<LuaTable>(out var table) && this.TryParseDropdownItem(table, out var item))
|
|
|
|
|
|
{
|
|
|
|
|
|
result = item;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// List<AssistantDropdownItem>
|
|
|
|
|
|
if (val.TryRead<LuaTable>(out var listTable) && this.TryParseDropdownItemList(listTable, out var itemList))
|
|
|
|
|
|
{
|
|
|
|
|
|
result = itemList;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-22 18:15:36 +00:00
|
|
|
|
result = null!;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-09-30 21:12:16 +00:00
|
|
|
|
|
|
|
|
|
|
private bool TryParseDropdownItem(LuaTable table, out AssistantDropdownItem item)
|
|
|
|
|
|
{
|
|
|
|
|
|
item = new AssistantDropdownItem();
|
|
|
|
|
|
|
|
|
|
|
|
if (!table.TryGetValue("Value", out var valueVal) || !valueVal.TryRead<string>(out var value))
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
if (!table.TryGetValue("Display", out var displayVal) || !displayVal.TryRead<string>(out var display))
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
item.Value = value;
|
|
|
|
|
|
item.Display = display;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private bool TryParseDropdownItemList(LuaTable table, out List<AssistantDropdownItem> items)
|
|
|
|
|
|
{
|
|
|
|
|
|
items = new List<AssistantDropdownItem>();
|
|
|
|
|
|
|
|
|
|
|
|
var length = table.ArrayLength;
|
|
|
|
|
|
for (var i = 1; i <= length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var value = table[i];
|
|
|
|
|
|
|
|
|
|
|
|
if (value.TryRead<LuaTable>(out var subTable) && this.TryParseDropdownItem(subTable, out var item))
|
|
|
|
|
|
{
|
|
|
|
|
|
items.Add(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
items = null!;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2025-07-21 12:38:08 +00:00
|
|
|
|
}
|