From 4eb0cc67c38d2bfdff549d44ac6175ed5d337246 Mon Sep 17 00:00:00 2001 From: krut_ni Date: Mon, 2 Mar 2026 15:24:18 +0100 Subject: [PATCH] added advanced prompt building option by creating a new lua function `ASSISTANT.BuildPrompt` that users can override --- .../Dynamic/AssistantDynamic.razor.cs | 93 +++++++++++- .../Plugins/assistants/README.md | 134 +++++++++++++++++- .../Assistants/PluginAssistants.cs | 36 +++++ 3 files changed, 260 insertions(+), 3 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs index 7fbb79cb..a87b7dd0 100644 --- a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs +++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs @@ -7,6 +7,7 @@ using AIStudio.Tools.PluginSystem; using AIStudio.Settings; using AIStudio.Tools.PluginSystem.Assistants; using AIStudio.Tools.PluginSystem.Assistants.DataModel; +using Lua; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.WebUtilities; @@ -35,6 +36,7 @@ public partial class AssistantDynamic : AssistantBaseCore private string selectedTargetLanguage = string.Empty; private string customTargetLanguage = string.Empty; private bool showFooterProfileSelection = true; + private PluginAssistants? assistantPlugin; private Dictionary inputFields = new(); private Dictionary dropdownFields = new(); @@ -56,6 +58,7 @@ public partial class AssistantDynamic : AssistantBaseCore return; } + this.assistantPlugin = assistantPlugin; this.RootComponent = assistantPlugin.RootComponent; this.title = assistantPlugin.AssistantTitle; this.description = assistantPlugin.AssistantDescription; @@ -226,7 +229,93 @@ public partial class AssistantDynamic : AssistantBaseCore }; } - private string CollectUserPrompt() + private async Task CollectUserPromptAsync() + { + if (this.assistantPlugin?.HasCustomPromptBuilder != true) return this.CollectUserPromptFallback(); + + var input = this.BuildPromptInput(); + var prompt = await this.assistantPlugin.TryBuildPromptAsync(input, this.cancellationTokenSource?.Token ?? CancellationToken.None); + return !string.IsNullOrWhiteSpace(prompt) ? prompt : this.CollectUserPromptFallback(); + } + + private LuaTable BuildPromptInput() + { + var input = new LuaTable(); + + var fields = new LuaTable(); + foreach (var entry in this.inputFields) + fields[entry.Key] = entry.Value ?? string.Empty; + foreach (var entry in this.dropdownFields) + fields[entry.Key] = entry.Value ?? string.Empty; + foreach (var entry in this.switchFields) + fields[entry.Key] = entry.Value; + foreach (var entry in this.webContentFields) + fields[entry.Key] = entry.Value.Content ?? string.Empty; + foreach (var entry in this.fileContentFields) + fields[entry.Key] = entry.Value.Content ?? string.Empty; + + input["fields"] = fields; + + var meta = new LuaTable(); + var rootComponent = this.RootComponent; + if (rootComponent is not null) + { + foreach (var component in rootComponent.Children) + { + switch (component) + { + case AssistantTextArea textArea: + this.AddMetaEntry(meta, textArea.Name, component.Type, textArea.Label, textArea.UserPrompt); + break; + case AssistantDropdown dropdown: + this.AddMetaEntry(meta, dropdown.Name, component.Type, dropdown.Label, dropdown.UserPrompt); + break; + case AssistantSwitch switchComponent: + this.AddMetaEntry(meta, switchComponent.Name, component.Type, switchComponent.Label, switchComponent.UserPrompt); + break; + case AssistantWebContentReader webContent: + this.AddMetaEntry(meta, webContent.Name, component.Type, null, webContent.UserPrompt); + break; + case AssistantFileContentReader fileContent: + this.AddMetaEntry(meta, fileContent.Name, component.Type, null, fileContent.UserPrompt); + break; + } + } + } + + input["meta"] = meta; + + var profile = new LuaTable + { + ["Name"] = this.currentProfile.Name, + ["NeedToKnow"] = this.currentProfile.NeedToKnow, + ["Actions"] = this.currentProfile.Actions, + ["Num"] = this.currentProfile.Num, + }; + input["profile"] = profile; + + return input; + } + + private void AddMetaEntry(LuaTable meta, string name, AssistantComponentType type, string? label, string? userPrompt) + { + if (string.IsNullOrWhiteSpace(name)) + return; + + var entry = new LuaTable + { + ["Type"] = type.ToString(), + }; + + if (!string.IsNullOrWhiteSpace(label)) + entry["Label"] = label!; + if (!string.IsNullOrWhiteSpace(userPrompt)) + entry["UserPrompt"] = userPrompt!; + + meta[name] = entry; + } + + private string CollectUserPromptFallback() { var prompt = string.Empty; var rootComponent = this.RootComponent; @@ -339,7 +428,7 @@ public partial class AssistantDynamic : AssistantBaseCore private async Task Submit() { this.CreateChatThread(); - var time = this.AddUserRequest(this.CollectUserPrompt()); + var time = this.AddUserRequest(await this.CollectUserPromptAsync()); await this.AddAIResponseAsync(time); } diff --git a/app/MindWork AI Studio/Plugins/assistants/README.md b/app/MindWork AI Studio/Plugins/assistants/README.md index 9adc5bbc..27ef2120 100644 --- a/app/MindWork AI Studio/Plugins/assistants/README.md +++ b/app/MindWork AI Studio/Plugins/assistants/README.md @@ -33,10 +33,142 @@ user prompt: For switches the “value” is the boolean `true/false`; for readers it is the fetched/selected content. Always provide a meaningful `UserPrompt` so the final concatenated prompt remains coherent from the LLM’s perspective. +### Advanced: BuildPrompt (optional) +If you want full control over prompt composition, define `ASSISTANT.BuildPrompt` as a Lua function. When present, AI Studio calls it and uses its return value as the final user prompt. The default prompt assembly is skipped. + +#### Contract +- `ASSISTANT.BuildPrompt(input)` must return a **string**. +- If the function is missing, returns `nil`, or returns a non-string, AI Studio falls back to the default prompt assembly. +- Errors in the function are caught and logged, then fall back to the default prompt assembly. + +#### Input table shape +The function receives a single `input` table with: +- `input.fields`: values keyed by component `Name` + - Text area, dropdown, and readers are strings + - Switch is a boolean +- `input.meta`: per-component metadata keyed by component `Name` + - `Type` (string, e.g. `TEXT_AREA`, `DROPDOWN`, `SWITCH`) + - `Label` (string, when provided) + - `UserPrompt` (string, when provided) +- `input.profile`: selected profile data + - `Id`, `Name`, `NeedToKnow`, `Actions`, `Num` + - When no profile is selected, values match the built-in "Use no profile" entry + +#### Table shapes (quick reference) +``` +input = { + fields = { + [""] = "", + ... + }, + meta = { + [""] = { + Type = "", + Label = "", + UserPrompt = "" + }, + ... + }, + profile = { + Id = "", + Name = "", + NeedToKnow = "", + Actions = "", + Num = + } +} +``` + +#### Using `meta` 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). + +Example: iterate all fields with labels and include their values +```lua +ASSISTANT.BuildPrompt = function(input) + local parts = {} + for name, value in pairs(input.fields) do + local meta = input.meta[name] + if meta and meta.Label and value ~= "" then + table.insert(parts, meta.Label .. ": " .. tostring(value)) + end + end + return table.concat(parts, "\n") +end +``` + +Example: handle types differently +```lua +ASSISTANT.BuildPrompt = function(input) + local parts = {} + for name, meta in pairs(input.meta) do + local value = input.fields[name] + if meta.Type == "SWITCH" then + table.insert(parts, name .. ": " .. tostring(value)) + elseif value and value ~= "" then + table.insert(parts, name .. ": " .. value) + end + end + return table.concat(parts, "\n") +end +``` + +#### 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. + +Example: +```lua +ASSISTANT.BuildPrompt = function(input) + local parts = {} + if input.profile and input.profile.NeedToKnow ~= "" then + table.insert(parts, "User context:") + table.insert(parts, input.profile.NeedToKnow) + table.insert(parts, "") + end + table.insert(parts, input.fields.Main or "") + return table.concat(parts, "\n") +end +``` + +#### Example: simple custom prompt +```lua +ASSISTANT.BuildPrompt = function(input) + local f = input.fields + return "Topic: " .. (f.Topic or "") .. "\nDetails:\n" .. (f.Details or "") +end +``` + +#### Example: structured prompt (similar to Coding assistant) +```lua +ASSISTANT.BuildPrompt = function(input) + local f = input.fields + local parts = {} + + if (f.Code or "") ~= "" then + table.insert(parts, "I have the following code:") + table.insert(parts, "```") + table.insert(parts, f.Code) + table.insert(parts, "```") + table.insert(parts, "") + end + + if (f.CompilerMessages or "") ~= "" then + table.insert(parts, "I have the following compiler messages:") + table.insert(parts, "```") + table.insert(parts, f.CompilerMessages) + table.insert(parts, "```") + table.insert(parts, "") + end + + table.insert(parts, "My questions are:") + table.insert(parts, f.Questions or "") + return table.concat(parts, "\n") +end +``` + # Tips 1. Give every component a unique `Name`— it’s used to track state. 2. Keep in mind that components and their properties are case-sensitive (e.g. if you write `["Type"] = "heading"` instead of `["Type"] = "HEADING"` the component will not be registered). Always copy-paste the component from the `plugin.lua` manifest to avoid this. 3. When you expect default content (e.g., a textarea with instructions), keep `UserPrompt` but also set `PrefillText` so the user starts with a hint. 4. If you need extra explanatory text (before or after the interactive controls), use `TEXT` or `HEADING` components. -5. Keep `Preselect`/`PreselectContentCleanerAgent` flags in `WEB_CONTENT_READER` to simplify the initial UI for the user. \ No newline at end of file +5. Keep `Preselect`/`PreselectContentCleanerAgent` flags in `WEB_CONTENT_READER` to simplify the initial UI for the user. diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs index b5437601..acd89094 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs @@ -17,6 +17,9 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType public string SubmitText { get; set; } = string.Empty; public bool AllowProfiles { get; set; } = true; public bool HasEmbeddedProfileSelection { get; private set; } + public bool HasCustomPromptBuilder => this.buildPromptFunction is not null; + + private LuaFunction? buildPromptFunction; public void TryLoad() { @@ -38,6 +41,7 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType { message = string.Empty; this.HasEmbeddedProfileSelection = false; + this.buildPromptFunction = null; // Ensure that the main ASSISTANT table exists and is a valid Lua table: if (!this.state.Environment["ASSISTANT"].TryRead(out var assistantTable)) @@ -81,6 +85,14 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType return false; } + if (assistantTable.TryGetValue("BuildPrompt", out var buildPromptValue)) + { + if (buildPromptValue.TryRead(out var buildPrompt)) + this.buildPromptFunction = buildPrompt; + else + message = TB("ASSISTANT.BuildPrompt exists but is not a Lua function or has invalid syntax."); + } + this.AssistantTitle = assistantTitle; this.AssistantDescription = assistantDescription; this.SystemPrompt = assistantSystemPrompt; @@ -104,6 +116,30 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType return true; } + public async Task TryBuildPromptAsync(LuaTable input, CancellationToken cancellationToken = default) + { + if (this.buildPromptFunction is null) + return null; + + try + { + var results = await this.buildPromptFunction.InvokeAsync(this.state, [input], cancellationToken: cancellationToken); + if (results.Length == 0) + return string.Empty; + + if (results[0].TryRead(out var prompt)) + return prompt; + + LOGGER.LogWarning("ASSISTANT.BuildPrompt returned a non-string value."); + return string.Empty; + } + catch (Exception e) + { + LOGGER.LogError(e, "ASSISTANT.BuildPrompt failed to execute."); + return string.Empty; + } + } + /// /// Parses the root FORM component and start to parse its required children (main ui components) ///