← Back to Blog

What You'll Build

If you've been searching for a real way to build a chatbot with Claude that actually does something useful, this is it. You're going to build a production-ready multi-agent restaurant system that handles food orders, books reservations, and confirms everything automatically — all in about 200 lines of Python. By the end, you'll have three specialized Claude agents talking to each other, routed by a central orchestrator that decides which agent handles which customer request.

This isn't a toy demo. The pattern you'll learn here is the same one we use at Naples AI to automate real restaurant operations for clients in Southwest Florida.

📦 Full Source Code Note: The complete, working code is built step by step in the sections below. Each snippet is self-contained and clearly labeled. Copy them in order and you'll have the full system running by the end of Step 5.

Prerequisites

  • Python 3.10 or higher
  • An Anthropic API key — grab one at console.anthropic.com
  • Basic Python knowledge (classes, functions, dictionaries)
  • anthropic SDK installed: pip install anthropic
  • A .env file or environment variable set for ANTHROPIC_API_KEY

Step 1: Set Up Claude API and Dependencies

Start by installing the Anthropic SDK and setting up your environment. This one command gets you everything you need.

terminal
pip install anthropic python-dotenv

Now create a .env file in your project root with your API key.

.env
ANTHROPIC_API_KEY=sk-ant-your-key-here

Next, set up your base configuration file. This loads the key and gives you a reusable client you'll import in every agent module.

config.py
import os
from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv()

# Single client instance shared across all agents
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-6"

Step 2: Define the Order-Taking Agent

The order-taking agent is the most complex one because it uses Claude's tool use feature to structure the order data. Instead of parsing freeform text, we give Claude a tool definition and let it fill in the fields automatically. This is what makes the output reliable enough to feed into a real POS system.

agents/order_agent.py
import json
from config import client, MODEL

# Tool definition tells Claude exactly what fields to extract from the conversation
ORDER_TOOLS = [
    {
        "name": "submit_order",
        "description": "Submit a structured food order after collecting all required details from the customer.",
        "input_schema": {
            "type": "object",
            "properties": {
                "items": {
                    "type": "array",
                    "description": "List of ordered menu items",
                    "items": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string", "description": "Menu item name"},
                            "quantity": {"type": "integer", "description": "Number of this item"},
                            "special_instructions": {"type": "string", "description": "Any modifications or allergies"}
                        },
                        "required": ["name", "quantity"]
                    }
                },
                "customer_name": {"type": "string", "description": "Customer's name for the order"},
                "order_type": {"type": "string", "enum": ["dine-in", "takeout", "delivery"], "description": "How the customer wants their order"},
                "estimated_total": {"type": "number", "description": "Estimated total in USD based on menu pricing"}
            },
            "required": ["items", "customer_name", "order_type"]
        }
    }
]

ORDER_SYSTEM_PROMPT = """You are a friendly restaurant order-taking assistant for Bella Vista, an Italian restaurant in Naples, Florida.

Menu items and prices:
- Margherita Pizza: $16
- Pasta Carbonara: $18
- Caesar Salad: $12
- Chicken Parmesan: $22
- Tiramisu: $8
- House Wine (glass): $10

Your job is to:
1. Greet the customer warmly
2. Take their complete order including any special instructions or dietary needs
3. Confirm the order back to them
4. Use the submit_order tool to finalize the order

Always be friendly and confirm the order before submitting it."""


