Advanced Real-World

🎧 Customer Support Triage

A routing orchestrator classifies incoming support tickets and delegates to specialist sub-agents β€” each with their own domain tools, instructions, and expertise.

This is a realistic production pattern. A single TriageAgent receives every customer message, determines the category (billing, technical, or account), and routes to the right specialist. Each specialist has its own focused system prompt and dedicated tools, so it never has to deal with concerns outside its domain.

πŸ—οΈ Architecture: Triage Orchestrator β†’ [BillingAgent | TechnicalAgent | AccountAgent]
Each specialist is a full AIAgent registered as a tool via .AsAIFunction().

When to Use This Pattern

Use a Sub-Agent when…Use a Tool (function) when…
The task needs its own focused system prompt / personaThe operation is deterministic (database lookup, API call)
The task requires multi-step LLM reasoning internallyNo LLM reasoning is needed β€” just data retrieval or mutation
The domain is complex enough to benefit from specialist contextThe result is always the same given the same inputs
You want the sub-task to be independently testable and reusableSpeed matters β€” tools run in milliseconds without an LLM call
The sub-agent may itself call other tools or sub-agentsThe function is shared across multiple agents as a utility

NuGet Packages

dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
dotnet add package Azure.AI.OpenAI --prerelease
dotnet add package Azure.Identity

Full Example β€” Customer Support Triage System

using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

// ── Tool functions (deterministic β€” no LLM needed, so these are plain C# functions) ──

string GetInvoice(
    [Description("The invoice ID to look up")] string invoiceId)
    => $"Invoice #{invoiceId}: $149.99 due 2026-02-01. Status: Overdue.";

string ProcessRefund(
    [Description("The invoice ID to refund")] string invoiceId)
    => $"Refund for invoice #{invoiceId} has been initiated. Allow 3-5 business days.";

string GetServiceStatus(
    [Description("The service name, e.g. 'API', 'Dashboard', 'Auth'")] string service)
    => $"{service} is currently experiencing degraded performance in EU-West region.";

string ResetPassword(
    [Description("The customer account email address")] string email)
    => $"Password reset email sent to {email}. Link expires in 24 hours.";

string GetAccountDetails(
    [Description("The customer account email address")] string email)
    => $"Account for {email}: Plan=Pro, Seats=5, Renewal=2026-06-01, MFA=Enabled.";

// ── Azure OpenAI client ──
var azureClient = new AzureOpenAIClient(
    new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!),
    new AzureCliCredential());

// ── Specialist Agent 1: Billing ──
// Sub-agent because: needs domain expertise, multi-step reasoning (empathy + policy + refund decision)
AIAgent billingAgent = azureClient
    .GetChatClient("gpt-4o-mini")
    .AsAIAgent(
        instructions: """
            You are a billing specialist with deep knowledge of invoicing and refund policies.
            Always be empathetic and professional. When looking up invoices or processing refunds,
            use your tools. Explain charges clearly and offer solutions proactively.
            Never promise a refund before checking the invoice first.
            """,
        name: "BillingAgent",
        description: "Handles billing issues: invoice queries, payment problems, refund requests, subscription charges.",
        tools:
        [
            AIFunctionFactory.Create(GetInvoice),
            AIFunctionFactory.Create(ProcessRefund)
        ]);

// ── Specialist Agent 2: Technical Support ──
// Sub-agent because: needs systematic troubleshooting reasoning and domain knowledge
AIAgent technicalAgent = azureClient
    .GetChatClient("gpt-4o-mini")
    .AsAIAgent(
        instructions: """
            You are a technical support engineer. Diagnose problems methodically:
            1. Check service status first, 2. Gather symptoms, 3. Provide step-by-step fixes.
            Use your tools to check live service status. If an outage is detected, acknowledge it
            immediately and give an ETA if possible. Always end with a follow-up offer.
            """,
        name: "TechnicalAgent",
        description: "Handles technical issues: bugs, outages, API errors, integration problems, performance issues.",
        tools:
        [
            AIFunctionFactory.Create(GetServiceStatus)
        ]);

