Skip to content
Learn Agentic AI
Learn Agentic AI14 min read25 views

Building a Restaurant Reservation Agent: AI-Powered Booking and Waitlist Management

Learn how to build an AI agent that manages restaurant reservations, handles capacity logic, confirms and cancels bookings, and operates an intelligent waitlist with automatic promotion.

Why Restaurants Need AI Reservation Agents

Restaurants lose an estimated 20 percent of potential bookings because phone lines go unanswered during peak hours. A human host can handle one call at a time, but an AI reservation agent manages unlimited concurrent conversations across phone, web chat, and messaging platforms simultaneously.

Beyond simple booking, a well-built reservation agent handles capacity optimization, waitlist management, confirmation reminders, and cancellation recovery — turning a cost center into a revenue multiplier.

Modeling Restaurant Capacity

Before an agent can book tables, it needs to understand the restaurant's physical constraints. This means modeling table inventory, seating configurations, and time-slot availability.

Hear it before you finish reading

Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.

Try Live Demo →
flowchart LR
    CALLER(["Guest or Prospect"])
    subgraph TEL["Telephony"]
        SIP["Twilio SIP and PSTN"]
    end
    subgraph BRAIN["Hotel Concierge AI Agent"]
        STT["Streaming STT<br/>Deepgram or Whisper"]
        NLU{"Intent and<br/>Entity Extraction"}
        TOOLS["Tool Calls"]
        TTS["Streaming TTS<br/>ElevenLabs or Rime"]
    end
    subgraph DATA["Live Data Plane"]
        CRM[("CRM and Notes")]
        CAL[("Calendar and<br/>Schedule")]
        KB[("Knowledge Base<br/>and Policies")]
    end
    subgraph OUT["Outcomes"]
        O1(["Reservation confirmed"])
        O2(["Room service order"])
        O3(["Front desk handoff"])
    end
    CALLER --> SIP --> STT --> NLU
    NLU -->|Lookup| TOOLS
    TOOLS <--> CRM
    TOOLS <--> CAL
    TOOLS <--> KB
    NLU --> TTS --> SIP --> CALLER
    NLU -->|Resolved| O1
    NLU -->|Schedule| O2
    NLU -->|Escalate| O3
    style CALLER fill:#f1f5f9,stroke:#64748b,color:#0f172a
    style NLU fill:#4f46e5,stroke:#4338ca,color:#fff
    style O1 fill:#059669,stroke:#047857,color:#fff
    style O2 fill:#0ea5e9,stroke:#0369a1,color:#fff
    style O3 fill:#f59e0b,stroke:#d97706,color:#1f2937
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum

class TableStatus(Enum):
    AVAILABLE = "available"
    RESERVED = "reserved"
    OCCUPIED = "occupied"
    BLOCKED = "blocked"

@dataclass
class Table:
    table_id: str
    capacity: int
    section: str
    status: TableStatus = TableStatus.AVAILABLE

@dataclass
class TimeSlot:
    start: datetime
    duration: timedelta = timedelta(hours=1, minutes=30)

    @property
    def end(self) -> datetime:
        return self.start + self.duration

@dataclass
class Restaurant:
    name: str
    tables: list[Table] = field(default_factory=list)
    reservations: list[dict] = field(default_factory=list)
    default_dining_duration: timedelta = timedelta(hours=1, minutes=30)

    def available_tables(self, party_size: int, requested_time: datetime) -> list[Table]:
        slot_end = requested_time + self.default_dining_duration
        reserved_table_ids = set()
        for res in self.reservations:
            if res["status"] == "confirmed":
                if res["start"] < slot_end and res["end"] > requested_time:
                    reserved_table_ids.add(res["table_id"])

        return [
            t for t in self.tables
            if t.capacity >= party_size
            and t.table_id not in reserved_table_ids
            and t.status != TableStatus.BLOCKED
        ]

    def best_fit_table(self, party_size: int, requested_time: datetime) -> Table | None:
        candidates = self.available_tables(party_size, requested_time)
        if not candidates:
            return None
        # Pick the smallest table that fits to maximize capacity utilization
        return min(candidates, key=lambda t: t.capacity)

The best_fit_table method applies a bin-packing heuristic: it assigns the smallest table that accommodates the party. This prevents a party of two from claiming a table for eight during a busy evening.

Building the Reservation Agent

With the capacity model in place, the agent exposes booking tools that the language model can invoke during conversation.

from agents import Agent, Runner, function_tool
from datetime import datetime, timedelta