def run_order_agent(user_message: str, conversation_history: list) -> dict:
    """
    Runs one turn of the order-taking agent.
    Returns a dict with 'response' (text to show user) and 'order_data' (if order was submitted).
    """
    # Append the new user message to ongoing conversation
    conversation_history.append({"role": "user", "content": user_message})

    response = client.messages.create(
        model=MODEL,
        max_tokens=1024,
        system=ORDER_SYSTEM_PROMPT,
        tools=ORDER_TOOLS,
        messages=conversation_history
    )

    result = {"response": "", "order_data": None, "history": conversation_history}

    # Check if Claude decided to call a tool
    if response.stop_reason == "tool_use":
        for block in response.content:
            if block.type == "tool_use" and block.name == "submit_order":
                order_data = block.input
                result["order_data"] = order_data

                # Add assistant response and tool result back to history
                conversation_history.append({"role": "assistant", "content": response.content})
                conversation_history.append({
                    "role": "user",
                    "content": [{
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps({"status": "success", "order_id": "ORD-2847"})
                    }]
                })

                # Get Claude's final confirmation message
                confirmation_response = client.messages.create(
                    model=MODEL,
                    max_tokens=512,
                    system=ORDER_SYSTEM_PROMPT,
                    tools=ORDER_TOOLS,
                    messages=conversation_history
                )
                result["response"] = confirmation_response.content[0].text
    else:
        # Claude is still gathering information — just return its message
        for block in response.content:
            if hasattr(block, "text"):
                result["response"] = block.text
        conversation_history.append({"role": "assistant", "content": response.content})

    result["history"] = conversation_history
    return result
💡 Why tool use? Without tools, you'd have to parse Claude's response text to extract order details — fragile and error-prone. With tools, Claude returns structured JSON every single time. That's what makes this production-ready instead of a prototype.

Step 3: Create the Reservation-Booking Agent

The reservation agent works the same way — it uses a tool to capture structured reservation data. Notice it has its own system prompt and tool definition, which means it behaves completely independently from the order agent. That's the whole point of multi-agent design: each agent is focused and doesn't have to know what the others are doing.

agents/reservation_agent.py
import json
from config import client, MODEL

RESERVATION_TOOLS = [
    {
        "name": "book_reservation",
        "description": "Book a table reservation after collecting all required details.",
        "input_schema": {
            "type": "object",
            "properties": {
                "guest_name": {"type": "string", "description": "Name for the reservation"},
                "party_size": {"type": "integer", "description": "Number of guests"},
                "date": {"type": "string", "description": "Reservation date in YYYY-MM-DD format"},
                "time": {"type": "string", "description": "Reservation time in HH:MM format (24-hour)"},
                "special_requests": {"type": "string", "description": "Any special requests like birthday setup, accessibility needs, etc."},
                "contact_phone": {"type": "string", "description": "Phone number for confirmation"}
            },
            "required": ["guest_name", "party_size", "date", "time", "contact_phone"]
        }
    }
]

RESERVATION_SYSTEM_PROMPT = """You are a reservation specialist for Bella Vista Italian Restaurant in Naples, Florida.

Restaurant hours: Tuesday-Sunday, 5:00 PM - 10:00 PM. Closed Mondays.
Available reservation times: 5:00 PM, 5:30 PM, 6:00 PM, 6:30 PM, 7:00 PM, 7:30 PM, 8:00 PM, 8:30 PM, 9:00 PM

Your job is to:
1. Collect the guest's name, party size, preferred date and time, and phone number
2. Check if the requested time is within available slots
3. Ask about any special requests (birthdays, anniversaries, dietary needs)
4. Confirm all details with the guest
5. Use the book_reservation tool to finalize the booking

Today's date for reference: 2026-05-29. Always confirm the day of the week when repeating a date back to the guest."""


