Skip to main content
Customer-facing systems place agentic design under direct human scrutiny. Unlike code agents that iterate against objective tests, customer systems must satisfy subjective human expectations while handling the full variety of human communication. Unlike research systems that operate asynchronously, customer systems often require real-time response. And unlike internal tools where failures are inconvenient, customer system failures directly impact user experience and business outcomes. This chapter builds a customer service system that classifies inquiries, retrieves relevant knowledge, generates appropriate responses, and escalates when confidence is low. The system learns from resolutions to improve over time, accumulating expertise that makes it more capable with use.

14.1 The Customer Service Challenge

Customer service has properties that make it both suitable and challenging for automation. High volume, repetitive queries. Many customer inquiries follow patterns—password resets, order status, billing questions, feature explanations. These repetitive queries are ideal for automation because the system can learn effective responses and apply them consistently. Variable complexity. While many queries are straightforward, some are complex, ambiguous, or emotionally charged. A system must recognize when a query exceeds its capabilities and route to human agents appropriately. Quality expectations. Customers expect accurate information, appropriate tone, and genuine helpfulness. A response that’s technically correct but cold or confusing fails the customer even if it answers their question. Stakes matter. Stakes matter because poor customer service costs revenue, drives customers away, and can create legal liability when it spreads misinformation. The system must be reliable enough to trust with real customer interactions. These properties shape the design: classify to route appropriately, retrieve knowledge to ground responses, generate with appropriate tone, evaluate confidence to know when to escalate, and learn from outcomes to improve over time.

14.2 System Architecture

The customer system routes inquiries through specialized handlers based on classification, with escalation paths for uncertain or complex cases.
┌─────────────────────────────────────────────────────────────────────────┐
│                        Customer System                                   │
│                                                                         │
│   Customer Inquiry                                                      │
│         │                                                               │
│         ▼                                                               │
│   ┌─────────────┐                                                      │
│   │ Classifier  │                                                      │
│   └─────────────┘                                                      │
│         │                                                               │
│         ├─────────────┬─────────────┬─────────────┐                    │
│         ▼             ▼             ▼             ▼                    │
│   ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐             │
│   │    FAQ    │ │  Account  │ │ Technical │ │  Billing  │             │
│   │  Handler  │ │  Handler  │ │  Handler  │ │  Handler  │             │
│   └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘             │
│         │             │             │             │                    │
│         └─────────────┴─────────────┴─────────────┘                    │
│                       │                                                 │
│                       ▼                                                 │
│              ┌────────────────┐        ┌────────────────┐              │
│              │   Confidence   │───────▶│   Escalation   │              │
│              │     Check      │  Low   │    to Human    │              │
│              └────────────────┘        └────────────────┘              │
│                       │ High                                            │
│                       ▼                                                 │
│              ┌────────────────┐                                        │
│              │    Response    │                                        │
│              │   Generation   │                                        │
│              └────────────────┘                                        │
│                       │                                                 │
│                       ▼                                                 │
│   ┌────────────────────────────────────────────────────────────────┐  │
│   │                     Knowledge Base                              │  │
│   │                 (FAQs, Docs, Resolution Patterns)               │  │
│   └────────────────────────────────────────────────────────────────┘  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
The Classifier analyzes incoming inquiries to determine intent and route to the appropriate handler. Classification happens quickly and determines the entire subsequent flow. Specialized Handlers process inquiries within their domain. Each handler has access to relevant tools, knowledge, and response patterns for its category. Confidence Checking evaluates whether the generated response should be delivered or escalated. In this chapter, confidence is a scalar between 0 and 1 computed by a separate evaluation prompt that scores relevance, accuracy, completeness, and tone, then aggregates these into an overall confidence score used for routing decisions. Low confidence triggers human review. Knowledge Base provides grounding for responses—FAQs, documentation, previous resolutions, and account information. The knowledge base is a combination of a vector index over FAQs and docs plus a keyed store for resolutions. Retrieval uses semantic search over this index and returns documents with a relevance score between 0 and 1.

14.3 Intent Classification

Classification is the routing decision that determines everything downstream. A misclassified inquiry goes to the wrong handler, uses wrong knowledge, and produces wrong responses.
interface Intent {
  category: 'faq' | 'account' | 'technical' | 'billing' | 'complaint' | 'other';
  confidence: number;
  entities: Entity[];
  sentiment: 'positive' | 'neutral' | 'negative' | 'frustrated';
  urgency: 'low' | 'medium' | 'high';
}

