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.