def run_reservation_agent(user_message: str, conversation_history: list) -> dict:
    """
    Runs one turn of the reservation-booking agent.
    Returns a dict with 'response' (text) and 'reservation_data' (if booking was completed).
    """
    conversation_history.append({"role": "user", "content": user_message})

    response = client.messages.create(
        model=MODEL,
        max_tokens=1024,
        system=RESERVATION_SYSTEM_PROMPT,
        tools=RESERVATION_TOOLS,
        messages=conversation_history
    )

    result = {"response": "", "reservation_data": None, "history": conversation_history}

    if response.stop_reason == "tool_use":
        for block in response.content:
            if block.type == "tool_use" and block.name == "book_reservation":
                reservation_data = block.input
                result["reservation_data"] = reservation_data

                conversation_history.append({"role": "assistant", "content": response.content})
                conversation_history.append({
                    "role": "user",
                    "content": [{
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps({"status": "confirmed", "confirmation_number": "RES-4491"})
                    }]
                })

                confirmation_response = client.messages.create(
                    model=MODEL,
                    max_tokens=512,
                    system=RESERVATION_SYSTEM_PROMPT,
                    tools=RESERVATION_TOOLS,
                    messages=conversation_history
                )
                result["response"] = confirmation_response.content[0].text
    else:
        for block in response.content:
            if hasattr(block, "text"):
                result["response"] = block.text
        conversation_history.append({"role": "assistant", "content": response.content})

    result["history"] = conversation_history
    return result

Step 4: Build the Order Confirmation Agent

This lightweight agent handles follow-up questions after an order or reservation is placed. It doesn't need tools — it just needs context about what was already confirmed. This is a great example of how not every agent in a system needs to be complex.

agents/confirmation_agent.py
from config import client, MODEL

CONFIRMATION_SYSTEM_PROMPT = """You are a helpful follow-up assistant for Bella Vista Italian Restaurant in Naples, Florida.

Your role is to:
1. Answer questions about existing orders or reservations
2. Provide estimated wait times (kitchen orders: 20-30 min, reservations: confirmed immediately)
3. Handle simple modifications if mentioned (note: major changes require calling the restaurant)
4. Thank the customer warmly and encourage them to enjoy their experience

Restaurant phone for major changes: (239) 555-0192
Restaurant address: 1247 Gulf Shore Blvd, Naples, FL 34102

Keep responses brief and friendly."""


def run_confirmation_agent(user_message: str, context: dict, conversation_history: list) -> dict:
    """
    Runs the confirmation agent with context from a prior order or reservation.
    'context' should contain the order_data or reservation_data dict from a previous agent.
    """
    conversation_history.append({"role": "user", "content": user_message})

    # Inject the confirmed booking context directly into the system prompt
    context_str = f"\n\nCurrent confirmed context:\n{context}" if context else ""

    response = client.messages.create(
        model=MODEL,
        max_tokens=512,
        system=CONFIRMATION_SYSTEM_PROMPT + context_str,
        messages=conversation_history
    )

    reply = response.content[0].text
    conversation_history.append({"role": "assistant", "content": reply})

    return {"response": reply, "history": conversation_history}

Step 5: Connect Agents with Routing Logic

Here's where it all comes together. The main routing agent uses Claude to classify the user's intent and then hands off to the right specialist agent. This is the orchestrator — it doesn't handle orders or reservations itself, it just decides who should. This separation is what makes the system scalable: you can add new agents without rewriting the router.

main.py
import json
from config import client, MODEL
from agents.order_agent import run_order_agent
from agents.reservation_agent import run_reservation_agent
from agents.confirmation_agent import run_confirmation_agent

# ─── Main Routing Agent ────────────────────────────────────────────────────────

ROUTER_SYSTEM_PROMPT = """You are a routing assistant for Bella Vista Italian Restaurant.
Classify the customer's intent into exactly one of these categories:
- "order" — customer wants to place a food or drink order
- "reservation" — customer wants to book a table or check availability
- "confirmation" — customer has a follow-up question about an existing order or reservation
- "general" — any other question (hours, location, menu info)

Respond with ONLY a JSON object like: {"intent": "order"}
No explanation. No extra text. Just the JSON."""