interface Entity {
  type: 'order_id' | 'product' | 'feature' | 'date' | 'amount' | 'email';
  value: string;
  confidence: number;
}

export default class InquiryClassifier extends AgenticSystem {
  @field categories: CategoryDefinition[] = [
    {
      name: 'faq',
      description: 'General questions about products, features, or policies',
      examples: ['How do I reset my password?', 'What are your business hours?', 'Do you ship internationally?']
    },
    {
      name: 'account',
      description: 'Questions about user accounts, profiles, or settings',
      examples: ['I need to update my email', 'How do I delete my account?', 'My account was locked']
    },
    {
      name: 'technical',
      description: 'Technical issues, bugs, or troubleshooting',
      examples: ['The app keeps crashing', 'I get an error when I try to save', 'The sync is not working']
    },
    {
      name: 'billing',
      description: 'Payment, subscription, refund, or pricing questions',
      examples: ['I was charged twice', 'How do I cancel my subscription?', 'Can I get a refund?']
    },
    {
      name: 'complaint',
      description: 'Expressions of dissatisfaction or frustration',
      examples: ['This is unacceptable', 'I want to speak to a manager', 'Your service is terrible']
    }
  ];

  async classify(inquiry: string, customerContext?: CustomerContext): Promise<Intent> {
    // Extract entities first
    const entities = await this.extractEntities(inquiry);

    // Classify with context
    const contextInfo = customerContext
      ? `Customer has been with us ${customerContext.tenureDays} days, has ${customerContext.openTickets} open tickets, last contacted ${customerContext.daysSinceLastContact} days ago.`
      : '';

    const completion = await llm.complete([
      { role: 'system', content: `Classify this customer inquiry.
        Categories: ${this.categories.map(c => `${c.name}: ${c.description}`).join('\n')}

        Return JSON only with no additional text:
        {
          "category": "...",
          "confidence": 0.0-1.0,
          "sentiment": "positive|neutral|negative|frustrated",
          "urgency": "low|medium|high",
          "reasoning": "..."
        }` },
      { role: 'user', content: `Inquiry: ${inquiry}\n\n${contextInfo}` }
    ]);

    const raw = completion.choices[0].message.content;
    const classification = JSON.parse(raw);

    return {
      category: classification.category,
      confidence: classification.confidence,
      entities,
      sentiment: classification.sentiment,
      urgency: classification.urgency
    };
  }

  private async extractEntities(inquiry: string): Promise<Entity[]> {
    const completion = await llm.complete([
      { role: 'system', content: `Extract entities from this customer inquiry.
        Entity types: order_id, product, feature, date, amount, email
        Return a JSON array only with no additional text, for example:
        [
          {"type": "order_id", "value": "123-456", "confidence": 0.92}
        ]` },
      { role: 'user', content: inquiry }
    ]);

    const raw = completion.choices[0].message.content;
    return JSON.parse(raw);
  }

  // Route based on classification
  routeToHandler(intent: Intent): string {
    // Complaints always get special handling
    if (intent.category === 'complaint' || intent.sentiment === 'frustrated') {
      return 'complaint';
    }

    // High urgency gets escalated faster
    if (intent.urgency === 'high' && intent.confidence < 0.8) {
      return 'escalation';
    }

    // Route by category
    return intent.category;
  }
}

interface CategoryDefinition {
  name: string;
  description: string;
  examples: string[];
}

interface CustomerContext {
  customerId: string;
  tenureDays: number;
  openTickets: number;
  daysSinceLastContact: number;
  previousCategories: string[];
}
Classification extracts multiple signals: the primary category, confidence level, sentiment, and urgency. These combine to inform routing decisions. A billing question from a frustrated, long-term customer gets different handling than a casual FAQ from a new user. This classifier is probabilistic and will mislabel some inquiries. In production, you should log disagreements between human agents and the classifier, then periodically retrain or adjust prompts based on those errors.

14.4 Knowledge-Grounded Response

Handlers generate responses grounded in retrieved knowledge. This prevents hallucination—responses cite actual documentation, policies, and facts rather than generating plausible-sounding fiction.
export default class FAQHandler extends AgenticSystem {
  @field knowledgeBase: KnowledgeBase;
  @field responsePatterns: ResponsePattern[] = [];

