// ExecutorsDemo.cs
// Demonstrates Microsoft Agent Framework Workflows - Executors concepts:
// Basic Executor, Multiple Input Types, Function-Based Executors, and IWorkflowContext usage.
//
// Scenario: A warehouse inventory processing system.
// 1. Incoming shipments (ShipmentItem) are received by the ReceivingDockExecutor.
// 2. The ReceivingDockExecutor inspects items and sends them to QualityGateExecutor.
// 3. QualityGateExecutor handles multiple input types (ShipmentItem and BulkPallet).
// 4. A function-based executor calculates shelf labels.
// 5. The FinalLogExecutor yields the workflow output.
using System.Text.Json.Serialization;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
// ───────────────────────────────────────────────────────
// 1. DATA MODELS
// ───────────────────────────────────────────────────────
/// <summary>
/// Represents a single item arriving at the warehouse.
/// </summary>
public sealed class ShipmentItem
{
public string ProductName { get; set; } = string.Empty;
public string Sku { get; set; } = string.Empty;
public int Quantity { get; set; }
public double WeightKg { get; set; }
}
/// <summary>
/// Represents a bulk pallet containing multiple identical items.
/// </summary>
public sealed class BulkPallet
{
public string ProductName { get; set; } = string.Empty;
public string Sku { get; set; } = string.Empty;
public int PalletCount { get; set; }
public int ItemsPerPallet { get; set; }
}
/// <summary>
/// The result produced after quality inspection.
/// </summary>
public sealed class InspectionResult
{
public string Sku { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
public int TotalQuantity { get; set; }
public string Grade { get; set; } = string.Empty; // "A", "B", or "C"
public string Notes { get; set; } = string.Empty;
}
/// <summary>
/// A shelf label assigned to an inspected item.
/// </summary>
public sealed class ShelfLabel
{
public string Sku { get; set; } = string.Empty;
public string Location { get; set; } = string.Empty;
public string LabelText { get; set; } = string.Empty;
}
// ───────────────────────────────────────────────────────
// 2. BASIC EXECUTOR — Single MessageHandler, returning a value
// ───────────────────────────────────────────────────────
/// <summary>
/// Receiving dock executor: accepts a ShipmentItem, logs it, and forwards
/// an InspectionResult. Demonstrates the simplest executor pattern where the
/// return value is automatically sent to connected executors.
/// </summary>
internal sealed partial class ReceivingDockExecutor() : Executor("ReceivingDockExecutor")
{
[MessageHandler]
private ValueTask<InspectionResult> HandleAsync(ShipmentItem item, IWorkflowContext context)
{
// Assign a quality grade based on weight thresholds (simulated logic).
string grade = item.WeightKg switch
{
<= 5.0 => "A",
<= 20.0 => "B",
_ => "C"
};
var result = new InspectionResult
{
Sku = item.Sku,
ProductName = item.ProductName,
TotalQuantity = item.Quantity,
Grade = grade,
Notes = $"Single shipment received. Weight: {item.WeightKg} kg. Grade assigned: {grade}."
};
// Returning the value automatically sends it to connected executors via edges.
return ValueTask.FromResult(result);
}
}
// ───────────────────────────────────────────────────────
// 3. MULTIPLE INPUT TYPES — Two [MessageHandler] methods
// ───────────────────────────────────────────────────────
/// <summary>
/// Quality gate executor: handles both ShipmentItem and BulkPallet inputs.
/// Demonstrates defining multiple [MessageHandler] methods in one executor.
/// Each handler produces the same output type (InspectionResult) so that
/// downstream executors receive a uniform message regardless of source.
/// </summary>
internal sealed partial class QualityGateExecutor() : Executor("QualityGateExecutor")
{
[MessageHandler]
private ValueTask<InspectionResult> HandleItemAsync(ShipmentItem item, IWorkflowContext context)
{
string grade = item.WeightKg <= 10.0 ? "A" : "B";
return ValueTask.FromResult(new InspectionResult
{
Sku = item.Sku,
ProductName = item.ProductName,
TotalQuantity = item.Quantity,
Grade = grade,
Notes = $"Item inspected at quality gate. Weight {item.WeightKg} kg → Grade {grade}."
});
}
[MessageHandler]
private ValueTask<InspectionResult> HandlePalletAsync(BulkPallet pallet, IWorkflowContext context)
{
int totalItems = pallet.PalletCount * pallet.ItemsPerPallet;
string grade = totalItems > 500 ? "C" : "A";
return ValueTask.FromResult(new InspectionResult
{
Sku = pallet.Sku,
ProductName = pallet.ProductName,
TotalQuantity = totalItems,
Grade = grade,
Notes = $"Bulk pallet inspected: {pallet.PalletCount} pallets x {pallet.ItemsPerPallet} items = {totalItems}. Grade {grade}."
});
}
}
// ───────────────────────────────────────────────────────
// 4. EXECUTOR WITH MANUAL MESSAGE SENDING
// ───────────────────────────────────────────────────────
/// <summary>
/// Shelf assignment executor: receives an InspectionResult, determines a
/// warehouse location, and manually sends a ShelfLabel using context.SendMessageAsync().
/// Demonstrates the alternative to returning a value.
/// </summary>
internal sealed partial class ShelfAssignmentExecutor() : Executor("ShelfAssignmentExecutor")
{
[MessageHandler]
private async ValueTask HandleAsync(InspectionResult result, IWorkflowContext context)
{
// Determine shelf location based on grade
string location = result.Grade switch
{
"A" => "Aisle-1, Rack-A",
"B" => "Aisle-2, Rack-B",
_ => "Aisle-3, Rack-C (Bulk Storage)"
};
var label = new ShelfLabel
{
Sku = result.Sku,
Location = location,
LabelText = $"[{result.Sku}] {result.ProductName} — Qty: {result.TotalQuantity}, Grade: {result.Grade} → {location}"
};
// Manually send the message to connected executors instead of returning.
await context.SendMessageAsync(label);
}
}
// ───────────────────────────────────────────────────────
// 5. FINAL OUTPUT EXECUTOR — Uses YieldOutputAsync
// ───────────────────────────────────────────────────────
/// <summary>
/// Final log executor: receives a ShelfLabel and yields workflow output.
/// </summary>
internal sealed partial class FinalLogExecutor() : Executor("FinalLogExecutor")
{
[MessageHandler]
private async ValueTask HandleAsync(ShelfLabel label, IWorkflowContext context)
{
await context.YieldOutputAsync($"INVENTORY LOG: {label.LabelText}");
}
}
// ───────────────────────────────────────────────────────
// 6. FUNCTION-BASED EXECUTOR (BindExecutor)
// ───────────────────────────────────────────────────────
// Shown inside Program.Main below using the BindExecutor extension method.
// ───────────────────────────────────────────────────────
// 7. WORKFLOW BUILDER & EXECUTION
// ───────────────────────────────────────────────────────
public static class Program
{
private static async Task Main()
{
// ── Demo 1: Basic executor chain (return-based) ──────────
Console.WriteLine("══════ Demo 1: Basic Executor Chain ══════");
var receivingDock = new ReceivingDockExecutor();
var shelfAssignment = new ShelfAssignmentExecutor();
var finalLog = new FinalLogExecutor();
var workflow1 = new WorkflowBuilder(receivingDock)
.AddEdge(receivingDock, shelfAssignment)
.AddEdge(shelfAssignment, finalLog)
.WithOutputFrom(finalLog)
.Build<ShipmentItem>();
var item = new ShipmentItem
{
ProductName = "Wireless Keyboard",
Sku = "KB-7720",
Quantity = 150,
WeightKg = 3.2
};
StreamingRun run1 = await InProcessExecution.StreamAsync(workflow1, item);
await foreach (WorkflowEvent evt in run1.WatchStreamAsync())
{
if (evt is ExecutorInvokedEvent invoked)
Console.WriteLine($" [STARTED] {invoked.ExecutorId}");
else if (evt is ExecutorCompletedEvent completed)
Console.WriteLine($" [COMPLETED] {completed.ExecutorId}");
else if (evt is WorkflowOutputEvent output)
Console.WriteLine($" [OUTPUT] {output.Data}");
}
// ── Demo 2: Multiple input types ─────────────────────────
Console.WriteLine();
Console.WriteLine("══════ Demo 2: Multiple Input Types ══════");
var qualityGate = new QualityGateExecutor();
var shelfAssignment2 = new ShelfAssignmentExecutor();
var finalLog2 = new FinalLogExecutor();
var workflow2 = new WorkflowBuilder(qualityGate)
.AddEdge(qualityGate, shelfAssignment2)
.AddEdge(shelfAssignment2, finalLog2)
.WithOutputFrom(finalLog2)
.Build<BulkPallet>();
var pallet = new BulkPallet
{
ProductName = "USB-C Charger",
Sku = "CHG-4410",
PalletCount = 12,
ItemsPerPallet = 50
};
StreamingRun run2 = await InProcessExecution.StreamAsync(workflow2, pallet);
await foreach (WorkflowEvent evt in run2.WatchStreamAsync())
{
if (evt is WorkflowOutputEvent output)
Console.WriteLine($" [OUTPUT] {output.Data}");
}
// ── Demo 3: Function-based executor ──────────────────────
Console.WriteLine();
Console.WriteLine("══════ Demo 3: Function-Based Executor ══════");
// Create an executor from a simple function using BindExecutor.
Func<string, string> generateTag = sku => $"TAG-{sku.Replace("-", "").ToUpperInvariant()}-{DateTime.UtcNow:yyyyMMdd}";
var tagGenerator = generateTag.BindExecutor("TagGeneratorExecutor");
// A small executor to print the output
Func<string, string> printTag = tag => { Console.WriteLine($" [TAG] {tag}"); return tag; };
var printer = printTag.BindExecutor("PrinterExecutor");
var workflow3 = new WorkflowBuilder(tagGenerator)
.AddEdge(tagGenerator, printer)
.WithOutputFrom(printer)
.Build<string>();
Run run3 = await InProcessExecution.RunAsync(workflow3, "KB-7720");
foreach (WorkflowEvent evt in run3.NewEvents)
{
if (evt is WorkflowOutputEvent output)
Console.WriteLine($" [OUTPUT] {output.Data}");
}
}
}
// Expected output:
// ══════ Demo 1: Basic Executor Chain ══════
// [STARTED] ReceivingDockExecutor
// [COMPLETED] ReceivingDockExecutor
// [STARTED] ShelfAssignmentExecutor
// [COMPLETED] ShelfAssignmentExecutor
// [STARTED] FinalLogExecutor
// [COMPLETED] FinalLogExecutor
// [OUTPUT] INVENTORY LOG: [KB-7720] Wireless Keyboard — Qty: 150, Grade: A → Aisle-1, Rack-A
//
// ══════ Demo 2: Multiple Input Types ══════
// [OUTPUT] INVENTORY LOG: [CHG-4410] USB-C Charger — Qty: 600, Grade: C → Aisle-3, Rack-C (Bulk Storage)
//
// ══════ Demo 3: Function-Based Executor ══════
// [TAG] TAG-KB7720-20260225
// [OUTPUT] TAG-KB7720-20260225