class RestaurantOrchestrator:
    """
    Central routing class that manages agent selection and session state.
    Maintains separate conversation histories per agent so context isn't mixed.
    """

    def __init__(self):
        self.current_agent = None          # Tracks which agent is active
        self.confirmed_context = {}        # Stores completed order/reservation data
        self.order_history = []            # Isolated history for order agent
        self.reservation_history = []      # Isolated history for reservation agent
        self.confirmation_history = []     # Isolated history for confirmation agent

    def classify_intent(self, user_message: str) -> str:
        """Uses Claude to classify what the user wants."""
        response = client.messages.create(
            model=MODEL,
            max_tokens=64,
            system=ROUTER_SYSTEM_PROMPT,
            messages=[{"role": "user", "content": user_message}]
        )
        raw = response.content[0].text.strip()
        try:
            return json.loads(raw).get("intent", "general")
        except json.JSONDecodeError:
            return "general"

    def handle_general(self, user_message: str) -> str:
        """Handles basic restaurant questions without a specialist agent."""
        response = client.messages.create(
            model=MODEL,
            max_tokens=512,
            system="""You are a helpful assistant for Bella Vista Italian Restaurant in Naples, FL.
Hours: Tue-Sun 5PM-10PM. Address: 1247 Gulf Shore Blvd, Naples, FL 34102.
Phone: (239) 555-0192. Answer briefly and warmly.""",
            messages=[{"role": "user", "content": user_message}]
        )
        return response.content[0].text

    def run(self, user_message: str) -> str:
        """
        Main entry point. Routes each message to the correct agent
        or continues with the currently active agent if a session is ongoing.
        """
        # If we're mid-conversation with a specific agent, keep going with it
        if self.current_agent == "order":
            result = run_order_agent(user_message, self.order_history)
            self.order_history = result["history"]
            if result.get("order_data"):
                self.confirmed_context = {"order": result["order_data"]}
                self.current_agent = "confirmation"  # Switch to follow-up mode
            return result["response"]

        if self.current_agent == "reservation":
            result = run_reservation_agent(user_message, self.reservation_history)
            self.reservation_history = result["history"]
            if result.get("reservation_data"):
                self.confirmed_context = {"reservation": result["reservation_data"]}
                self.current_agent = "confirmation"
            return result["response"]

        if self.current_agent == "confirmation":
            result = run_confirmation_agent(user_message, self.confirmed_context, self.confirmation_history)
            self.confirmation_history = result["history"]
            return result["response"]

        # No active agent — classify intent and route
        intent = self.classify_intent(user_message)
        print(f"  [Router] Detected intent: {intent}")

        if intent == "order":
            self.current_agent = "order"
            result = run_order_agent(user_message, self.order_history)
            self.order_history = result["history"]
            return result["response"]

        elif intent == "reservation":
            self.current_agent = "reservation"
            result = run_reservation_agent(user_message, self.reservation_history)
            self.reservation_history = result["history"]
            return result["response"]

        elif intent == "confirmation":
            self.current_agent = "confirmation"
            result = run_confirmation_agent(user_message, self.confirmed_context, self.confirmation_history)
            self.confirmation_history = result["history"]
            return result["response"]

        else:
            return self.handle_general(user_message)


# ─── Agent Orchestration Run Loop ─────────────────────────────────────────────

def main():
    print("=" * 60)
    print("  Bella Vista Restaurant — AI Assistant")
    print("  Powered by Naples AI  |  naplesai.agency")
    print("=" * 60)
    print("Type 'quit' to exit.\n")

    orchestrator = RestaurantOrchestrator()

    while True:
        user_input = input("You: ").strip()

        if not user_input:
            continue

        if user_input.lower() in ["quit", "exit", "bye"]:
            print("Assistant: Thanks for choosing Bella Vista. See you soon!")
            break

        response = orchestrator.run(user_input)
        print(f"\nAssistant: {response}\n")


if __name__ == "__main__":
    main()

Example Conversation Output with JSON Responses

Here's what a real session looks like when you run the system. I've included the router's internal intent labels so you can see what's happening under the hood.

sample_output.txt
============================================================
  Bella Vista Restaurant — AI Assistant
  Powered by Naples AI  |  naplesai.agency
============================================================
Type 'quit' to exit.

You: I'd like to order some food for pickup

  [Router] Detected intent: order