  async handle(inquiry: string, intent: Intent, context: CustomerContext): Promise<HandlerResponse> {
    // Retrieve relevant knowledge
    const knowledge = await this.retrieveKnowledge(inquiry, intent);

    if (knowledge.length === 0) {
      return {
        canHandle: false,
        reason: 'No relevant knowledge found',
        suggestedEscalation: 'Route to human agent'
      };
    }

    // Generate response grounded in knowledge
    const response = await this.generateResponse(inquiry, intent, knowledge, context);

    // Evaluate response quality
    const evaluation = await this.evaluateResponse(response, inquiry, knowledge);

    return {
      canHandle: evaluation.confidence > 0.7,
      response: response,
      confidence: evaluation.confidence,
      sources: knowledge.map(k => k.source),
      evaluation
    };
  }

  private async retrieveKnowledge(inquiry: string, intent: Intent): Promise<KnowledgeItem[]> {
    const results: KnowledgeItem[] = [];

    // Search FAQ database
    const faqResults = await this.knowledgeBase.searchFAQs(inquiry, 5);
    results.push(...faqResults);

    // Search documentation
    const docResults = await this.knowledgeBase.searchDocs(inquiry, 3);
    results.push(...docResults);

    // Search previous resolutions for similar inquiries
    const resolutionResults = await this.knowledgeBase.searchResolutions(inquiry, 3);
    results.push(...resolutionResults);

    // Filter by relevance threshold
    return results
      .filter(r => r.relevance > 0.6)
      .sort((a, b) => b.relevance - a.relevance)
      .slice(0, 5);
  }

  private async generateResponse(
    inquiry: string,
    intent: Intent,
    knowledge: KnowledgeItem[],
    context: CustomerContext
  ): Promise<string> {
    const toneGuidance = this.getToneGuidance(intent, context);

    const completion = await llm.complete([
      { role: 'system', content: `You are a helpful customer service representative.
        ${toneGuidance}

        IMPORTANT: Only use information from the provided knowledge base.
        If the knowledge doesn't fully answer the question, acknowledge what you can answer
        and note what additional help they might need.

        Never make up information about products, policies, or procedures.` },
      { role: 'user', content: `Customer inquiry: ${inquiry}

        Relevant knowledge:
        ${knowledge.map((k, i) => `[${i + 1}] ${k.content}`).join('\n\n')}

        Generate a helpful response.` }
    ]);

    const text = completion.choices[0].message.content;
    return text;
  }

  private getToneGuidance(intent: Intent, context: CustomerContext): string {
    const guidelines: string[] = [];

    // Sentiment-based guidance
    if (intent.sentiment === 'frustrated') {
      guidelines.push('Acknowledge their frustration empathetically before addressing the issue.');
      guidelines.push('Use phrases like "I understand this is frustrating" or "I apologize for the inconvenience."');
    } else if (intent.sentiment === 'positive') {
      guidelines.push('Match their positive energy while being helpful.');
    }

    // Context-based guidance
    if (context.tenureDays > 365) {
      guidelines.push('This is a long-term customer. Show appreciation for their loyalty.');
    }

    if (context.openTickets > 2) {
      guidelines.push('They have multiple open issues. Acknowledge this and ensure this inquiry is fully resolved.');
    }

    return guidelines.length > 0
      ? `Tone guidance:\n${guidelines.map(g => `- ${g}`).join('\n')}`
      : 'Be professional, friendly, and helpful.';
  }

  private async evaluateResponse(
    response: string,
    inquiry: string,
    knowledge: KnowledgeItem[]
  ): Promise<ResponseEvaluation> {
    const completion = await llm.complete([
      { role: 'system', content: `Evaluate this customer service response.
        Score each dimension 0-1 and provide overall confidence.

        Return JSON only with no comments or extra text:
        {
          "relevance": 0.0-1.0,
          "accuracy": 0.0-1.0,
          "completeness": 0.0-1.0,
          "tone": 0.0-1.0,
          "confidence": 0.0-1.0,
          "issues": ["..."]
        }` },
      { role: 'user', content: `Inquiry: ${inquiry}

        Response: ${response}

        Knowledge used:
        ${knowledge.map(k => k.content).join('\n')}` }
    ]);

    const raw = completion.choices[0].message.content;
    return JSON.parse(raw);
  }
}

