V7 / 2a442462cf3f
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) : ScenarioFlowStep(Prompt);
public sealed record ScenarioRunStep(string Scenario, IReadOnlyDictionary<string, string> Inputs) : ScenarioFlowStep("Run " + Scenario);
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", [], []);
if (string.IsNullOrWhiteSpace(element.Value) || element.Value.Length > 20000) throw new InvalidDataException("Invalid chat step.");
return new ScenarioChatStep(element.Value.Trim());
case "run":
ScenarioXmlParser.Shape(element, "run", ["scenario"], ["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);
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 || !ScenarioXmlParser.ValidName(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"]);
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;
}
}
}