V17 / d6d373561692
ScenarioFlowStep.cs
Lilith/Core/Scenarios/ScenarioFlowStep.cs
Unchanged from the preceding versionCS
using System.Xml.Linq;
namespace Lilith.Core.Scenarios;
public abstract record ScenarioFlowStep(string Description);
public sealed record ScenarioChatStep(string Prompt, string? Result = null) : ScenarioFlowStep(Prompt);
public sealed record ScenarioRunStep(string Scenario, IReadOnlyDictionary<string, string> Inputs, string? Result = null) : ScenarioFlowStep("Run " + Scenario);
public sealed record ScenarioToolStep(string Tool, IReadOnlyDictionary<string, string> Inputs, string Result) : ScenarioFlowStep("Call " + Tool);
public sealed record ScenarioResultStep(IReadOnlyDictionary<string, string> Fields) : ScenarioFlowStep("Return structured result");
public sealed record ScenarioForEachStep(string Items, string Item, string Collect, string Result, string Limit, int Attempts, string? SuccessProperty, string? IdentityProperty, ScenarioFlowStep[] Body) : ScenarioFlowStep("Process collection");
public sealed record ScenarioIfStep(string? Input, string? EqualsValue, string? NotEqualsValue, string? WorkspacePath, bool Exists,
ScenarioFlowStep[] Then, ScenarioFlowStep[] Else) : ScenarioFlowStep("Evaluate condition");
internal static class ScenarioFlowParser
{
internal static ScenarioFlowStep[] Parse(XElement section)
{
ScenarioFlowStep[] ParseSteps(XElement container) => container.Elements().Select(ParseStep).ToArray();
ScenarioFlowStep ParseStep(XElement element)
{
switch (element.Name.LocalName)
{
case "step":
ScenarioXmlParser.Shape(element, "step", ["result"], []);
if (string.IsNullOrWhiteSpace(element.Value) || element.Value.Length > 20000) throw new InvalidDataException("Invalid chat step.");
return new ScenarioChatStep(element.Value.Trim(), ScenarioBindings.ResultName(element));
case "tool":
ScenarioXmlParser.Shape(element, "tool", ["name", "result"], ["input"]);
string tool = (string?)element.Attribute("name") ?? "";
if (!ScenarioXmlParser.ValidName(tool)) throw new InvalidDataException("Invalid tool name.");
return new ScenarioToolStep(tool, ScenarioBindings.ReadFields(element, "input"), ScenarioBindings.ResultName(element) ?? throw new InvalidDataException("Tool needs result."));
case "result":
ScenarioXmlParser.Shape(element, "result", [], ["field"]);
return new ScenarioResultStep(ScenarioBindings.ReadFields(element, "field"));
case "foreach":
ScenarioXmlParser.Shape(element, "foreach", ["items", "item", "collect", "result", "limit", "attempts", "successProperty", "identityProperty"], ["step", "run", "tool", "if", "result"]);
string items = (string?)element.Attribute("items") ?? "", item = (string?)element.Attribute("item") ?? "", collect = (string?)element.Attribute("collect") ?? "";
string limit = (string?)element.Attribute("limit") ?? "5";
if (!ScenarioBindings.ValidReference(items) || !ScenarioXmlParser.ValidName(item) || !ScenarioXmlParser.ValidName(collect) || !int.TryParse((string?)element.Attribute("attempts") ?? "30", out int attempts) || attempts is < 1 or > 30 || !element.Elements().Any()) throw new InvalidDataException("Invalid bounded foreach.");
string? success = (string?)element.Attribute("successProperty");
if (success is not null && !ScenarioXmlParser.ValidName(success)) throw new InvalidDataException("Invalid success property.");
string? identity = (string?)element.Attribute("identityProperty");
if (identity is not null && !ScenarioXmlParser.ValidName(identity)) throw new InvalidDataException("Invalid identity property.");
return new ScenarioForEachStep(items, item, collect, ScenarioBindings.ResultName(element) ?? throw new InvalidDataException("Foreach needs result."), limit, attempts, success, identity, ParseSteps(element));
case "run":
ScenarioXmlParser.Shape(element, "run", ["scenario", "result"], ["input"]);
string name = (string?)element.Attribute("scenario") ?? "";
if (!ScenarioXmlParser.ValidName(name)) throw new InvalidDataException("Nested run requires a literal saved scenario name.");
var inputs = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var input in element.Elements())
{
ScenarioXmlParser.Shape(input, "input", ["name", "value"], []);
string key = (string?)input.Attribute("name") ?? "";
string? value = (string?)input.Attribute("value");
if (!ScenarioXmlParser.ValidName(key) || value is null || value.Length > 20000 || !inputs.TryAdd(key, value)) throw new InvalidDataException("Invalid nested input.");
}
if (inputs.Count > 32) throw new InvalidDataException("Too many nested inputs.");
return new ScenarioRunStep(name, inputs, ScenarioBindings.ResultName(element));
case "if":
ScenarioXmlParser.Shape(element, "if", ["input", "equals", "notEquals", "workspacePath", "exists"], ["then", "else"]);
string? inputName = (string?)element.Attribute("input"), path = (string?)element.Attribute("workspacePath");
string? equal = (string?)element.Attribute("equals"), unequal = (string?)element.Attribute("notEquals");
string? exists = (string?)element.Attribute("exists");
bool expected = true;
if (path is not null ? inputName is not null || equal is not null || unequal is not null || string.IsNullOrWhiteSpace(path) || (exists is not null && !bool.TryParse(exists, out expected))
: inputName is null || !ScenarioBindings.ValidReference(inputName) || (equal is null) == (unequal is null) || exists is not null)
throw new InvalidDataException("Use either input + equals/notEquals, or workspacePath + optional exists=true/false.");
var then = element.Elements("then").ToArray();
var otherwise = element.Elements("else").ToArray();
if (then.Length != 1 || otherwise.Length > 1) throw new InvalidDataException("A condition needs one then and at most one else.");
foreach (var branch in then.Concat(otherwise))
{
ScenarioXmlParser.Shape(branch, branch.Name.LocalName, [], ["step", "run", "if", "tool", "foreach", "result"]);
if (!branch.Elements().Any()) throw new InvalidDataException("Conditional branches cannot be empty.");
}
return new ScenarioIfStep(inputName, equal, unequal, path, expected, ParseSteps(then[0]), otherwise.Length == 0 ? [] : ParseSteps(otherwise[0]));
default: throw new InvalidDataException("Unknown scenario flow step.");
}
}
var result = ParseSteps(section);
if (Flatten(result).Count() > 64) throw new InvalidDataException("Too many scenario flow steps (maximum 64).");
return result;
}
internal static IEnumerable<ScenarioFlowStep> Flatten(IEnumerable<ScenarioFlowStep> steps)
{
foreach (var step in steps)
{
yield return step;
if (step is ScenarioIfStep condition)
foreach (var nested in Flatten(condition.Then.Concat(condition.Else))) yield return nested;
if (step is ScenarioForEachStep loop)
foreach (var nested in Flatten(loop.Body)) yield return nested;
}
}
}