V18 / 8d896b8a86e5
AddonScenarioDraft.cs
Lilith/Core/Addons/AddonScenarioDraft.cs
Changed in this versionCS
using System.Text.Json;
using System.Xml.Linq;
using Lilith.Core.Scenarios;
namespace Lilith.Core.Addons;
internal sealed record AddonInput(string Name,bool Required=false,string? Default=null);
internal sealed record AddonStep(string Kind,string? Prompt=null,string? Result=null,string? Scenario=null,Dictionary<string,string>? Inputs=null,string? Input=null,[property: System.Text.Json.Serialization.JsonPropertyName("equals")] string? EqualsValue=null,AddonStep[]? Then=null,AddonStep[]? Else=null);
internal sealed record AddonScenarioDraft(string Id,string Title,string SystemPrompt,AddonInput[] Inputs,AddonStep[] Steps)
{
internal static string Example(string name)=>JsonSerializer.Serialize(new AddonScenarioDraft(name,"Wallpaper","Use actual tool receipts.",
[new("query",true),new("action",Default:"set")],
[new("if",Input:"action",EqualsValue:"restore",Then:[new("chat",Prompt:$"Call {name}_tool with action restore. Return its actual receipt.")],Else:[
new("run",Result:"images",Scenario:"image_candidates",Inputs:new(){{"query","{{query}}"},{"count","5"}}),
new("if",Input:"images.status",EqualsValue:"completed",Then:[
new("run",Result:"selected",Scenario:"image_select",Inputs:new(){{"query","{{query}}"},{"candidates","{{images.candidates}}"}}),
new("chat",Prompt:$"Call {name}_tool with action set and download_url copied exactly from "+"{{selected.download_url}}. Return only the actual tool receipt.")
],Else:[new("chat",Prompt:"Return only: No suitable images were found; nothing was changed.")])
])]),new JsonSerializerOptions{WriteIndented=true,DefaultIgnoreCondition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,PropertyNamingPolicy=JsonNamingPolicy.CamelCase});
internal static JsonElement Schema => JsonDocument.Parse("""
{"type":"object","additionalProperties":false,"required":["id","title","systemPrompt","inputs","steps"],"properties":{
"id":{"type":"string"},"title":{"type":"string"},"systemPrompt":{"type":"string"},
"inputs":{"type":"array","maxItems":16,"items":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string"},"required":{"type":"boolean"},"default":{"type":"string"}}}},
"steps":{"type":"array","items":{"$ref":"#/$defs/step"}}
},"$defs":{"step":{"type":"object","additionalProperties":false,"required":["kind"],"properties":{
"kind":{"type":"string","enum":["chat","run","if"]},"prompt":{"type":"string"},"result":{"type":"string"},"scenario":{"type":"string"},"inputs":{"type":"object","additionalProperties":{"type":"string"}},"input":{"type":"string"},"equals":{"type":"string"},"then":{"type":"array","items":{"$ref":"#/$defs/step"}},"else":{"type":"array","items":{"$ref":"#/$defs/step"}}
}}}}
""").RootElement.Clone();
internal static string Render(string json,string name,string toolName)
{
var p=JsonSerializer.Deserialize<AddonScenarioDraft>(json,new JsonSerializerOptions{PropertyNameCaseInsensitive=true})??throw new InvalidDataException("Missing XML authoring plan.");
if(p.Id!=name||p.Inputs is null||p.Steps is null||p.Inputs.Length>16)throw new InvalidDataException("XML plan has wrong ID or missing bounded input/step arrays.");
int total=0;
XElement Step(AddonStep s,int depth)
{
if(++total>64||depth>6)throw new InvalidDataException("XML authoring plan exceeds bounds.");
if(s.Kind!="if"&&(s.Then is not null||s.Else is not null||s.Input is not null||s.EqualsValue is not null))throw new InvalidDataException("Only an if step can contain then/else/input/equals; run steps cannot contain following steps. Put the next step as a sibling in the surrounding array.");
if(s.Kind!="chat"&&s.Prompt is not null||s.Kind!="run"&&(s.Scenario is not null||s.Inputs is not null))throw new InvalidDataException("Step fields do not match its kind.");
if(s.Kind=="if"&&(s.Input is null||s.EqualsValue is null||s.Then is null||s.Result is not null))throw new InvalidDataException("If requires input, equals and then, and cannot declare result.");
XElement Result(XElement e){if(s.Result is {} result)e.SetAttributeValue("result",result);return e;}
return s.Kind switch
{
"chat"=>Result(new XElement("step",s.Prompt??throw new InvalidDataException("Chat requires prompt."))),
"run"=>Result(new XElement("run",new XAttribute("scenario",s.Scenario??""),(s.Inputs??[]).Select(i=>new XElement("input",new XAttribute("name",i.Key),new XAttribute("value",i.Value))))),
"if"=>new XElement("if",new XAttribute("input",s.Input??""),new XAttribute("equals",s.EqualsValue??""),new XElement("then",(s.Then??[]).Select(x=>Step(x,depth+1))),new XElement("else",(s.Else??[]).Select(x=>Step(x,depth+1)))),
_=>throw new InvalidDataException("XML plan step kind must be chat, run or if.")
};
}
var xml=new XElement("scenario",new XAttribute("schemaVersion","3"),new XAttribute("id",p.Id),new XAttribute("title",p.Title),
new XElement("inputs",p.Inputs.Select(i=>new XElement("input",new XAttribute("name",i.Name),i.Required?new XAttribute("required","true"):null,i.Default is null?null:new XAttribute("default",i.Default)))),
new XElement("systemPrompt",p.SystemPrompt),new XElement("tools",new XElement("allow",new XAttribute("name",toolName))),new XElement("steps",p.Steps.Select(s=>Step(s,0))),new XElement("exit",new XAttribute("mode","copy"),"Return the actual final result unchanged."));
string text=xml.ToString();ScenarioXmlParser.Parse(text);return text;
}
}