Series 6 — Part 2 of 10
the WhatsApp AI agent serves lawyers, staff, and clients on the same WhatsApp number. A lawyer asking for case notes needs a different response style and access level than a client asking for hearing dates. Multi-persona routing resolves a phone number to a user+role and routes accordingly.
Resolving Phone Number to User + Role
function resolve_persona(string $phoneNumber, PDO $pdo): PersonaInterface
{
// Strip country code prefix variations
$normalized = normalize_phone($phoneNumber);
$stmt = $pdo->prepare(
'SELECT u.id, u.role, u.full_name, u.team_id
FROM wa_contacts wc
JOIN users u ON wc.user_id = u.id
WHERE wc.wa_number = ? AND u.is_active = 1 LIMIT 1'
);
$stmt->execute([$normalized]);
$user = $stmt->fetch();
if (!$user) {
return new UnknownPersona($phoneNumber);
}
return match($user['role']) {
'lawyer' => new LawyerPersona($user),
'client' => new ClientPersona($user),
'staff' => new StaffPersona($user),
default => new UnknownPersona($phoneNumber),
};
}
Persona Classes
interface PersonaInterface {
public function getSystemPromptContext(): string;
public function canAccessMatter(int $matterId, PDO $pdo): bool;
public function preferredLanguage(): string;
}
class LawyerPersona implements PersonaInterface {
public function getSystemPromptContext(): string {
return "You are speaking with {$this->user['full_name']}, a lawyer on the team. "
. "Provide full case details, work logs, and billing information as requested. "
. "Use professional legal terminology. Be concise.";
}
public function canAccessMatter(int $matterId, PDO $pdo): bool {
// Lawyers can access all matters in their team
$stmt = $pdo->prepare('SELECT 1 FROM matters WHERE id = ? AND team_id = ?');
$stmt->execute([$matterId, $this->user['team_id']]);
return (bool) $stmt->fetchColumn();
}
}
class ClientPersona implements PersonaInterface {
public function getSystemPromptContext(): string {
return "You are speaking with {$this->user['full_name']}, a client. "
. "Provide only information about their own matters. Do not share billing rates. "
. "Use plain language. Always offer to connect them with their lawyer for legal advice.";
}
public function canAccessMatter(int $matterId, PDO $pdo): bool {
// Clients only see matters where they are a party
$stmt = $pdo->prepare(
'SELECT 1 FROM parties WHERE matter_id = ? AND user_id = ? AND is_our_client = 1'
);
$stmt->execute([$matterId, $this->user['id']]);
return (bool) $stmt->fetchColumn();
}
}
class UnknownPersona implements PersonaInterface {
public function getSystemPromptContext(): string {
return "You are speaking with an unregistered contact. "
. "Politely inform them that this is a private system and offer the office phone number. "
. "Do not provide any case information.";
}
public function canAccessMatter(int $matterId, PDO $pdo): bool { return false; }
public function preferredLanguage(): string { return 'en'; }
}
What to Watch For
- Phone number normalisation — WhatsApp sends numbers without leading +, in E.164 format without the +. Normalise all numbers to the same format before comparison or lookups will fail silently.
- Unknown caller handling — The UnknownPersona must never provide any case data. It should offer only public contact information. Treat unknown callers as untrusted by default.
- Role changes — Cache persona lookups for the duration of a conversation session. If a user's role changes mid-conversation, the change takes effect at the next session, not mid-message.