// EdgesDemo.cs
// Demonstrates Microsoft Agent Framework Workflows — Edge types:
// Direct, Conditional, Switch-Case, Fan-Out (Multi-Selection), and Fan-In.
//
// Scenario: A ticket routing system for a tech support centre.
// Tickets arrive with a priority (Low, Normal, High, Critical).
// The workflow classifies them and routes to the correct handler(s).
using Microsoft.Agents.AI.Workflows;
// ── Data Models ─────────────────────────────────────────
public sealed class Ticket
{
public int Id { get; set; }
public string Subject { get; set; } = string.Empty;
public string Priority { get; set; } = string.Empty; // Low | Normal | High | Critical
}
public sealed class ClassifiedTicket
{
public int Id { get; set; }
public string Subject { get; set; } = string.Empty;
public string Priority { get; set; } = string.Empty;
public string AssignedTeam { get; set; } = string.Empty;
}
// ── Executors ───────────────────────────────────────────
internal sealed partial class ClassifierExecutor() : Executor("Classifier")
{
[MessageHandler]
private ValueTask<ClassifiedTicket> HandleAsync(Ticket ticket, IWorkflowContext ctx)
{
string team = ticket.Priority switch
{
"Critical" => "Escalation",
"High" => "Senior",
"Normal" => "General",
_ => "Self-Service"
};
return ValueTask.FromResult(new ClassifiedTicket
{
Id = ticket.Id,
Subject = ticket.Subject,
Priority = ticket.Priority,
AssignedTeam = team
});
}
}
internal sealed partial class SelfServiceExecutor() : Executor("SelfService")
{
[MessageHandler]
private async ValueTask HandleAsync(ClassifiedTicket t, IWorkflowContext ctx)
=> await ctx.YieldOutputAsync($"[SELF-SERVICE] Ticket #{t.Id} auto-replied.");
}
internal sealed partial class GeneralExecutor() : Executor("General")
{
[MessageHandler]
private async ValueTask HandleAsync(ClassifiedTicket t, IWorkflowContext ctx)
=> await ctx.YieldOutputAsync($"[GENERAL] Ticket #{t.Id} assigned to general queue.");
}
internal sealed partial class SeniorExecutor() : Executor("Senior")
{
[MessageHandler]
private async ValueTask HandleAsync(ClassifiedTicket t, IWorkflowContext ctx)
=> await ctx.YieldOutputAsync($"[SENIOR] Ticket #{t.Id} assigned to senior engineer.");
}
internal sealed partial class EscalationExecutor() : Executor("Escalation")
{
[MessageHandler]
private async ValueTask HandleAsync(ClassifiedTicket t, IWorkflowContext ctx)
=> await ctx.YieldOutputAsync($"[ESCALATION] Ticket #{t.Id} paged on-call team!");
}
internal sealed partial class LoggerExecutor() : Executor("Logger")
{
[MessageHandler]
private async ValueTask HandleAsync(ClassifiedTicket t, IWorkflowContext ctx)
=> await ctx.YieldOutputAsync($"[LOG] Ticket #{t.Id} ({t.Priority}) → {t.AssignedTeam}");
}
// ── Condition helpers ───────────────────────────────────
static class TicketConditions
{
public static Func<object?, bool> PriorityIs(string priority) =>
msg => msg is ClassifiedTicket t && t.Priority == priority;
}
// ── Demo runner ─────────────────────────────────────────
public static class Program
{
private static async Task Main()
{
await Demo1_DirectEdge();
await Demo2_ConditionalEdge();
await Demo3_SwitchCase();
await Demo4_FanOut();
}
// Demo 1: Direct Edge (Classifier → Logger)
private static async Task Demo1_DirectEdge()
{
Console.WriteLine("══════ Demo 1: Direct Edge ══════");
var classifier = new ClassifierExecutor();
var logger = new LoggerExecutor();
var workflow = new WorkflowBuilder(classifier)
.AddEdge(classifier, logger)
.WithOutputFrom(logger)
.Build<Ticket>();
await RunAsync(workflow, new Ticket { Id = 1, Subject = "Printer jam", Priority = "Normal" });
Console.WriteLine();
}
// Demo 2: Conditional Edges (Classifier → High|Normal)
private static async Task Demo2_ConditionalEdge()
{
Console.WriteLine("══════ Demo 2: Conditional Edges ══════");
var classifier = new ClassifierExecutor();
var senior = new SeniorExecutor();
var general = new GeneralExecutor();
var workflow = new WorkflowBuilder(classifier)
.AddEdge(classifier, senior, condition: TicketConditions.PriorityIs("High"))
.AddEdge(classifier, general, condition: TicketConditions.PriorityIs("Normal"))
.WithOutputFrom(senior, general)
.Build<Ticket>();
await RunAsync(workflow, new Ticket { Id = 2, Subject = "VPN down", Priority = "High" });
await RunAsync(workflow, new Ticket { Id = 3, Subject = "Password reset", Priority = "Normal" });
Console.WriteLine();
}
// Demo 3: Switch-Case Edge
private static async Task Demo3_SwitchCase()
{
Console.WriteLine("══════ Demo 3: Switch-Case Edge ══════");
var classifier = new ClassifierExecutor();
var selfService = new SelfServiceExecutor();
var general = new GeneralExecutor();
var senior = new SeniorExecutor();
var escalation = new EscalationExecutor();
var builder = new WorkflowBuilder(classifier);
builder.AddSwitch(classifier, sw => sw
.AddCase(TicketConditions.PriorityIs("Critical"), escalation)
.AddCase(TicketConditions.PriorityIs("High"), senior)
.AddCase(TicketConditions.PriorityIs("Normal"), general)
.WithDefault(selfService)
)
.WithOutputFrom(selfService, general, senior, escalation);
var workflow = builder.Build<Ticket>();
await RunAsync(workflow, new Ticket { Id = 4, Subject = "Server fire", Priority = "Critical" });
await RunAsync(workflow, new Ticket { Id = 5, Subject = "Slow laptop", Priority = "Low" });
Console.WriteLine();
}
// Demo 4: Fan-Out (Multi-Selection)
private static async Task Demo4_FanOut()
{
Console.WriteLine("══════ Demo 4: Fan-Out (Multi-Selection) ══════");
var classifier = new ClassifierExecutor();
var escalation = new EscalationExecutor();
var senior = new SeniorExecutor();
var general = new GeneralExecutor();
var logger = new LoggerExecutor();
Func<ClassifiedTicket?, int, IEnumerable<int>> selector = (ticket, _) =>
{
if (ticket is null) return [2];
return ticket.Priority switch
{
"Critical" => [0, 3], // escalation + logger
"High" => [1, 3], // senior + logger
_ => [2] // general only
};
};
var builder = new WorkflowBuilder(classifier);
builder.AddFanOutEdge(classifier,
targets: [escalation, senior, general, logger],
targetSelector: selector)
.WithOutputFrom(escalation, senior, general, logger);
var workflow = builder.Build<Ticket>();
await RunAsync(workflow, new Ticket { Id = 6, Subject = "Database crash", Priority = "Critical" });
await RunAsync(workflow, new Ticket { Id = 7, Subject = "Email lag", Priority = "High" });
await RunAsync(workflow, new Ticket { Id = 8, Subject = "New mouse", Priority = "Normal" });
Console.WriteLine();
}
private static async Task RunAsync(Workflow<Ticket> workflow, Ticket ticket)
{
StreamingRun run = await InProcessExecution.StreamAsync(workflow, ticket);
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
if (evt is WorkflowOutputEvent e)
Console.WriteLine($" {e.Data}");
}
}
}
// Expected output:
// ══════ Demo 1: Direct Edge ══════
// [LOG] Ticket #1 (Normal) → General
//
// ══════ Demo 2: Conditional Edges ══════
// [SENIOR] Ticket #2 assigned to senior engineer.
// [GENERAL] Ticket #3 assigned to general queue.
//
// ══════ Demo 3: Switch-Case Edge ══════
// [ESCALATION] Ticket #4 paged on-call team!
// [SELF-SERVICE] Ticket #5 auto-replied.
//
// ══════ Demo 4: Fan-Out (Multi-Selection) ══════
// [ESCALATION] Ticket #6 paged on-call team!
// [LOG] Ticket #6 (Critical) → Escalation
// [SENIOR] Ticket #7 assigned to senior engineer.
// [LOG] Ticket #7 (High) → Senior
// [GENERAL] Ticket #8 assigned to general queue.