Building an AI Receptionist: Front Desk Automation for Small Offices
Learn how to build an AI receptionist agent that greets visitors, routes calls to the right staff member, manages visitor sign-ins, and handles package deliveries for small office environments.
The Modern Small Office Front Desk Problem
Small offices with five to fifty employees rarely justify a full-time receptionist, yet someone still needs to answer the phone, greet visitors, accept deliveries, and direct people to the right room. These tasks typically fall on whoever happens to be nearby — pulling accountants, engineers, or managers away from their actual work. An AI receptionist handles these routine interactions consistently, freeing the team to focus.
This guide builds a multi-function receptionist agent that manages calls, visitors, and deliveries through a unified interface.
Staff Directory and Routing Model
The receptionist needs to know who works in the office, their roles, their availability, and how to reach them. We model this as a staff directory with presence tracking.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart LR
subgraph HUMAN["Human Receptionist"]
H1["8 hours per day<br/>limited language coverage"]
H2["Salary plus benefits<br/>3,000 to 5,000 per month"]
H3["Sick days, holidays,<br/>turnover"]
H4["Call notes typed<br/>manually into CRM"]
end
subgraph AI["CallSphere AI Voice Agent"]
A1["24 by 7 coverage<br/>57 plus languages"]
A2["Flat fee from 199<br/>per month, unlimited calls"]
A3["Zero turnover, instant<br/>script updates"]
A4["Auto written CRM<br/>notes plus sentiment"]
end
H1 -.->|Upgrade| A1
H2 -.->|Upgrade| A2
H3 -.->|Upgrade| A3
H4 -.->|Upgrade| A4
style HUMAN fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
style AI fill:#dcfce7,stroke:#059669,color:#064e3b
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
from datetime import datetime
class PresenceStatus(Enum):
AVAILABLE = "available"
IN_MEETING = "in_meeting"
OUT_OF_OFFICE = "out_of_office"
DO_NOT_DISTURB = "do_not_disturb"
LUNCH = "lunch"
@dataclass
class StaffMember:
id: str
name: str
title: str
department: str
extension: str
email: str
status: PresenceStatus = PresenceStatus.AVAILABLE
backup_contact: Optional[str] = None # another staff ID
@dataclass
class VisitorRecord:
name: str
company: str
visiting: str # staff member ID
purpose: str
badge_number: Optional[str] = None
check_in: datetime = field(default_factory=datetime.now)
check_out: Optional[datetime] = None
@dataclass
class PackageRecord:
tracking_number: str
carrier: str
recipient_id: str
received_at: datetime = field(default_factory=datetime.now)
picked_up: bool = False
Staff Directory Service
The directory service acts as the central lookup for the receptionist. It supports searching by name, department, or role.
class StaffDirectory:
def __init__(self):
self.staff: dict[str, StaffMember] = {}
self.visitors: list[VisitorRecord] = []
self.packages: list[PackageRecord] = []
def add_member(self, member: StaffMember):
self.staff[member.id] = member
def find_by_name(self, query: str) -> list[StaffMember]:
query_lower = query.lower()
return [
s for s in self.staff.values()
if query_lower in s.name.lower()
or query_lower in s.title.lower()
]
def find_by_department(self, dept: str) -> list[StaffMember]:
return [
s for s in self.staff.values()
if dept.lower() in s.department.lower()
]
def get_routing_target(self, staff_id: str) -> dict:
member = self.staff.get(staff_id)
if not member:
return {"action": "not_found"}
if member.status == PresenceStatus.AVAILABLE:
return {
"action": "transfer",
"extension": member.extension,
"message": f"Connecting you to {member.name} now.",
}
if member.status == PresenceStatus.IN_MEETING:
backup = self.staff.get(member.backup_contact)
return {
"action": "take_message",
"message": (
f"{member.name} is in a meeting. "
+ (f"I can connect you to {backup.name} instead, "
if backup else "")
+ "or I can take a message."
),
}
return {
"action": "take_message",
"message": f"{member.name} is currently unavailable. Let me take a message.",
}
directory = StaffDirectory()
directory.add_member(StaffMember(
"m1", "Sarah Chen", "Managing Partner", "Leadership",
"101", "[email protected]", backup_contact="m2"
))
directory.add_member(StaffMember(
"m2", "James Rodriguez", "Office Manager", "Operations",
"102", "[email protected]"
))
directory.add_member(StaffMember(
"m3", "Priya Patel", "Senior Accountant", "Finance",
"103", "[email protected]", backup_contact="m2"
))
Receptionist Agent Tools
from agents import Agent, Runner, function_tool
@function_tool
def lookup_staff(query: str) -> str:
"""Find a staff member by name, title, or department."""
results = directory.find_by_name(query)
if not results:
results = directory.find_by_department(query)
if not results:
return "No staff member found matching that query."
lines = []
for s in results:
lines.append(f"{s.name} - {s.title} ({s.department}) - Status: {s.status.value}")
return "\n".join(lines)
@function_tool
def route_call(staff_id: str) -> str:
"""Route a call to a specific staff member based on their availability."""
routing = directory.get_routing_target(staff_id)
return routing.get("message", "Unable to route call.")
@function_tool
def check_in_visitor(
visitor_name: str, company: str,
host_staff_id: str, purpose: str
) -> str:
"""Register a visitor and notify the host staff member."""
host = directory.staff.get(host_staff_id)
if not host:
return "Host not found. Please verify the name."
record = VisitorRecord(
name=visitor_name, company=company,
visiting=host_staff_id, purpose=purpose,
badge_number=f"V-{len(directory.visitors) + 1:03d}",
)
directory.visitors.append(record)
return (
f"Welcome, {visitor_name}. Your visitor badge is {record.badge_number}. "
f"I have notified {host.name} that you have arrived. "
f"Please have a seat in the lobby."
)
@function_tool
def log_package(
tracking_number: str, carrier: str, recipient_name: str
) -> str:
"""Log an incoming package and notify the recipient."""
results = directory.find_by_name(recipient_name)
if not results:
return f"No staff member named '{recipient_name}' found."
recipient = results[0]
record = PackageRecord(
tracking_number=tracking_number,
carrier=carrier,
recipient_id=recipient.id,
)
directory.packages.append(record)
return (
f"Package logged: {carrier} tracking {tracking_number} "
f"for {recipient.name}. Notification sent to {recipient.email}."
)
The Receptionist Agent
receptionist = Agent(
name="Office Receptionist",
instructions="""You are the front desk receptionist for a small professional office.
For phone calls:
1. Greet the caller professionally.
2. Ask who they are trying to reach. Use lookup_staff to find the person.
3. Use route_call to connect them or offer to take a message.
For visitors:
1. Welcome them and ask their name, company, and who they are visiting.
2. Use check_in_visitor to register them and issue a badge.
For deliveries:
1. Ask for the tracking number, carrier, and recipient name.
2. Use log_package to record the delivery and notify the recipient.
Always be warm but professional. If unsure who a caller needs,
ask clarifying questions about the nature of their inquiry to narrow
down the right department.""",
tools=[lookup_staff, route_call, check_in_visitor, log_package],
)
result = Runner.run_sync(
receptionist,
"Hi, I have a meeting with Sarah about our quarterly taxes.",
)
print(result.final_output)
Handling Ambiguous Requests
Callers rarely say "Connect me to staff ID m3." They say "I need to talk to someone about my taxes" or "Is the boss available?" The agent instructions handle this naturally — the LLM maps "taxes" to the Finance department and "the boss" to the Managing Partner. The lookup_staff tool supports searching by title and department, not just name, which covers most ambiguous cases.
FAQ
How does the agent handle multiple visitors arriving at the same time?
Each visitor interaction is an independent agent run. If the system receives multiple check-in requests simultaneously, they execute in parallel, each producing its own badge number and notification. The visitor list is append-only, so there are no concurrency conflicts in the check-in process itself.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
Can I integrate this with a real calendar system?
Yes. Replace the static PresenceStatus with a live lookup against Google Calendar or Microsoft Outlook via their APIs. Before routing a call, the agent tool queries the calendar to determine whether the staff member is in a meeting, then updates the routing decision accordingly.
How do I handle sensitive visitor information for compliance?
Add a data retention policy to the VisitorRecord model — automatically purge records after 90 days. For HIPAA or SOC 2 environments, encrypt the visitor log at rest and restrict access to the visitors list through role-based permissions on the API layer.
#AIReceptionist #OfficeAutomation #CallRouting #VisitorManagement #Python #AgenticAI #LearnAI #AIEngineering
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.