interface KnowledgeItem {
  content: string;
  source: string;
  type: 'faq' | 'doc' | 'resolution';
  relevance: number;
}

interface HandlerResponse {
  canHandle: boolean;
  response?: string;
  confidence?: number;
  sources?: string[];
  evaluation?: ResponseEvaluation;
  reason?: string;
  suggestedEscalation?: string;
}

interface ResponseEvaluation {
  relevance: number;
  accuracy: number;
  completeness: number;
  tone: number;
  confidence: number;
  issues: string[];
}

interface ResponsePattern {
  category: string;
  pattern: string;
  successRate: number;
}
Response generation is explicitly grounded in retrieved knowledge. The system prompt instructs the model not to introduce facts that do not appear in the retrieved documents. The evaluation checks whether the response actually uses the provided knowledge and addresses the inquiry.

14.5 Confidence-Based Escalation

Not every inquiry should be handled automatically. The system must know when to escalate to human agents.
export default class EscalationManager extends AgenticSystem {
  @field thresholds = {
    confidenceMinimum: 0.7,      // Below this, always escalate
    sentimentEscalation: true,   // Escalate frustrated customers
    repeatInquiryLimit: 2,       // Escalate after N failed attempts
    highValueCustomer: true      // Escalate VIP customers
  };

  @field escalationQueue: EscalationTicket[] = [];

  shouldEscalate(
    intent: Intent,
    response: HandlerResponse,
    context: CustomerContext
  ): EscalationDecision {
    const reasons: string[] = [];

    // Confidence too low
    if (response.confidence && response.confidence < this.thresholds.confidenceMinimum) {
      reasons.push(`Low confidence: ${Math.round(response.confidence * 100)}%`);
    }

    // Handler couldn't handle
    if (!response.canHandle) {
      reasons.push(response.reason || 'Handler unable to process');
    }

    // Frustrated customer
    if (this.thresholds.sentimentEscalation && intent.sentiment === 'frustrated') {
      reasons.push('Customer sentiment: frustrated');
    }

    // Repeat contact
    if (context.openTickets >= this.thresholds.repeatInquiryLimit) {
      reasons.push(`Repeat contact: ${context.openTickets} open tickets`);
    }

    // Complaint category
    if (intent.category === 'complaint') {
      reasons.push('Complaint requires human attention');
    }

    // High urgency with any uncertainty
    if (intent.urgency === 'high' && response.confidence && response.confidence < 0.9) {
      reasons.push('High urgency with less than 90% confidence');
    }

    return {
      shouldEscalate: reasons.length > 0,
      reasons,
      priority: this.calculatePriority(intent, context, reasons),
      suggestedAgent: this.suggestAgent(intent, context)
    };
  }

  private calculatePriority(
    intent: Intent,
    context: CustomerContext,
    reasons: string[]
  ): 'low' | 'medium' | 'high' | 'urgent' {
    let score = 0;

    // Intent factors
    if (intent.urgency === 'high') score += 3;
    if (intent.sentiment === 'frustrated') score += 2;
    if (intent.category === 'complaint') score += 2;

    // Context factors
    if (context.tenureDays > 365) score += 1; // Long-term customer
    if (context.openTickets > 2) score += 1;  // Multiple issues

    // Reason factors
    if (reasons.some(r => r.includes('confidence'))) score += 1;

    if (score >= 6) return 'urgent';
    if (score >= 4) return 'high';
    if (score >= 2) return 'medium';
    return 'low';
  }

  private suggestAgent(intent: Intent, context: CustomerContext): string {
    // Match agent specialty to inquiry type
    if (intent.category === 'technical') return 'technical_support';
    if (intent.category === 'billing') return 'billing_specialist';
    if (intent.category === 'complaint') return 'customer_success';
    return 'general_support';
  }

  async createEscalation(
    inquiry: string,
    intent: Intent,
    response: HandlerResponse,
    context: CustomerContext,
    decision: EscalationDecision
  ): Promise<EscalationTicket> {
    const ticket: EscalationTicket = {
      id: crypto.randomUUID(),
      customerId: context.customerId,
      inquiry,
      intent,
      attemptedResponse: response.response,
      escalationReasons: decision.reasons,
      priority: decision.priority,
      suggestedAgent: decision.suggestedAgent,
      createdAt: Date.now(),
      status: 'pending'
    };

    this.escalationQueue.push(ticket);

    // Generate handoff summary for human agent
    ticket.handoffSummary = await this.generateHandoffSummary(ticket);

    return ticket;
  }

