35.1 Intent classification and routing
Overview and links for this section of the guide.
On this page
Defining the Taxonomy
The foundation of any classification system is a well-defined taxonomy. Rules:
- Fixed categories: Never let the model invent new ones
- Mutually exclusive: Each email fits exactly one category
- Exhaustive: Every possible email can be classified
- Actionable: Each category maps to a specific handler or queue
// taxonomy.ts
export const INTENT_CATEGORIES = {
// Billing-related
BILLING_REFUND: {
description: 'Customer wants money back for a purchase',
handler: 'billing_team',
autoReplyEligible: false, // Requires human approval
},
BILLING_INVOICE: {
description: 'Customer needs invoice or receipt',
handler: 'billing_team',
autoReplyEligible: true,
},
BILLING_PAYMENT_ISSUE: {
description: 'Payment failed, card declined, etc.',
handler: 'billing_team',
autoReplyEligible: false,
},
// Shipping-related
SHIPPING_STATUS: {
description: 'Customer asks where their order is',
handler: 'shipping_bot', // Fully automated
autoReplyEligible: true,
},
SHIPPING_ADDRESS_CHANGE: {
description: 'Customer wants to change delivery address',
handler: 'shipping_team',
autoReplyEligible: false, // Time-sensitive, needs human
},
SHIPPING_DAMAGED: {
description: 'Package arrived damaged',
handler: 'shipping_team',
autoReplyEligible: false,
},
// Technical/Product
TECHNICAL_LOGIN: {
description: 'Cannot log in, password issues',
handler: 'tech_bot',
autoReplyEligible: true, // Send password reset link
},
TECHNICAL_BUG: {
description: 'Feature not working as expected',
handler: 'engineering_queue',
autoReplyEligible: false,
},
// Catch-all
OTHER: {
description: 'Does not fit any category above',
handler: 'general_queue',
autoReplyEligible: false,
},
} as const;
export type IntentCategory = keyof typeof INTENT_CATEGORIES;
Always have an OTHER category. If the model is unsure, it should dump tickets there for a human. Never
force a square peg into a round hole—misclassification is worse than no classification.
The Classification Prompt
The classification prompt needs to be precise and include examples:
// classifier-prompt.ts
export function buildClassificationPrompt(email: string): string {
return `You are a customer support triage assistant. Your job is to classify incoming emails into exactly ONE category.
## Categories
${Object.entries(INTENT_CATEGORIES).map(([key, value]) =>
`- ${key}: ${value.description}`
).join('\n')}
## Rules
1. Choose exactly ONE category that best fits the email
2. If multiple categories could apply, pick the PRIMARY intent
3. If truly uncertain, choose OTHER
4. Provide a confidence score from 0.0 to 1.0
5. Explain your reasoning briefly
## Examples
Email: "Hi, I ordered a laptop last week but haven't received any tracking info"
Output: {"category": "SHIPPING_STATUS", "confidence": 0.95, "reasoning": "Customer is asking about order delivery status"}
Email: "Your app crashes every time I try to upload a photo"
Output: {"category": "TECHNICAL_BUG", "confidence": 0.90, "reasoning": "Customer reporting a software malfunction"}
Email: "I'd like to get a copy of my invoice from January"
Output: {"category": "BILLING_INVOICE", "confidence": 0.95, "reasoning": "Customer requesting billing document"}
## Email to Classify
${email}
## Output
Respond with ONLY a JSON object in this exact format:
{"category": "CATEGORY_NAME", "confidence": 0.0-1.0, "reasoning": "brief explanation"}`;
}
Full Implementation
// classifier.ts
import { GoogleGenerativeAI } from '@google/generative-ai';
import { INTENT_CATEGORIES, IntentCategory } from './taxonomy';
interface ClassificationResult {
category: IntentCategory;
confidence: number;
reasoning: string;
raw?: string; // For debugging
}
export class EmailClassifier {
private model: any;
constructor(apiKey: string) {
const genAI = new GoogleGenerativeAI(apiKey);
this.model = genAI.getGenerativeModel({
model: 'gemini-1.5-flash',
generationConfig: {
temperature: 0, // Deterministic for classification
maxOutputTokens: 256,
}
});
}
async classify(email: string): Promise {
const prompt = buildClassificationPrompt(email);
const result = await this.model.generateContent(prompt);
const text = result.response.text();
// Extract JSON from response
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error(`Failed to parse classification response: ${text}`);
}
const parsed = JSON.parse(jsonMatch[0]);
// Validate category exists in taxonomy
if (!(parsed.category in INTENT_CATEGORIES)) {
console.warn(`Unknown category: ${parsed.category}, defaulting to OTHER`);
parsed.category = 'OTHER';
parsed.confidence = 0.5;
}
return {
category: parsed.category as IntentCategory,
confidence: Math.max(0, Math.min(1, parsed.confidence)),
reasoning: parsed.reasoning,
raw: text,
};
}
// Batch classification for efficiency
async classifyBatch(emails: string[]): Promise {
return Promise.all(emails.map(e => this.classify(e)));
}
}
// Usage
const classifier = new EmailClassifier(process.env.GEMINI_API_KEY!);
const result = await classifier.classify(`
Hi support,
I placed an order (#12345) three days ago and still haven't received
any shipping confirmation. Can you tell me when it will arrive?
Thanks,
John
`);
console.log(result);
// {
// category: 'SHIPPING_STATUS',
// confidence: 0.95,
// reasoning: 'Customer asking about order delivery timeline'
// }
Routing Logic
Once classified, route the ticket to the appropriate handler:
// router.ts
interface Ticket {
id: string;
email: string;
classification: ClassificationResult;
customerId?: string;
extractedData?: any;
}
interface RoutingDecision {
action: 'auto_reply' | 'draft_for_review' | 'route_to_human';
handler: string;
priority: 'low' | 'medium' | 'high' | 'urgent';
reason: string;
}
export function routeTicket(ticket: Ticket): RoutingDecision {
const categoryConfig = INTENT_CATEGORIES[ticket.classification.category];
const confidence = ticket.classification.confidence;
// Rule 1: High confidence + auto-reply eligible = fully automated
if (confidence >= 0.9 && categoryConfig.autoReplyEligible) {
return {
action: 'auto_reply',
handler: categoryConfig.handler,
priority: 'low',
reason: `High confidence (${confidence}) classification with auto-reply eligibility`,
};
}
// Rule 2: Medium confidence = draft for human review
if (confidence >= 0.7) {
return {
action: 'draft_for_review',
handler: categoryConfig.handler,
priority: 'medium',
reason: `Medium confidence (${confidence}), human should verify before sending`,
};
}
// Rule 3: Low confidence = pure human handling
return {
action: 'route_to_human',
handler: categoryConfig.handler,
priority: 'medium',
reason: `Low confidence (${confidence}), requires human classification`,
};
}
// Priority escalation based on content
export function adjustPriority(
ticket: Ticket,
decision: RoutingDecision
): RoutingDecision {
const email = ticket.email.toLowerCase();
// Urgent keywords
const urgentKeywords = ['urgent', 'asap', 'emergency', 'immediately'];
if (urgentKeywords.some(kw => email.includes(kw))) {
return { ...decision, priority: 'urgent' };
}
// Legal/escalation keywords - always route to human
const escalationKeywords = ['lawyer', 'sue', 'legal', 'attorney', 'refund or else'];
if (escalationKeywords.some(kw => email.includes(kw))) {
return {
action: 'route_to_human',
handler: 'escalation_team',
priority: 'urgent',
reason: 'Potential legal escalation detected',
};
}
return decision;
}
Handling Multi-Intent Messages
Sometimes customers ask multiple things in one email:
"My order #123 hasn't arrived AND your app keeps crashing. Also, can I get a refund?"
Options for handling this:
| Approach | Pros | Cons |
|---|---|---|
| Primary intent only | Simple, one handler | May miss secondary issues |
| All intents | Complete handling | Complex routing, duplicate responses |
| Split ticket | Clean separation | Customer gets multiple replies |
For most cases, primary intent + mention secondary works best:
// multi-intent-prompt-addition.ts
const MULTI_INTENT_ADDITION = `
If the email contains MULTIPLE distinct requests:
1. Identify the PRIMARY intent (most urgent or first mentioned)
2. Note any SECONDARY intents
Output format:
{
"category": "PRIMARY_CATEGORY",
"confidence": 0.0-1.0,
"reasoning": "...",
"secondary_intents": ["CATEGORY_2", "CATEGORY_3"] // optional
}
`;
Multi-intent emails often benefit from human handling. The agent can address all issues in one coherent response rather than fragmented automated replies.