// ── Specialist Agent 3: Account Management ──
// Sub-agent because: handles sensitive account operations with its own compliance instructions
AIAgent accountAgent = azureClient
    .GetChatClient("gpt-4o-mini")
    .AsAIAgent(
        instructions: """
            You are an account management specialist. Help customers with account settings,
            security, and plan management. Always verify the customer's email before making changes.
            For security-sensitive operations (password reset, MFA), confirm the action explicitly
            before proceeding. Log all actions taken.
            """,
        name: "AccountAgent",
        description: "Handles account issues: password resets, MFA setup, plan upgrades, user management, profile changes.",
        tools:
        [
            AIFunctionFactory.Create(ResetPassword),
            AIFunctionFactory.Create(GetAccountDetails)
        ]);

// ── Triage Orchestrator ──
// This agent classifies and routes β€” it does NOT handle domain topics itself.
AIAgent triageAgent = azureClient
    .GetChatClient("gpt-4o-mini")
    .AsAIAgent(
        instructions: """
            You are the first point of contact for customer support. Your job is to:
            1. Greet the customer warmly.
            2. Understand their issue.
            3. Route to the correct specialist agent β€” do NOT try to resolve domain issues yourself.

            Routing rules:
            - Billing questions (invoices, payments, refunds, charges) β†’ BillingAgent
            - Technical problems (bugs, outages, API errors, slow performance) β†’ TechnicalAgent
            - Account issues (login, password, MFA, plan, profile) β†’ AccountAgent

            After the specialist responds, relay their answer to the customer in a friendly tone.
            If the issue spans multiple domains, call each relevant agent and synthesise the answers.
            """,
        tools:
        [
            billingAgent.AsAIFunction(),
            technicalAgent.AsAIFunction(),
            accountAgent.AsAIFunction()
        ]);

// ── Run the triage system ──

// Example 1: Billing issue β€” routed to BillingAgent
Console.WriteLine("=== Ticket 1: Billing ===");
Console.WriteLine(await triageAgent.RunAsync(
    "Hi, I was charged $149.99 for invoice #INV-2042 but I thought I cancelled. Can I get a refund?"));

Console.WriteLine();

// Example 2: Technical issue β€” routed to TechnicalAgent
Console.WriteLine("=== Ticket 2: Technical ===");
Console.WriteLine(await triageAgent.RunAsync(
    "The Dashboard has been loading slowly for the past hour. Is there an outage?"));

Console.WriteLine();

// Example 3: Mixed issue β€” triage calls BOTH AccountAgent and BillingAgent
Console.WriteLine("=== Ticket 3: Mixed ===");
Console.WriteLine(await triageAgent.RunAsync(
    "I can't log in to my account (user@example.com) and I also need to check my current plan details."));

Architecture Explained

Customer message
    β”‚
    β–Ό
[TriageAgent]  ← "routing brain" β€” classifies and delegates
    β”œβ”€ billing keywords  β†’ [BillingAgent]  ← GetInvoice(), ProcessRefund()
    β”œβ”€ technical issues  β†’ [TechnicalAgent] ← GetServiceStatus()
    └─ account problems  β†’ [AccountAgent]  ← ResetPassword(), GetAccountDetails()
                                                            β”‚
                                             Each specialist returns a resolution
    ◄────────────────────────────────────────────────────────
[TriageAgent] relays result to customer in a friendly tone

Why Tools for Data Lookups?

Notice that GetInvoice(), GetServiceStatus(), and similar helpers are plain C# functions, not sub-agents. They are fast, deterministic, and require no reasoning β€” they just look up data. The LLM reasoning happens around the data, inside the specialist agent's instructions. This keeps costs low and latency minimal.

Contrast this with the specialist agents themselves β€” they are sub-agents because each one needs a focused system prompt, domain expertise, and the ability to make multi-step decisions (e.g. check invoice, then decide whether refund policy applies, then process the refund, then draft a reply). That reasoning chain benefits from a dedicated LLM context and persona.

Key Design Decisions

  • The TriageAgent does NOT solve domain problems itself β€” its instructions explicitly say "do not resolve domain issues yourself". This keeps responsibilities clear and prevents the orchestrator from hallucinating domain knowledge.
  • Rich description strings on sub-agents β€” the triage LLM reads these to decide routing. Poor descriptions cause mis-routing. Treat them like job titles + responsibilities.
  • Mixed-issue support β€” Ticket 3 shows the orchestrator calling two sub-agents and synthesising a combined answer. This happens automatically.
  • Each specialist is independently testable β€” you can call billingAgent.RunAsync(...) directly in unit tests without the triage layer.

Next Steps