  private async generateHandoffSummary(ticket: EscalationTicket): Promise<string> {
    const completion = await llm.complete([
      { role: 'system', content: `Create a brief summary for a human agent taking over this ticket.
        Include:
        - What the customer is asking
        - Why it was escalated
        - What was attempted
        - Suggested approach` },
      { role: 'user', content: `Inquiry: ${ticket.inquiry}

        Intent: ${ticket.intent.category} (${Math.round(ticket.intent.confidence * 100)}% confident)
        Sentiment: ${ticket.intent.sentiment}
        Urgency: ${ticket.intent.urgency}

        Escalation reasons: ${ticket.escalationReasons.join(', ')}

        ${ticket.attemptedResponse ? `Attempted response:\n${ticket.attemptedResponse}` : 'No response was generated.'}` }
    ]);

    const summary = completion.choices[0].message.content;
    return summary;
  }
}

interface EscalationDecision {
  shouldEscalate: boolean;
  reasons: string[];
  priority: 'low' | 'medium' | 'high' | 'urgent';
  suggestedAgent: string;
}

interface EscalationTicket {
  id: string;
  customerId: string;
  inquiry: string;
  intent: Intent;
  attemptedResponse?: string;
  escalationReasons: string[];
  priority: 'low' | 'medium' | 'high' | 'urgent';
  suggestedAgent: string;
  handoffSummary?: string;
  createdAt: number;
  status: 'pending' | 'assigned' | 'resolved';
}
Escalation is multi-factor. Low confidence alone might not trigger escalation, but low confidence plus frustrated sentiment plus repeat contact definitely does. In a live system, escalation decisions must also respect queue capacity. You may cap simultaneous escalations or dynamically adjust thresholds when human queues are saturated, trading off speed against automation. The handoff summary ensures human agents have context to continue the conversation effectively.

14.6 Learning from Resolutions

The system increases its automated resolution rate by capturing successful human responses and reusing them as templates or retrieval items. When a human agent resolves an escalated ticket, or when customer feedback indicates satisfaction, the system captures what worked.
export default class ResolutionLearner extends AgenticSystem {
  @field knowledgeBase: KnowledgeBase;
  @field patterns: ResolutionPattern[] = [];

  async learnFromResolution(
    ticket: ResolvedTicket,
    resolution: Resolution,
    feedback: CustomerFeedback | null
  ) {
    // Only learn from successful resolutions
    if (!this.isSuccessfulResolution(resolution, feedback)) {
      return;
    }

    // Extract learnable patterns
    const patterns = await this.extractPatterns(ticket, resolution);

    for (const pattern of patterns) {
      // Check if this updates an existing pattern
      const existing = this.patterns.find(p =>
        p.category === pattern.category &&
        p.similarity(pattern) > 0.8
      );

      if (existing) {
        // Reinforce existing pattern
        existing.successCount += 1;
        existing.lastUsed = Date.now();
      } else {
        // Add new pattern
        this.patterns.push(pattern);
      }
    }

    // Store successful response as potential template
    if (resolution.response && feedback?.satisfaction === 'satisfied') {
      await this.knowledgeBase.storeResolution({
        inquiry: ticket.inquiry,
        response: resolution.response,
        category: ticket.intent.category,
        successScore: feedback.satisfaction === 'satisfied' ? 1 : 0.5,
        timestamp: Date.now()
      });
    }
  }

  private isSuccessfulResolution(resolution: Resolution, feedback: CustomerFeedback | null): boolean {
    // Resolution marked as successful by agent
    if (resolution.status === 'resolved' && resolution.successIndicator) {
      return true;
    }

    // Positive customer feedback
    if (feedback?.satisfaction === 'satisfied') {
      return true;
    }

    return false;
  }

