Intent classification routes an incoming message to the right action handler. For a legal AI agent, getting intent wrong has consequences — a misclassified "I am in danger" message is not a minor UX issue. This article covers the two-layer classification system and confidence thresholds.
Two-Layer Classification
Layer 1 is fast keyword matching for common, unambiguous intents. Layer 2 uses the LLM for ambiguous cases. This avoids LLM latency for straightforward requests.
class IntentClassifier
{
private const KEYWORD_PATTERNS = [
'case_status' => ['/case status/i', '/my case/i', '/matter update/i', '/\bstatus\b/i'],
'hearings_today' => ['/hearing today/i', '/today.*hearing/i', '/court.*today/i'],
'set_reminder' => ['/remind me/i', '/set reminder/i', '/don\'t forget/i'],
'add_note' => ['/add note/i', '/note this/i', '/remember that/i'],
'emergency' => ['/help me/i', '/danger/i', '/emergency/i', '/urgent.*legal/i'],
];
public function classify(string $text, PersonaInterface $persona): IntentResult
{
// Layer 1: keyword matching
foreach (self::KEYWORD_PATTERNS as $intent => $patterns) {
foreach ($patterns as $pattern) {
if (preg_match($pattern, $text)) {
return new IntentResult($intent, 1.0, 'keyword');
}
}
}
// Layer 2: LLM classification for ambiguous text
return $this->classifyWithLLM($text, $persona);
}
private function classifyWithLLM(string $text, PersonaInterface $persona): IntentResult
{
$prompt = "Classify the intent of the following message from a " . get_class($persona) . ".\n"
. "Respond with JSON: {\"intent\": \"...\", \"confidence\": 0.0-1.0}\n"
. "Valid intents: case_status, hearings_today, set_reminder, add_note, get_document, "
. "billing_query, appointment_request, general_query, emergency, unknown\n\n"
. "Message: " . $text;
$raw = $this->llm->generate($prompt, temperature: 0.1);
$parsed = json_decode($raw, true);
$intent = $parsed['intent'] ?? 'unknown';
$confidence = (float)($parsed['confidence'] ?? 0.0);
// If confidence is below threshold, fall back to general_query
if ($confidence < 0.6 && $intent !== 'emergency') {
$intent = 'general_query';
}
return new IntentResult($intent, $confidence, 'llm');
}
}
Routing to Action Handlers
$handlers = [
'case_status' => new CaseStatusHandler($pdo, $persona),
'hearings_today' => new HearingsTodayHandler($pdo, $persona),
'set_reminder' => new SetReminderHandler($pdo, $persona),
'add_note' => new AddNoteHandler($pdo, $persona),
'emergency' => new EmergencyHandler($notifier),
'general_query' => new LLMQueryHandler($llm, $persona, $rag),
];
$result = $classifier->classify($incomingText, $persona);
$handler = $handlers[$result->intent] ?? $handlers['general_query'];
$response = $handler->handle($incomingText);
What to Watch For
- Emergency intents must never fall through — If keyword matching catches 'emergency', it must route immediately. Do not pass emergency intents through the LLM classifier — that adds latency when it matters most.
- Unknown intent vs fallback — When in doubt, ask the user to clarify rather than assuming. "I didn't understand that — could you tell me more about what you need?" is always better than a silent wrong action.
- Multilingual intent — Keyword patterns in English will miss Hindi and Punjabi messages. For multilingual agents, include patterns in all supported languages or rely primarily on the LLM layer.