mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-03-29 19:11:38 +00:00
added advanced prompt building option by creating a new lua function ASSISTANT.BuildPrompt that users can override
This commit is contained in:
parent
9ae3fcaed9
commit
4eb0cc67c3
@ -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<SettingsDialogDynamic>
|
||||
private string selectedTargetLanguage = string.Empty;
|
||||
private string customTargetLanguage = string.Empty;
|
||||
private bool showFooterProfileSelection = true;
|
||||
private PluginAssistants? assistantPlugin;
|
||||
|
||||
private Dictionary<string, string> inputFields = new();
|
||||
private Dictionary<string, string> dropdownFields = new();
|
||||
@ -56,6 +58,7 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
|
||||
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<SettingsDialogDynamic>
|
||||
};
|
||||
}
|
||||
|
||||
private string CollectUserPrompt()
|
||||
private async Task<string> 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<SettingsDialogDynamic>
|
||||
private async Task Submit()
|
||||
{
|
||||
this.CreateChatThread();
|
||||
var time = this.AddUserRequest(this.CollectUserPrompt());
|
||||
var time = this.AddUserRequest(await this.CollectUserPromptAsync());
|
||||
await this.AddAIResponseAsync(time);
|
||||
}
|
||||
|
||||
|
||||
@ -33,6 +33,138 @@ 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 = {
|
||||
["<Name>"] = "<string|boolean>",
|
||||
...
|
||||
},
|
||||
meta = {
|
||||
["<Name>"] = {
|
||||
Type = "<TEXT_AREA|DROPDOWN|SWITCH|WEB_CONTENT_READER|FILE_CONTENT_READER>",
|
||||
Label = "<string?>",
|
||||
UserPrompt = "<string?>"
|
||||
},
|
||||
...
|
||||
},
|
||||
profile = {
|
||||
Id = "<string guid>",
|
||||
Name = "<string>",
|
||||
NeedToKnow = "<string>",
|
||||
Actions = "<string>",
|
||||
Num = <number>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 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.
|
||||
|
||||
@ -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<LuaTable>(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<LuaFunction>(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<string?> 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<string>(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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the root <c>FORM</c> component and start to parse its required children (main ui components)
|
||||
/// </summary>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user