  private async extractPatterns(ticket: ResolvedTicket, resolution: Resolution): Promise<ResolutionPattern[]> {
    const completion = await llm.complete([
      { role: 'system', content: `Extract reusable patterns from this successful customer service resolution.
        Focus on:
        - What made the response effective
        - Key phrases or approaches that worked
        - How the issue was resolved
        Return a JSON array only with no prose, for example:
        [
          {
            "description": "Apologize and restate the problem in your own words",
            "applicableTo": "billing confusion",
            "approach": "acknowledge issue, clarify charges, offer adjustment where appropriate"
          }
        ]` },
      { role: 'user', content: `Original inquiry: ${ticket.inquiry}
        Category: ${ticket.intent.category}
        Sentiment: ${ticket.intent.sentiment}

        Successful resolution:
        ${resolution.response}

        Agent notes: ${resolution.notes || 'None'}` }
    ]);

    const raw = completion.choices[0].message.content;
    const extracted = JSON.parse(raw);
    return extracted.map((p: any) => ({
      id: crypto.randomUUID(),
      category: ticket.intent.category,
      description: p.description,
      applicableTo: p.applicableTo,
      approach: p.approach,
      successCount: 1,
      createdAt: Date.now(),
      lastUsed: Date.now(),
      similarity: (other: ResolutionPattern) => this.calculateSimilarity(p, other)
    }));
  }

  private calculateSimilarity(a: any, b: ResolutionPattern): number {
    // Simple similarity based on description overlap
    const aWords = new Set(a.description.toLowerCase().split(/\W+/));
    const bWords = new Set(b.description.toLowerCase().split(/\W+/));
    const intersection = [...aWords].filter(w => bWords.has(w));
    const union = new Set([...aWords, ...bWords]);
    return intersection.length / union.size;
  }

  // Apply learned patterns to new inquiries
  async getRelevantPatterns(intent: Intent, inquiry: string): Promise<ResolutionPattern[]> {
    return this.patterns
      .filter(p => p.category === intent.category)
      .filter(p => p.successCount >= 2) // Only patterns with multiple successes
      .sort((a, b) => b.successCount - a.successCount)
      .slice(0, 3);
  }
}

interface ResolvedTicket {
  inquiry: string;
  intent: Intent;
  customerId: string;
}

interface Resolution {
  response: string;
  status: 'resolved' | 'unresolved' | 'escalated';
  successIndicator: boolean;
  notes?: string;
  resolvedBy: 'system' | 'human';
  resolvedAt: number;
}

interface CustomerFeedback {
  satisfaction: 'satisfied' | 'neutral' | 'dissatisfied';
  comment?: string;
}

interface ResolutionPattern {
  id: string;
  category: string;
  description: string;
  applicableTo: string;
  approach: string;
  successCount: number;
  createdAt: number;
  lastUsed: number;
  similarity: (other: ResolutionPattern) => number;
}
The patterns extracted here only matter if they feed back into the rest of the system. In practice, you would use getRelevantPatterns inside handlers like the FAQ handler by injecting the top patterns into the system prompt as additional examples or by storing them in the knowledge base so they are retrieved alongside documentation. This closes the loop between human resolutions and future automated answers.

14.7 The Complete Customer System

The following class wires the classifier, handlers, escalation manager, and learner into a single service entrypoint.
export default class CustomerServiceSystem extends AgenticSystem {
  // Components
  @field classifier: InquiryClassifier;
  @field faqHandler: FAQHandler;
  @field accountHandler: AccountHandler;
  @field technicalHandler: TechnicalHandler;
  @field billingHandler: BillingHandler;
  @field escalationManager: EscalationManager;
  @field resolutionLearner: ResolutionLearner;
  @field knowledgeBase: KnowledgeBase;

  // State
  @field conversations: Map<string, Conversation> = new Map();
  @field metrics: ServiceMetrics;

