renewed callback contract and made live value updates from all writeable props and values possible

This commit is contained in:
nilsk 2026-03-21 02:03:05 +01:00
parent 82099a0677
commit d1ece556a6
5 changed files with 199 additions and 356 deletions

View File

@ -194,9 +194,9 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
private LuaTable BuildPromptInput() private LuaTable BuildPromptInput()
{ {
var input = new LuaTable(); var state = new LuaTable();
var rootComponent = this.RootComponent; var rootComponent = this.RootComponent;
input["state"] = rootComponent is not null state = rootComponent is not null
? this.assistantState.ToLuaTable(rootComponent.Children) ? this.assistantState.ToLuaTable(rootComponent.Children)
: new LuaTable(); : new LuaTable();
@ -207,9 +207,9 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
["Actions"] = this.currentProfile.Actions, ["Actions"] = this.currentProfile.Actions,
["Num"] = this.currentProfile.Num, ["Num"] = this.currentProfile.Num,
}; };
input["profile"] = profile; state["profile"] = profile;
return input; return state;
} }
private string CollectUserPromptFallback() private string CollectUserPromptFallback()
@ -308,22 +308,52 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
private void ApplyActionResult(LuaTable result, AssistantComponentType sourceType) private void ApplyActionResult(LuaTable result, AssistantComponentType sourceType)
{ {
if (!result.TryGetValue("fields", out var fieldsValue)) if (!result.TryGetValue("state", out var statesValue))
return; return;
if (!fieldsValue.TryRead<LuaTable>(out var fieldsTable)) if (!statesValue.TryRead<LuaTable>(out var stateTable))
{ {
this.Logger.LogWarning("Assistant {ComponentType} callback returned a non-table 'fields' value. The result is ignored.", sourceType); this.Logger.LogWarning($"Assistant {sourceType} callback returned a non-table 'state' value. The result is ignored.");
return; return;
} }
foreach (var pair in fieldsTable) foreach (var component in stateTable)
{ {
if (!pair.Key.TryRead<string>(out var fieldName) || string.IsNullOrWhiteSpace(fieldName)) if (!component.Key.TryRead<string>(out var componentName) || string.IsNullOrWhiteSpace(componentName))
continue; continue;
this.TryApplyFieldUpdate(fieldName, pair.Value, sourceType); if (!component.Value.TryRead<LuaTable>(out var componentUpdate))
{
this.Logger.LogWarning($"Assistant {sourceType} callback returned a non-table update for '{componentName}'. The result is ignored.");
continue;
} }
this.TryApplyComponentUpdate(componentName, componentUpdate, sourceType);
}
}
private void TryApplyComponentUpdate(string componentName, LuaTable componentUpdate, AssistantComponentType sourceType)
{
if (componentUpdate.TryGetValue("Value", out var value))
this.TryApplyFieldUpdate(componentName, value, sourceType);
if (!componentUpdate.TryGetValue("Props", out var propsValue))
return;
if (!propsValue.TryRead<LuaTable>(out var propsTable))
{
this.Logger.LogWarning($"Assistant {sourceType} callback returned a non-table 'Props' value for '{componentName}'. The props update is ignored.");
return;
}
var rootComponent = this.RootComponent;
if (rootComponent is null || !TryFindNamedComponent(rootComponent.Children, componentName, out var component))
{
this.Logger.LogWarning($"Assistant {sourceType} callback tried to update props of unknown component '{componentName}'. The props update is ignored.");
return;
}
this.ApplyPropUpdates(component, propsTable, sourceType);
} }
private void TryApplyFieldUpdate(string fieldName, LuaValue value, AssistantComponentType sourceType) private void TryApplyFieldUpdate(string fieldName, LuaValue value, AssistantComponentType sourceType)
@ -340,6 +370,51 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
this.Logger.LogWarning($"Assistant {sourceType} callback tried to update unknown field '{fieldName}'. The value is ignored."); this.Logger.LogWarning($"Assistant {sourceType} callback tried to update unknown field '{fieldName}'. The value is ignored.");
} }
private void ApplyPropUpdates(IAssistantComponent component, LuaTable propsTable, AssistantComponentType sourceType)
{
var propSpec = ComponentPropSpecs.SPECS.GetValueOrDefault(component.Type);
foreach (var prop in propsTable)
{
if (!prop.Key.TryRead<string>(out var propName) || string.IsNullOrWhiteSpace(propName))
continue;
if (propSpec is not null && propSpec.NonWriteable.Contains(propName, StringComparer.Ordinal))
{
this.Logger.LogWarning($"Assistant {sourceType} callback tried to update non-writeable prop '{propName}' on component '{GetComponentName(component)}'. The value is ignored.");
continue;
}
if (!AssistantLuaConversion.TryReadScalarOrStructuredValue(prop.Value, out var convertedValue))
{
this.Logger.LogWarning($"Assistant {sourceType} callback returned an unsupported value for prop '{propName}' on component '{GetComponentName(component)}'. The props update is ignored.");
continue;
}
component.Props[propName] = convertedValue;
}
}
private static bool TryFindNamedComponent(IEnumerable<IAssistantComponent> components, string componentName, out IAssistantComponent component)
{
foreach (var candidate in components)
{
if (candidate is INamedAssistantComponent named && string.Equals(named.Name, componentName, StringComparison.Ordinal))
{
component = candidate;
return true;
}
if (candidate.Children.Count > 0 && TryFindNamedComponent(candidate.Children, componentName, out component))
return true;
}
component = null!;
return false;
}
private static string GetComponentName(IAssistantComponent component) => component is INamedAssistantComponent named ? named.Name : component.Type.ToString();
private EventCallback<HashSet<string>> CreateMultiselectDropdownChangedCallback(string fieldName) => private EventCallback<HashSet<string>> CreateMultiselectDropdownChangedCallback(string fieldName) =>
EventCallback.Factory.Create<HashSet<string>>(this, values => EventCallback.Factory.Create<HashSet<string>>(this, values =>
{ {

View File

@ -24,9 +24,9 @@ This folder keeps the Lua manifest (`plugin.lua`) that defines a custom assistan
- [Advanced Prompt Assembly - BuildPrompt()](#advanced-prompt-assembly---buildprompt) - [Advanced Prompt Assembly - BuildPrompt()](#advanced-prompt-assembly---buildprompt)
- [Interface](#interface) - [Interface](#interface)
- [`input` table shape](#input-table-shape) - [`input` table shape](#input-table-shape)
- [Using `meta` inside BuildPrompt](#using-meta-inside-buildprompt) - [Using component metadata inside BuildPrompt](#using-component-metadata-inside-buildprompt)
- [Example: iterate all fields with labels and include their values](#example-iterate-all-fields-with-labels-and-include-their-values) - [Example: build a prompt from two fields](#example-build-a-prompt-from-two-fields)
- [Example: handle types differently](#example-handle-types-differently) - [Example: reuse a label from `Props`](#example-reuse-a-label-from-props)
- [Using `profile` inside BuildPrompt](#using-profile-inside-buildprompt) - [Using `profile` inside BuildPrompt](#using-profile-inside-buildprompt)
- [Example: Add user profile context to the prompt](#example-add-user-profile-context-to-the-prompt) - [Example: Add user profile context to the prompt](#example-add-user-profile-context-to-the-prompt)
- [Advanced Layout Options](#advanced-layout-options) - [Advanced Layout Options](#advanced-layout-options)
@ -147,7 +147,7 @@ More information on rendered components can be found [here](https://www.mudblazo
### `TEXT_AREA` reference ### `TEXT_AREA` reference
- Use `Type = "TEXT_AREA"` to render a MudBlazor text input or textarea. - Use `Type = "TEXT_AREA"` to render a MudBlazor text input or textarea.
- Required props: - Required props:
- `Name`: unique state key used in prompt assembly and `BuildPrompt(input.fields)`. - `Name`: unique state key used in prompt assembly and `BuildPrompt(input)`.
- `Label`: visible field label. - `Label`: visible field label.
- Optional props: - Optional props:
- `HelperText`: helper text rendered below the input. - `HelperText`: helper text rendered below the input.
@ -190,7 +190,7 @@ More information on rendered components can be found [here](https://www.mudblazo
### `DROPDOWN` reference ### `DROPDOWN` reference
- Use `Type = "DROPDOWN"` to render a MudBlazor select field. - Use `Type = "DROPDOWN"` to render a MudBlazor select field.
- Required props: - Required props:
- `Name`: unique state key used in prompt assembly, button actions, and `BuildPrompt(input.fields)`. - `Name`: unique state key used in prompt assembly, button actions, and `BuildPrompt(input)`.
- `Label`: visible field label. - `Label`: visible field label.
- `Default`: dropdown item table with the shape `{ ["Value"] = "<internal value>", ["Display"] = "<visible label>" }`. - `Default`: dropdown item table with the shape `{ ["Value"] = "<internal value>", ["Display"] = "<visible label>" }`.
- `Items`: array of dropdown item tables with the same shape as `Default`. - `Items`: array of dropdown item tables with the same shape as `Default`.
@ -211,8 +211,8 @@ More information on rendered components can be found [here](https://www.mudblazo
- `Value`: the internal raw value stored in component state and passed to prompt building. - `Value`: the internal raw value stored in component state and passed to prompt building.
- `Display`: the visible label shown to the user in the menu and selection text. - `Display`: the visible label shown to the user in the menu and selection text.
- Behavior notes: - Behavior notes:
- For single-select dropdowns, `input.fields.<Name>` is a single raw value such as `germany`. - For single-select dropdowns, `input.<Name>.Value` is a single raw value such as `germany`.
- For multiselect dropdowns, `input.fields.<Name>` is an array-like Lua table of raw values. - For multiselect dropdowns, `input.<Name>.Value` is an array-like Lua table of raw values.
- The UI shows the `Display` text, while prompt assembly and `BuildPrompt(input)` receive the raw `Value`. - The UI shows the `Display` text, while prompt assembly and `BuildPrompt(input)` receive the raw `Value`.
- `Default` should usually also exist in `Items`. If it is missing there, the runtime currently still renders it as an available option. - `Default` should usually also exist in `Items`. If it is missing there, the runtime currently still renders it as an available option.
@ -272,13 +272,20 @@ More information on rendered components can be found [here](https://www.mudblazo
#### `Action(input)` interface #### `Action(input)` interface
- The function receives the same `input` structure as `ASSISTANT.BuildPrompt(input)`. - The function receives the same `input` structure as `ASSISTANT.BuildPrompt(input)`.
- Return `nil` for no state update. - Return `nil` for no state update.
- To update component state, return a table with a `fields` table. - Each named component is available as `input.<Name>` and exposes:
- `fields` keys must reference existing component `Name` values. - `Type`: component type such as `TEXT_AREA` or `SWITCH`
- Supported write targets: - `Value`: current component value
- `Props`: readable component props
- To update component state, return a table with a `state` table.
- `state` keys must reference existing component `Name` values.
- Each component update may include:
- `Value`: updates the current state value
- `Props`: partial prop updates for writable props
- Supported `Value` write targets:
- `TEXT_AREA`, single-select `DROPDOWN`, `WEB_CONTENT_READER`, `FILE_CONTENT_READER`, `COLOR_PICKER`, `DATE_PICKER`, `DATE_RANGE_PICKER`, `TIME_PICKER`: string values - `TEXT_AREA`, single-select `DROPDOWN`, `WEB_CONTENT_READER`, `FILE_CONTENT_READER`, `COLOR_PICKER`, `DATE_PICKER`, `DATE_RANGE_PICKER`, `TIME_PICKER`: string values
- multiselect `DROPDOWN`: array-like Lua table of strings - multiselect `DROPDOWN`: array-like Lua table of strings
- `SWITCH`: boolean values - `SWITCH`: boolean values
- Unknown field names and wrong value types are ignored and logged. - Unknown component names, wrong value types, unsupported prop values, and non-writeable props are ignored and logged.
#### Example Button component #### Example Button component
```lua ```lua
@ -296,8 +303,8 @@ More information on rendered components can be found [here](https://www.mudblazo
["IconColor"] = "Inherit", ["IconColor"] = "Inherit",
["IconSize"] = "Medium", ["IconSize"] = "Medium",
["Action"] = function(input) ["Action"] = function(input)
local email = input.fields.emailContent or "" local email = input.emailContent and input.emailContent.Value or ""
local translate = input.fields.translateEmail or false local translate = input.translateEmail and input.translateEmail.Value or false
local output = email local output = email
if translate then if translate then
@ -305,8 +312,10 @@ More information on rendered components can be found [here](https://www.mudblazo
end end
return { return {
fields = { state = {
outputTextField = output outputTextField = {
Value = output
}
} }
} }
end, end,
@ -330,8 +339,10 @@ More information on rendered components can be found [here](https://www.mudblazo
["StartIcon"] = "Icons.Material.Filled.Refresh", ["StartIcon"] = "Icons.Material.Filled.Refresh",
["Action"] = function(input) ["Action"] = function(input)
return { return {
fields = { state = {
outputTextField = "Preview refreshed at " .. Timestamp() outputTextField = {
Value = "Preview refreshed at " .. Timestamp()
}
} }
} }
end end
@ -374,8 +385,10 @@ More information on rendered components can be found [here](https://www.mudblazo
["Text"] = "Build output", ["Text"] = "Build output",
["Action"] = function(input) ["Action"] = function(input)
return { return {
fields = { state = {
outputBuffer = input.fields.emailContent or "" outputBuffer = {
Value = input.emailContent and input.emailContent.Value or ""
}
} }
} }
end, end,
@ -388,7 +401,8 @@ More information on rendered components can be found [here](https://www.mudblazo
["Name"] = "logColor", ["Name"] = "logColor",
["Text"] = "Log color", ["Text"] = "Log color",
["Action"] = function(input) ["Action"] = function(input)
LogError("ColorPicker value: " .. tostring(input.fields.colorPicker or "")) local colorValue = input.colorPicker and input.colorPicker.Value or ""
LogError("ColorPicker value: " .. colorValue)
return nil return nil
end, end,
["EndIcon"] = "Icons.Material.Filled.BugReport" ["EndIcon"] = "Icons.Material.Filled.BugReport"
@ -402,11 +416,11 @@ More information on rendered components can be found [here](https://www.mudblazo
### `SWITCH` reference ### `SWITCH` reference
- Use `Type = "SWITCH"` to render a boolean toggle. - Use `Type = "SWITCH"` to render a boolean toggle.
- Required props: - Required props:
- `Name`: unique state key used in prompt assembly and `BuildPrompt(input.fields)`. - `Name`: unique state key used in prompt assembly and `BuildPrompt(input)`.
- `Value`: initial boolean state (`true` or `false`). - `Value`: initial boolean state (`true` or `false`).
- Optional props: - Optional props:
- `Label`: If set, renders the switch inside an outlines Box, otherwise renders it raw. Visible label for the switch field. - `Label`: If set, renders the switch inside an outlines Box, otherwise renders it raw. Visible label for the switch field.
- `OnChanged`: Lua callback invoked after the switch value changes. It receives the same `input` table as `BUTTON.Action(input)` and may return `{ fields = { ... } }` to update component state. The new switch value is already reflected in `input.fields[Name]`. - `OnChanged`: Lua callback invoked after the switch value changes. It receives the same `input` table as `BUTTON.Action(input)` and may return `{ state = { ... } }` to update component state. The new switch value is already reflected in `input.<Name>.Value`.
- `Disabled`: defaults to `false`; disables user interaction while still allowing the value to be included in prompt assembly. - `Disabled`: defaults to `false`; disables user interaction while still allowing the value to be included in prompt assembly.
- `UserPrompt`: prompt context text for this field. - `UserPrompt`: prompt context text for this field.
- `LabelOn`: text shown when the switch value is `true`. - `LabelOn`: text shown when the switch value is `true`.
@ -427,10 +441,12 @@ More information on rendered components can be found [here](https://www.mudblazo
["Label"] = "Include summary", ["Label"] = "Include summary",
["Value"] = true, ["Value"] = true,
["OnChanged"] = function(input) ["OnChanged"] = function(input)
local includeSummary = input.fields.IncludeSummary or false local includeSummary = input.IncludeSummary and input.IncludeSummary.Value or false
return { return {
fields = { state = {
SummaryMode = includeSummary and "short-summary" or "no-summary" SummaryMode = {
Value = includeSummary and "short-summary" or "no-summary"
}
} }
} }
end, end,
@ -452,7 +468,7 @@ More information on rendered components can be found [here](https://www.mudblazo
### `COLOR_PICKER` reference ### `COLOR_PICKER` reference
- Use `Type = "COLOR_PICKER"` to render a MudBlazor color picker. - Use `Type = "COLOR_PICKER"` to render a MudBlazor color picker.
- Required props: - Required props:
- `Name`: unique state key used in prompt assembly and `BuildPrompt(input.fields)`. - `Name`: unique state key used in prompt assembly and `BuildPrompt(input)`.
- `Label`: visible field label. - `Label`: visible field label.
- Optional props: - Optional props:
- `Placeholder`: default color hex string (e.g. `#FF10FF`) or initial hint text. - `Placeholder`: default color hex string (e.g. `#FF10FF`) or initial hint text.
@ -485,7 +501,7 @@ More information on rendered components can be found [here](https://www.mudblazo
### `DATE_PICKER` reference ### `DATE_PICKER` reference
- Use `Type = "DATE_PICKER"` to render a MudBlazor date picker. - Use `Type = "DATE_PICKER"` to render a MudBlazor date picker.
- Required props: - Required props:
- `Name`: unique state key used in prompt assembly and `BuildPrompt(input.fields)`. - `Name`: unique state key used in prompt assembly and `BuildPrompt(input)`.
- `Label`: visible field label. - `Label`: visible field label.
- Optional props: - Optional props:
- `Value`: initial date string. Use the same format as `DateFormat`; default recommendation is `yyyy-MM-dd`. - `Value`: initial date string. Use the same format as `DateFormat`; default recommendation is `yyyy-MM-dd`.
@ -520,7 +536,7 @@ More information on rendered components can be found [here](https://www.mudblazo
### `DATE_RANGE_PICKER` reference ### `DATE_RANGE_PICKER` reference
- Use `Type = "DATE_RANGE_PICKER"` to render a MudBlazor date range picker. - Use `Type = "DATE_RANGE_PICKER"` to render a MudBlazor date range picker.
- Required props: - Required props:
- `Name`: unique state key used in prompt assembly and `BuildPrompt(input.fields)`. - `Name`: unique state key used in prompt assembly and `BuildPrompt(input)`.
- `Label`: visible field label. - `Label`: visible field label.
- Optional props: - Optional props:
- `Value`: initial range string using `<start> - <end>`, for example `2026-03-01 - 2026-03-31`. - `Value`: initial range string using `<start> - <end>`, for example `2026-03-01 - 2026-03-31`.
@ -557,7 +573,7 @@ More information on rendered components can be found [here](https://www.mudblazo
### `TIME_PICKER` reference ### `TIME_PICKER` reference
- Use `Type = "TIME_PICKER"` to render a MudBlazor time picker. - Use `Type = "TIME_PICKER"` to render a MudBlazor time picker.
- Required props: - Required props:
- `Name`: unique state key used in prompt assembly and `BuildPrompt(input.fields)`. - `Name`: unique state key used in prompt assembly and `BuildPrompt(input)`.
- `Label`: visible field label. - `Label`: visible field label.
- Optional props: - Optional props:
- `Value`: initial time string. Use the same format as `TimeFormat`; default recommendations are `HH:mm` or `hh:mm tt`. - `Value`: initial time string. Use the same format as `TimeFormat`; default recommendations are `HH:mm` or `hh:mm tt`.
@ -613,34 +629,24 @@ If you want full control over prompt composition, define `ASSISTANT.BuildPrompt`
--- ---
### `input` table shape ### `input` table shape
The function receives a single `input` Lua table with: The function receives a single `input` Lua table with:
- `input.fields`: values keyed by component `Name` - `input.<Name>`: one entry per named component
- Text area, single-select dropdown, and readers are strings
- Multiselect dropdown is an array-like Lua table of strings
- Switch is a boolean
- Color picker is the selected color as a string
- Date picker is the selected date as a string
- Date range picker is the selected range as a single string in `<start> - <end>` format
- Time picker is the selected time as a string
- `input.meta`: per-component metadata keyed by component `Name`
- `Type` (string, e.g. `TEXT_AREA`, `DROPDOWN`, `SWITCH`, `COLOR_PICKER`, `DATE_PICKER`, `DATE_RANGE_PICKER`, `TIME_PICKER`) - `Type` (string, e.g. `TEXT_AREA`, `DROPDOWN`, `SWITCH`, `COLOR_PICKER`, `DATE_PICKER`, `DATE_RANGE_PICKER`, `TIME_PICKER`)
- `Label` (string, when provided) - `Value` (current component value)
- `UserPrompt` (string, when provided) - `Props` (readable component props)
- `input.profile`: selected profile data - `input.profile`: selected profile data
- `Name`, `NeedToKnow`, `Actions`, `Num` - `Name`, `NeedToKnow`, `Actions`, `Num`
- When no profile is selected, values match the built-in "Use no profile" entry - When no profile is selected, values match the built-in "Use no profile" entry
- `profile` is a reserved key in the input table
``` ```
input = { input = {
fields = {
["<Name>"] = "<string|boolean>",
...
},
meta = {
["<Name>"] = { ["<Name>"] = {
Type = "<TEXT_AREA|DROPDOWN|SWITCH|WEB_CONTENT_READER|FILE_CONTENT_READER|COLOR_PICKER|DATE_PICKER|DATE_RANGE_PICKER|TIME_PICKER>", Type = "<TEXT_AREA|DROPDOWN|SWITCH|WEB_CONTENT_READER|FILE_CONTENT_READER|COLOR_PICKER|DATE_PICKER|DATE_RANGE_PICKER|TIME_PICKER>",
Value = "<string|boolean|table>",
Props = {
Name = "<string>",
Label = "<string?>", Label = "<string?>",
UserPrompt = "<string?>" UserPrompt = "<string?>"
}, }
...
}, },
profile = { profile = {
Name = "<string>", Name = "<string>",
@ -654,47 +660,66 @@ input = {
``` ```
--- ---
### Using `meta` inside BuildPrompt ### Using component metadata inside BuildPrompt
`input.meta` is useful when you want to dynamically build the prompt based on component type or reuse existing UI text (labels/user prompts). `input.<Name>.Type` and `input.<Name>.Props` are useful when you want to build prompts from a few specific fields without depending on the default `UserPrompt` assembly.
#### Example: iterate all fields with labels and include their values #### Example: build a prompt from two fields
```lua ```lua
ASSISTANT.BuildPrompt = function(input) ASSISTANT.BuildPrompt = function(input)
local topic = input.Topic and input.Topic.Value or ""
local includeSummary = input.IncludeSummary and input.IncludeSummary.Value or false
local parts = {} local parts = {}
for name, value in pairs(input.fields) do if topic ~= "" then
local meta = input.meta[name] table.insert(parts, "Topic: " .. topic)
if meta and meta.Label and value ~= "" then
table.insert(parts, meta.Label .. ": " .. tostring(value))
end end
if includeSummary then
table.insert(parts, "Add a short summary at the end.")
end end
return table.concat(parts, "\n") return table.concat(parts, "\n")
end end
``` ```
#### Example: handle types differently #### Example: reuse a label from `Props`
```lua ```lua
ASSISTANT.BuildPrompt = function(input) ASSISTANT.BuildPrompt = function(input)
local parts = {} local main = input.Main
for name, meta in pairs(input.meta) do if not main then
local value = input.fields[name] return ""
if meta.Type == "SWITCH" then
table.insert(parts, name .. ": " .. tostring(value))
elseif meta.Type == "COLOR_PICKER" and value and value ~= "" then
table.insert(parts, name .. ": " .. value)
elseif meta.Type == "DATE_PICKER" and value and value ~= "" then
table.insert(parts, name .. ": " .. value)
elseif meta.Type == "DATE_RANGE_PICKER" and value and value ~= "" then
table.insert(parts, name .. ": " .. value)
elseif meta.Type == "TIME_PICKER" and value and value ~= "" then
table.insert(parts, name .. ": " .. value)
elseif value and value ~= "" then
table.insert(parts, name .. ": " .. value)
end end
end
return table.concat(parts, "\n") local label = main.Props and main.Props.Label or "Main"
local value = main.Value or ""
return label .. ": " .. value
end end
``` ```
--- ---
### Callback result shape
Callbacks may return a partial state update:
```lua
return {
state = {
["<Name>"] = {
Value = "<optional new value>",
Props = {
-- optional writable prop updates
}
}
}
}
```
- `Value` is optional
- `Props` is optional
- `Props` updates are partial
- non-writeable props are ignored and logged
---
### Using `profile` inside BuildPrompt ### Using `profile` inside BuildPrompt
Profiles are optional user context (e.g., "NeedToKnow" and "Actions"). You can inject this directly into the user prompt if you want the LLM to always see it. Profiles are optional user context (e.g., "NeedToKnow" and "Actions"). You can inject this directly into the user prompt if you want the LLM to always see it.
@ -707,7 +732,7 @@ ASSISTANT.BuildPrompt = function(input)
table.insert(parts, input.profile.NeedToKnow) table.insert(parts, input.profile.NeedToKnow)
table.insert(parts, "") table.insert(parts, "")
end end
table.insert(parts, input.fields.Main or "") table.insert(parts, input.Main and input.Main.Value or "")
return table.concat(parts, "\n") return table.concat(parts, "\n")
end end
``` ```
@ -1012,14 +1037,14 @@ The assistant runtime exposes basic logging helpers to Lua. Use them to debug cu
- `LogDebug(message)` - `LogDebug(message)`
- `LogInfo(message)` - `LogInfo(message)`
- `LogWarn(message)` - `LogWarning(message)`
- `LogError(message)` - `LogError(message)`
#### Example: Use Logging in lua functions #### Example: Use Logging in lua functions
```lua ```lua
ASSISTANT.BuildPrompt = function(input) ASSISTANT.BuildPrompt = function(input)
LogInfo("BuildPrompt called") LogInfo("BuildPrompt called")
return input.fields.Text or "" return input.Text and input.Text.Value or ""
end end
``` ```
--- ---

View File

@ -147,8 +147,8 @@ ASSISTANT = {
["IconColor"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", -- color of start and end icons on text buttons. Defaults to Inherit ["IconColor"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", -- color of start and end icons on text buttons. Defaults to Inherit
["IconSize"] = "<Small|Medium|Large>", -- size of icons. Defaults to null. When null, the value of ["Size"] is used ["IconSize"] = "<Small|Medium|Large>", -- size of icons. Defaults to null. When null, the value of ["Size"] is used
["Action"] = function(input) ["Action"] = function(input)
local email = input.fields.emailContent or "" local email = input.emailContent and input.emailContent.Value or ""
local translate = input.fields.translateEmail or false local translate = input.translateEmail and input.translateEmail.Value or false
local output = email local output = email
if translate then if translate then
@ -156,8 +156,10 @@ ASSISTANT = {
end end
return { return {
fields = { state = {
outputBuffer = output outputBuffer = {
Value = output
}
} }
} }
end, end,

View File

@ -1,4 +1,3 @@
using System.Collections;
using AIStudio.Assistants.Dynamic; using AIStudio.Assistants.Dynamic;
using Lua; using Lua;
@ -159,6 +158,7 @@ public sealed class AssistantState
{ {
target[named.Name] = new LuaTable target[named.Name] = new LuaTable
{ {
["Type"] = Enum.GetName<AssistantComponentType>(component.Type) ?? string.Empty,
["Value"] = component is IStatefulAssistantComponent ? this.ReadValueForLua(named.Name) : LuaValue.Nil, ["Value"] = component is IStatefulAssistantComponent ? this.ReadValueForLua(named.Name) : LuaValue.Nil,
["Props"] = this.CreatePropsTable(component), ["Props"] = this.CreatePropsTable(component),
}; };
@ -176,7 +176,7 @@ public sealed class AssistantState
if (this.SingleSelect.TryGetValue(name, out var singleSelectValue)) if (this.SingleSelect.TryGetValue(name, out var singleSelectValue))
return singleSelectValue; return singleSelectValue;
if (this.MultiSelect.TryGetValue(name, out var multiSelectValue)) if (this.MultiSelect.TryGetValue(name, out var multiSelectValue))
return CreateLuaArray(multiSelectValue.OrderBy(static value => value, StringComparer.Ordinal)); return AssistantLuaConversion.CreateLuaArray(multiSelectValue.OrderBy(static value => value, StringComparer.Ordinal));
if (this.Bools.TryGetValue(name, out var boolValue)) if (this.Bools.TryGetValue(name, out var boolValue))
return boolValue; return boolValue;
if (this.WebContent.TryGetValue(name, out var webContentValue)) if (this.WebContent.TryGetValue(name, out var webContentValue))
@ -210,138 +210,13 @@ public sealed class AssistantState
if (!component.Props.TryGetValue(key, out var value)) if (!component.Props.TryGetValue(key, out var value))
continue; continue;
if (!TryWriteLuaValue(table, key, value)) if (!AssistantLuaConversion.TryWriteAssistantValue(table, key, value))
continue; continue;
} }
return table; return table;
} }
private static bool TryWriteLuaValue(LuaTable table, string key, object? value)
{
if (value is null or LuaFunction)
return false;
switch (value)
{
case LuaValue { Type: not LuaValueType.Nil } luaValue:
table[key] = luaValue;
return true;
case LuaTable luaTable:
table[key] = luaTable;
return true;
case string stringValue:
table[key] = (LuaValue)stringValue;
return true;
case bool boolValue:
table[key] = (LuaValue)boolValue;
return true;
case byte byteValue:
table[key] = (LuaValue)byteValue;
return true;
case sbyte sbyteValue:
table[key] = (LuaValue)sbyteValue;
return true;
case short shortValue:
table[key] = (LuaValue)shortValue;
return true;
case ushort ushortValue:
table[key] = (LuaValue)ushortValue;
return true;
case int intValue:
table[key] = (LuaValue)intValue;
return true;
case uint uintValue:
table[key] = (LuaValue)uintValue;
return true;
case long longValue:
table[key] = (LuaValue)longValue;
return true;
case ulong ulongValue:
table[key] = (LuaValue)ulongValue;
return true;
case float floatValue:
table[key] = (LuaValue)floatValue;
return true;
case double doubleValue:
table[key] = (LuaValue)doubleValue;
return true;
case decimal decimalValue:
table[key] = (LuaValue)(double)decimalValue;
return true;
case Enum enumValue:
table[key] = enumValue.ToString() ?? string.Empty;
return true;
case AssistantDropdownItem dropdownItem:
table[key] = CreateDropdownItemTable(dropdownItem);
return true;
case IEnumerable<AssistantDropdownItem> dropdownItems:
table[key] = CreateLuaArray(dropdownItems.Select(CreateDropdownItemTable));
return true;
case IEnumerable<AssistantListItem> listItems:
table[key] = CreateLuaArray(listItems.Select(CreateListItemTable));
return true;
case IEnumerable<string> strings:
table[key] = CreateLuaArray(strings);
return true;
default:
return false;
}
}
private static LuaTable CreateDropdownItemTable(AssistantDropdownItem item) =>
new()
{
["Value"] = item.Value,
["Display"] = item.Display,
};
private static LuaTable CreateListItemTable(AssistantListItem item)
{
var table = new LuaTable
{
["Type"] = item.Type,
["Text"] = item.Text,
["Icon"] = item.Icon,
["IconColor"] = item.IconColor,
};
if (!string.IsNullOrWhiteSpace(item.Href))
table["Href"] = item.Href;
return table;
}
private static LuaTable CreateLuaArray(IEnumerable values)
{
var luaArray = new LuaTable();
var index = 1;
foreach (var value in values)
luaArray[index++] = value switch
{
null => LuaValue.Nil,
LuaValue luaValue => luaValue,
LuaTable luaTable => luaTable,
string stringValue => (LuaValue)stringValue,
bool boolValue => (LuaValue)boolValue,
byte byteValue => (LuaValue)byteValue,
sbyte sbyteValue => (LuaValue)sbyteValue,
short shortValue => (LuaValue)shortValue,
ushort ushortValue => (LuaValue)ushortValue,
int intValue => (LuaValue)intValue,
uint uintValue => (LuaValue)uintValue,
long longValue => (LuaValue)longValue,
ulong ulongValue => (LuaValue)ulongValue,
float floatValue => (LuaValue)floatValue,
double doubleValue => (LuaValue)doubleValue,
decimal decimalValue => (LuaValue)(double)decimalValue,
_ => LuaValue.Nil,
};
return luaArray;
}
private static HashSet<string> ReadStringValues(LuaTable values) private static HashSet<string> ReadStringValues(LuaTable values)
{ {
var parsedValues = new HashSet<string>(StringComparer.Ordinal); var parsedValues = new HashSet<string>(StringComparer.Ordinal);

View File

@ -399,141 +399,7 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
return true; return true;
} }
return this.TryConvertLuaValue(val, out result); return AssistantLuaConversion.TryReadScalarOrStructuredValue(val, out result);
}
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;
}
if (val.TryRead<LuaTable>(out var table) && this.TryParseDropdownItem(table, out var item))
{
result = item;
return true;
}
if (val.TryRead<LuaTable>(out var listTable) && this.TryParseDropdownItemList(listTable, out var itemList))
{
result = itemList;
return true;
}
if (val.TryRead<LuaTable>(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<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;
}
private bool TryParseListItem(LuaTable table, out AssistantListItem item)
{
item = new AssistantListItem();
if (!table.TryGetValue("Text", out var textVal) || !textVal.TryRead<string>(out var text))
return false;
if (!table.TryGetValue("Type", out var typeVal) || !typeVal.TryRead<string>(out var type))
return false;
table.TryGetValue("Icon", out var iconVal);
iconVal.TryRead<string>(out var icon);
icon ??= string.Empty;
table.TryGetValue("IconColor", out var iconColorVal);
iconColorVal.TryRead<string>(out var iconColor);
iconColor ??= string.Empty;
item.Text = text;
item.Type = type;
item.Icon = icon;
item.IconColor = iconColor;
if (table.TryGetValue("Href", out var hrefVal) && hrefVal.TryRead<string>(out var href))
{
item.Href = href;
}
return true;
}
private bool TryParseListItemList(LuaTable table, out List<AssistantListItem> items)
{
items = new List<AssistantListItem>();
var length = table.ArrayLength;
for (var i = 1; i <= length; i++)
{
var value = table[i];
if (value.TryRead<LuaTable>(out var subTable) && this.TryParseListItem(subTable, out var item))
{
items.Add(item);
}
else
{
items = null!;
return false;
}
}
return true;
} }
private void RegisterLuaHelpers() private void RegisterLuaHelpers()