restaurant = Restaurant(
    name="Trattoria Bella",
    tables=[
        Table("T1", 2, "patio"),
        Table("T2", 2, "patio"),
        Table("T3", 4, "main"),
        Table("T4", 4, "main"),
        Table("T5", 6, "private"),
        Table("T6", 8, "private"),
    ],
)

@function_tool
def check_availability(date: str, time: str, party_size: int) -> str:
    requested = datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M")
    tables = restaurant.available_tables(party_size, requested)
    if not tables:
        return f"No tables available for {party_size} guests at {time} on {date}."
    sections = set(t.section for t in tables)
    return f"{len(tables)} tables available in: {', '.join(sections)}."

@function_tool
def create_reservation(
    guest_name: str, phone: str, date: str, time: str, party_size: int
) -> str:
    requested = datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M")
    table = restaurant.best_fit_table(party_size, requested)
    if not table:
        return "UNAVAILABLE: No suitable table found for this time and party size."
    reservation = {
        "guest": guest_name,
        "phone": phone,
        "table_id": table.table_id,
        "party_size": party_size,
        "start": requested,
        "end": requested + restaurant.default_dining_duration,
        "status": "confirmed",
    }
    restaurant.reservations.append(reservation)
    return (
        f"Reservation confirmed for {guest_name}, party of {party_size}, "
        f"at {time} on {date}. Table {table.table_id} ({table.section} section). "
        f"A confirmation SMS will be sent to {phone}."
    )

@function_tool
def cancel_reservation(guest_name: str, date: str, time: str) -> str:
    requested = datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M")
    for res in restaurant.reservations:
        if res["guest"] == guest_name and res["start"] == requested:
            res["status"] = "cancelled"
            return f"Reservation for {guest_name} at {time} on {date} has been cancelled."
    return f"No reservation found for {guest_name} at {time} on {date}."

reservation_agent = Agent(
    name="Reservation Agent",
    instructions="""You are the reservation agent for Trattoria Bella.
    Help guests book, modify, or cancel reservations. Always confirm the
    date, time, and party size before booking. If unavailable, suggest
    alternative times within 30 minutes of the requested slot.""",
    tools=[check_availability, create_reservation, cancel_reservation],
)

Implementing the Waitlist

When all tables are booked, a smart agent does not simply reject the guest. It offers waitlist placement and automatically promotes guests when cancellations open up capacity.

from collections import deque

waitlist: deque[dict] = deque()

@function_tool
def add_to_waitlist(
    guest_name: str, phone: str, date: str, time: str, party_size: int
) -> str:
    entry = {
        "guest": guest_name,
        "phone": phone,
        "date": date,
        "time": time,
        "party_size": party_size,
        "added_at": datetime.now().isoformat(),
    }
    waitlist.append(entry)
    position = len(waitlist)
    return (
        f"{guest_name} added to waitlist at position {position}. "
        f"We will notify {phone} if a table opens up."
    )

def promote_from_waitlist(date: str, time: str) -> str | None:
    requested = datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M")
    for i, entry in enumerate(waitlist):
        entry_time = datetime.strptime(
            f"{entry['date']} {entry['time']}", "%Y-%m-%d %H:%M"
        )
        if abs((entry_time - requested).total_seconds()) <= 1800:
            table = restaurant.best_fit_table(entry["party_size"], requested)
            if table:
                promoted = waitlist[i]
                del waitlist[i]
                return f"Promoted {promoted['guest']} from waitlist to table {table.table_id}."
    return None

FAQ

How does the agent handle same-day vs. advance reservations differently?

Same-day reservations query real-time table status (including currently occupied tables and their expected turnover times), while advance reservations only check the future booking calendar. The agent adjusts its availability calculation based on whether the requested time is within the current service period or a future date.

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.

What happens when a guest wants to modify an existing reservation?

The agent treats modifications as a cancel-and-rebook operation atomically. It first verifies availability for the new time and party size, creates the new reservation, and only then cancels the original. This prevents the guest from losing their slot if the new time is unavailable.

How does the waitlist promotion logic avoid double-booking?

The promote_from_waitlist function runs through the best_fit_table method, which checks all existing confirmed reservations before assigning a table. Since confirmed reservations are added to the restaurant's reservation list immediately upon booking, the availability check is always current.


#RestaurantAI #ReservationSystem #AgenticAI #Hospitality #Python #LearnAI #AIEngineering

Share

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.