  @action()
  async handleInquiry(
    customerId: string,
    inquiry: string
  ): Promise<CustomerResponse> {
    const startTime = Date.now();

    // Get or create conversation
    const conversation = this.getOrCreateConversation(customerId);

    // Get customer context
    const context = await this.getCustomerContext(customerId);

    // Classify
    const intent = await this.classifier.classify(inquiry, context);
    conversation.addMessage('customer', inquiry, intent);

    // Route to handler
    const handlerName = this.classifier.routeToHandler(intent);
    const handler = this.getHandler(handlerName);

    // Handle
    const handlerResponse = await handler.handle(inquiry, intent, context);

    // Check for escalation
    const escalationDecision = this.escalationManager.shouldEscalate(
      intent,
      handlerResponse,
      context
    );

    let response: CustomerResponse;

    if (escalationDecision.shouldEscalate) {
      // Create escalation ticket
      const ticket = await this.escalationManager.createEscalation(
        inquiry,
        intent,
        handlerResponse,
        context,
        escalationDecision
      );

      response = {
        type: 'escalation',
        message: this.getEscalationMessage(escalationDecision),
        ticketId: ticket.id,
        estimatedWait: this.estimateWaitTime(escalationDecision.priority)
      };
    } else {
      // Deliver automated response
      response = {
        type: 'automated',
        message: handlerResponse.response!,
        confidence: handlerResponse.confidence!,
        sources: handlerResponse.sources
      };
    }

    // Record in conversation
    conversation.addMessage('agent', response.message, {
      type: response.type,
      confidence: response.confidence
    });

    // Update metrics
    this.metrics.record({
      category: intent.category,
      handled: response.type === 'automated',
      responseTime: Date.now() - startTime,
      confidence: response.confidence
    });

    return response;
  }

  @action()
  async recordFeedback(
    conversationId: string,
    feedback: CustomerFeedback
  ) {
    const conversation = this.conversations.get(conversationId);
    if (!conversation) return;

    conversation.feedback = feedback;

    // Learn from the outcome
    if (conversation.resolution) {
      await this.resolutionLearner.learnFromResolution(
        {
          inquiry: conversation.messages[0].content,
          intent: conversation.messages[0].metadata as Intent,
          customerId: conversation.customerId
        },
        conversation.resolution,
        feedback
      );
    }
  }

  private getHandler(name: string): Handler {
    switch (name) {
      case 'faq': return this.faqHandler;
      case 'account': return this.accountHandler;
      case 'technical': return this.technicalHandler;
      case 'billing': return this.billingHandler;
      default: return this.faqHandler;
    }
  }

  private getEscalationMessage(decision: EscalationDecision): string {
    const messages: Record<string, string> = {
      urgent: "I'm connecting you with a specialist right away. Please hold for just a moment.",
      high: "I want to make sure you get the best help for this. Let me connect you with a team member who can assist.",
      medium: "I'd like to have one of our team members help you with this. They'll be with you shortly.",
      low: "For the best assistance with this, I'm going to connect you with our support team."
    };

    return messages[decision.priority];
  }

  private estimateWaitTime(priority: string): string {
    const estimates: Record<string, string> = {
      urgent: 'Less than 1 minute',
      high: '1-2 minutes',
      medium: '3-5 minutes',
      low: '5-10 minutes'
    };
    return estimates[priority] || '5-10 minutes';
  }

  // Implementation-specific helpers like getOrCreateConversation and getCustomerContext
  // are omitted for brevity. In production, you would persist conversations to a database
  // keyed by customer and channel rather than keeping them in memory, and you would handle
  // concurrent updates using optimistic locking or a queue.
}

interface CustomerResponse {
  type: 'automated' | 'escalation';
  message: string;
  confidence?: number;
  sources?: string[];
  ticketId?: string;
  estimatedWait?: string;
}

interface Handler {
  handle(inquiry: string, intent: Intent, context: CustomerContext): Promise<HandlerResponse>;
}

interface Conversation {
  id: string;
  customerId: string;
  messages: ConversationMessage[];
  feedback?: CustomerFeedback;
  resolution?: Resolution;
  addMessage(role: string, content: string, metadata?: any): void;
}

interface ConversationMessage {
  role: 'customer' | 'system' | 'agent';
  content: string;
  timestamp: number;
  metadata?: any;
}

interface ServiceMetrics {
  record(entry: MetricEntry): void;
}

interface MetricEntry {
  category: string;
  handled: boolean;
  responseTime: number;
  confidence?: number;
}

Key Takeaways

  • Customer systems require balancing automation with appropriate escalation
  • Classification determines routing—get it right and everything downstream works better
  • Response grounding in knowledge prevents hallucination and enables citation
  • Confidence-based escalation uses multiple signals, not just a single threshold
  • Tone adaptation based on sentiment and context improves customer experience
  • Learning from resolutions creates compounding improvement

Transition

Chapter 14 addressed human-facing interactions with emphasis on appropriate escalation. Chapter 15: Content Pipeline builds a creative system that operates autonomously while maintaining quality through review gates and performance feedback.