Building a Phone Screening Agent: AI-Powered Call Screening and Routing
Build an AI phone screening agent that identifies callers, detects intent, filters spam, and routes calls by priority. Covers real-time caller analysis, blocklist management, and VIP routing patterns.
Why AI Call Screening Changes Everything
Traditional call screening is binary — either you answer or you do not. AI screening adds intelligence: the agent answers every call, identifies the caller, understands their purpose, and makes a routing decision in seconds. Legitimate callers get connected quickly. Spam calls get blocked. High-priority calls jump the queue.
This pattern is valuable for businesses that receive high call volumes (medical practices, law firms, real estate agencies) and for executives who need a smart gatekeeper without hiring a human receptionist.
Architecture of a Screening Agent
The screening agent operates in three phases: identification, intent assessment, and routing decision. Here is the core structure:
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
from datetime import datetime
class CallerPriority(Enum):
VIP = "vip"
KNOWN = "known"
NEW = "new"
SUSPICIOUS = "suspicious"
SPAM = "spam"
class RoutingAction(Enum):
CONNECT_IMMEDIATELY = "connect_immediately"
CONNECT_WITH_ANNOUNCE = "connect_with_announce"
TAKE_MESSAGE = "take_message"
SEND_TO_VOICEMAIL = "send_to_voicemail"
BLOCK = "block"
@dataclass
class ScreeningResult:
caller_number: str
caller_name: Optional[str]
priority: CallerPriority
intent_summary: str
routing_action: RoutingAction
confidence: float
metadata: dict = field(default_factory=dict)
class PhoneScreeningAgent:
"""AI-powered call screening and routing agent."""
def __init__(self, db_pool, ai_client, spam_checker):
self.db_pool = db_pool
self.ai_client = ai_client
self.spam_checker = spam_checker
async def screen_call(self, caller_number: str, call_sid: str):
"""Full screening pipeline for an incoming call."""
# Phase 1: Identification
caller_info = await self.identify_caller(caller_number)
# Phase 2: Spam check (fast path for known spam)
if await self.spam_checker.is_spam(caller_number):
return ScreeningResult(
caller_number=caller_number,
caller_name=None,
priority=CallerPriority.SPAM,
intent_summary="Spam call detected",
routing_action=RoutingAction.BLOCK,
confidence=0.95,
)
# Phase 3: For non-spam, engage in brief conversation
intent = await self.assess_intent(call_sid)
# Phase 4: Make routing decision
return await self.decide_routing(caller_info, intent)
Caller Identification
Before the AI even speaks, look up the caller against your known contacts, CRM, and spam databases:
import asyncpg
class CallerIdentifier:
"""Identifies callers from phone number lookup."""
def __init__(self, db_pool: asyncpg.Pool):
self.db_pool = db_pool
async def identify(self, phone_number: str) -> dict:
"""Look up caller in CRM and contact databases."""
# Check internal contacts first
contact = await self.db_pool.fetchrow(
"""
SELECT name, company, relationship, priority_level,
last_call_date, total_calls, notes
FROM contacts
WHERE phone_number = $1 AND is_active = true
""",
phone_number,
)
if contact:
return {
"known": True,
"name": contact["name"],
"company": contact["company"],
"priority": contact["priority_level"],
"relationship": contact["relationship"],
"call_history": {
"last_call": contact["last_call_date"],
"total_calls": contact["total_calls"],
},
"notes": contact["notes"],
}
# Check recent call history for repeat unknown callers
recent_calls = await self.db_pool.fetchval(
"""
SELECT COUNT(*) FROM call_log
WHERE caller_number = $1
AND created_at > NOW() - INTERVAL '30 days'
""",
phone_number,
)
return {
"known": False,
"name": None,
"recent_call_count": recent_calls,
}
Spam Detection Pipeline
Layer multiple spam signals for accurate detection:
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
class SpamDetector:
"""Multi-signal spam detection for incoming calls."""
def __init__(self, db_pool, external_api_key=None):
self.db_pool = db_pool
self.external_api_key = external_api_key
async def is_spam(self, phone_number: str) -> bool:
"""Check multiple spam signals."""
score = 0.0
# Check internal blocklist
blocked = await self.db_pool.fetchval(
"SELECT EXISTS(SELECT 1 FROM blocklist WHERE phone = $1)",
phone_number,
)
if blocked:
return True
# Check call frequency (high frequency = suspicious)
calls_today = await self.db_pool.fetchval(
"""
SELECT COUNT(*) FROM call_log
WHERE caller_number = $1
AND created_at > NOW() - INTERVAL '1 hour'
""",
phone_number,
)
if calls_today > 5:
score += 0.4
# Check against known spam patterns
if self.is_spoofed_pattern(phone_number):
score += 0.3
# External spam database lookup
if self.external_api_key:
external_score = await self.check_external_db(phone_number)
score += external_score * 0.3
return score >= 0.6
def is_spoofed_pattern(self, number: str) -> bool:
"""Detect common spoofing patterns."""
# Numbers with all same digits, sequential patterns
digits = number.replace("+", "").replace("-", "")
if len(set(digits[-4:])) == 1: # Last 4 digits identical
return True
return False
Intent Assessment via Conversation
For callers who pass the spam check, the AI engages in a brief screening conversation:
from openai import AsyncOpenAI
class IntentAssessor:
"""Assesses caller intent through brief conversation."""
def __init__(self):
self.client = AsyncOpenAI()
async def assess(self, transcript: str, caller_info: dict) -> dict:
context = ""
if caller_info.get("known"):
context = (
f"Known caller: {caller_info['name']} from "
f"{caller_info.get('company', 'N/A')}. "
f"Relationship: {caller_info.get('relationship', 'unknown')}."
)
response = await self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
"You are a call screening assistant. Assess the "
"caller's intent and urgency from their statement. "
f"Context: {context}\n"
"Return JSON with: intent, urgency (low/medium/high/"
"emergency), summary, and recommended_action "
"(connect/message/voicemail/block)."
),
},
{"role": "user", "content": transcript},
],
response_format={"type": "json_object"},
temperature=0.1,
)
import json
return json.loads(response.choices[0].message.content)
Priority Routing Logic
Combine identification and intent to make the final routing decision:
async def decide_routing(
self, caller_info: dict, intent: dict
) -> ScreeningResult:
"""Determine routing based on caller identity and intent."""
# VIP callers always connect immediately
if caller_info.get("priority") == "vip":
return ScreeningResult(
caller_number=caller_info.get("phone", ""),
caller_name=caller_info.get("name"),
priority=CallerPriority.VIP,
intent_summary=intent.get("summary", ""),
routing_action=RoutingAction.CONNECT_IMMEDIATELY,
confidence=1.0,
)
urgency = intent.get("urgency", "low")
# Emergency calls always connect
if urgency == "emergency":
return ScreeningResult(
caller_number=caller_info.get("phone", ""),
caller_name=caller_info.get("name"),
priority=CallerPriority.NEW,
intent_summary=intent["summary"],
routing_action=RoutingAction.CONNECT_IMMEDIATELY,
confidence=0.9,
)
# Known callers with medium+ urgency connect with announcement
if caller_info.get("known") and urgency in ("medium", "high"):
return ScreeningResult(
caller_number=caller_info.get("phone", ""),
caller_name=caller_info["name"],
priority=CallerPriority.KNOWN,
intent_summary=intent["summary"],
routing_action=RoutingAction.CONNECT_WITH_ANNOUNCE,
confidence=0.85,
)
# Low urgency or unknown callers take a message
return ScreeningResult(
caller_number=caller_info.get("phone", ""),
caller_name=caller_info.get("name"),
priority=CallerPriority.NEW,
intent_summary=intent["summary"],
routing_action=RoutingAction.TAKE_MESSAGE,
confidence=0.7,
)
FAQ
How do I avoid blocking legitimate calls?
Use a multi-signal approach and set conservative thresholds. Never block based on a single signal unless it is an explicit blocklist entry. Implement a "soft block" tier that sends callers to voicemail instead of disconnecting — they can still leave a message. Review blocked calls weekly and adjust thresholds. Allow users to whitelist numbers that were incorrectly flagged.
What is the typical screening conversation duration?
An effective screening interaction should complete in 10-15 seconds. The AI greets the caller, asks how it can help, and captures their initial statement. One exchange is usually enough to determine intent and urgency. Longer screening creates friction — if you cannot resolve the screening in two exchanges, route to a human.
How do I handle the "agent whisper" when connecting a screened call?
When routing a screened call to a human, use a conference bridge pattern. Connect the human first and play a whisper message (e.g., "Incoming call from John Smith regarding a billing dispute, high urgency") that only the human hears. Then bridge in the caller. Twilio supports this with the <Dial> verb's callerId attribute and <Conference> with coach mode.
#CallScreening #SpamDetection #CallRouting #VoiceAI #PriorityRouting #Telephony #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.