← Back to Blog

What You'll Build

By the end of this tutorial, you'll have a fully working restaurant ordering agent built in Python using the Claude API. It handles food orders, table reservations, and menu inquiries — all in a single conversational loop under 200 lines of code. This is the exact kind of agent we build for restaurant clients here at Naples AI, and I'm going to walk you through it piece by piece.

Prerequisites

  • Python 3.9 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with Python classes and functions
  • anthropic SDK installed: pip install anthropic
  • A .env file or shell variable with ANTHROPIC_API_KEY set
📦 Full Source Code
The complete, working code is built step by step in the sections below. Each snippet builds on the last, so by Step 5 you'll have the entire agent ready to run. Copy each section in order and you're good to go.

Step 1: Set Up Your Claude API Environment and Imports

First, let's get the foundation in place. We need the Anthropic SDK, a way to load our API key, and a basic client setup. I keep environment variables in a .env file so my key never ends up in source control.

restaurant_agent.py
import os
import json
from datetime import datetime
import anthropic

# Load your API key from the environment — never hardcode this
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

MODEL = "claude-sonnet-4-6"

# In-memory storage for this demo (swap for a real DB in production)
orders = []
reservations = []

That's all the setup you need. The anthropic.Anthropic() client handles authentication and all HTTP requests to the API. We're using claude-sonnet-4-6 because it's fast, accurate with tool use, and cost-effective for high-volume restaurant traffic.

Step 2: Define Your Restaurant Ordering Tools and Schemas

This is where the real power of the Claude API comes in. You define tools as JSON schemas, and Claude decides when and how to call them based on the conversation. Think of tools as structured actions the agent can take — placing an order, booking a table, or looking up the menu.

Here are the three tools our restaurant agent needs. I'll explain each one after the code.

restaurant_agent.py (continued)
tools = [
    {
        "name": "place_order",
        "description": (
            "Place a food or drink order for a customer. "
            "Use this when the customer confirms what they want to order."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "items": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "List of menu items being ordered, e.g. ['Margherita Pizza', 'Iced Tea']"
                },
                "table_number": {
                    "type": "integer",
                    "description": "The customer's table number"
                },
                "special_instructions": {
                    "type": "string",
                    "description": "Any dietary notes or customizations (optional)"
                }
            },
            "required": ["items", "table_number"]
        }
    },
    {
        "name": "make_reservation",
        "description": (
            "Book a table reservation for a customer. "
            "Use this when the customer wants to reserve a table for a future date and time."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "customer_name": {
                    "type": "string",
                    "description": "Full name for the reservation"
                },
                "party_size": {
                    "type": "integer",
                    "description": "Number of guests in the party"
                },
                "date": {
                    "type": "string",
                    "description": "Reservation date in YYYY-MM-DD format"
                },
                "time": {
                    "type": "string",
                    "description": "Reservation time in HH:MM format (24-hour)"
                },
                "phone": {
                    "type": "string",
                    "description": "Customer phone number for confirmation"
                }
            },
            "required": ["customer_name", "party_size", "date", "time"]
        }
    },
    {
        "name": "get_menu",
        "description": (
            "Retrieve the restaurant menu. Use this when the customer asks "
            "what's available, wants to see the menu, or asks about specific dishes."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "category": {
                    "type": "string",
                    "enum": ["appetizers", "mains", "desserts", "drinks", "all"],
                    "description": "Which section of the menu to retrieve"
                }
            },
            "required": ["category"]
        }
    }
]

Each tool has a name, a plain-English description that Claude reads to decide when to use it, and an input_schema that tells Claude exactly what data to extract from the conversation. The required field is important — Claude won't call the tool until it has those fields from the user.

Step 3: Create the Main Agent Loop with Tool Use

Now we build the core of the agent — the agentic loop. This is the part that keeps the conversation going, processes tool calls from Claude, and feeds results back so Claude can respond naturally. This loop is what makes it an agent rather than just a chatbot.

restaurant_agent.py (continued)
def process_tool_call(tool_name: str, tool_input: dict) -> str:
    """Execute a tool and return the result as a string."""
    if tool_name == "place_order":
        return handle_place_order(tool_input)
    elif tool_name == "make_reservation":
        return handle_reservation(tool_input)
    elif tool_name == "get_menu":
        return handle_get_menu(tool_input)
    return "Tool not found."


def run_agent(user_message: str, conversation_history: list) -> tuple[str, list]:
    """
    Main agentic loop. Sends messages to Claude, handles tool calls,
    and returns the final text response plus updated history.
    """
    # Append the new user message to the ongoing conversation
    conversation_history.append({"role": "user", "content": user_message})

    system_prompt = (
        "You are a friendly and efficient restaurant assistant for Bella Napoli Ristorante "
        "in Naples, Florida. Help customers place orders, make reservations, and answer "
        "menu questions. Always confirm order details before placing them. Be warm, concise, "
        "and professional. If a customer seems unsure, suggest popular dishes."
    )

    # Agentic loop — keeps running until Claude gives a final text response
    while True:
        response = client.messages.create(
            model=MODEL,
            max_tokens=1024,
            system=system_prompt,
            tools=tools,
            messages=conversation_history
        )

        # Claude finished without needing a tool — return the text response
        if response.stop_reason == "end_turn":
            final_text = ""
            for block in response.content:
                if hasattr(block, "text"):
                    final_text = block.text
            conversation_history.append({"role": "assistant", "content": response.content})
            return final_text, conversation_history

        # Claude wants to use a tool
        if response.stop_reason == "tool_use":
            # Append Claude's tool-use request to history
            conversation_history.append({"role": "assistant", "content": response.content})

            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    tool_result = process_tool_call(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": tool_result
                    })

            # Feed the tool results back to Claude so it can continue
            conversation_history.append({"role": "user", "content": tool_results})
            # Loop continues — Claude will now generate a response using the tool result

The key thing to understand here is the loop. Claude either returns text (done) or requests a tool (keep going). When it requests a tool, we run the tool locally and pass the result back as a tool_result message. Claude then uses that result to write a natural response to the customer.

💡 Why the loop matters: Some tasks require multiple tool calls in sequence. A customer might ask "Can I book a table and also place a pre-order?" — Claude will call make_reservation, process that result, then call place_order, all in the same conversation turn. The loop handles this automatically.

Step 4: Handle Customer Orders and Confirmations

Now let's write the actual business logic that runs when Claude calls a tool. These functions simulate what would normally be a database write or POS system call. In production, you'd replace the in-memory lists with real database inserts.

restaurant_agent.py (continued)
def handle_place_order(tool_input: dict) -> str:
    """Process a food order and store it."""
    order_id = f"ORD-{len(orders) + 1001}"
    order = {
        "order_id": order_id,
        "items": tool_input["items"],
        "table_number": tool_input["table_number"],
        "special_instructions": tool_input.get("special_instructions", "None"),
        "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "status": "confirmed"
    }
    orders.append(order)

    items_list = ", ".join(tool_input["items"])
    result = (
        f"Order {order_id} confirmed for table {tool_input['table_number']}. "
        f"Items: {items_list}. "
        f"Special instructions: {order['special_instructions']}. "
        f"Estimated wait time: 20-25 minutes."
    )
    return result

The function generates a unique order ID, stores the order, and returns a confirmation string. Claude reads that string and turns it into a friendly message for the customer — you don't need to format it perfectly, just make sure it has the key facts.

Step 5: Add Reservation and Menu Inquiry Capabilities

Here's the rest of the tool logic — reservations and menu lookups. The menu is hardcoded here for simplicity, but in a real system you'd pull this from your POS or a database.

restaurant_agent.py (continued)
def handle_reservation(tool_input: dict) -> str:
    """Book a table reservation."""
    reservation_id = f"RES-{len(reservations) + 2001}"
    reservation = {
        "reservation_id": reservation_id,
        "customer_name": tool_input["customer_name"],
        "party_size": tool_input["party_size"],
        "date": tool_input["date"],
        "time": tool_input["time"],
        "phone": tool_input.get("phone", "Not provided"),
        "status": "confirmed"
    }
    reservations.append(reservation)

    return (
        f"Reservation {reservation_id} confirmed for {tool_input['customer_name']}, "
        f"party of {tool_input['party_size']} on {tool_input['date']} at {tool_input['time']}. "
        f"Contact: {reservation['phone']}."
    )


def handle_get_menu(tool_input: dict) -> str:
    """Return menu items for the requested category."""
    menu = {
        "appetizers": [
            "Bruschetta al Pomodoro - $12",
            "Calamari Fritti - $16",
            "Burrata con Prosciutto - $18"
        ],
        "mains": [
            "Margherita Pizza - $22",
            "Rigatoni all'Amatriciana - $24",
            "Grilled Branzino - $34",
            "Osso Buco alla Milanese - $42"
        ],
        "desserts": [
            "Tiramisu - $10",
            "Panna Cotta - $9",
            "Cannoli Siciliani - $8"
        ],
        "drinks": [
            "House Red Wine (glass) - $12",
            "Aperol Spritz - $14",
            "San Pellegrino - $5",
            "Espresso - $4"
        ]
    }

    category = tool_input["category"]
    if category == "all":
        result_parts = []
        for cat, items in menu.items():
            result_parts.append(f"{cat.upper()}:\n" + "\n".join(items))
        return "\n\n".join(result_parts)

    if category in menu:
        return f"{category.upper()}:\n" + "\n".join(menu[category])

    return "Category not found. Available categories: appetizers, mains, desserts, drinks, all."


# Entry point — simple CLI loop to test the agent
if __name__ == "__main__":
    print("Welcome to Bella Napoli Ristorante! How can I help you today?")
    print("(Type 'quit' to exit)\n")

    history = []
    while True:
        user_input = input("You: ").strip()
        if user_input.lower() in ("quit", "exit"):
            print("Thank you for visiting Bella Napoli!")
            break
        if not user_input:
            continue

        response_text, history = run_agent(user_input, history)
        print(f"\nAgent: {response_text}\n")

The if __name__ == "__main__" block gives you a simple CLI to test everything right away. Run python restaurant_agent.py and start chatting. The history list carries the full conversation so Claude remembers what was said earlier in the session.

Example Conversation Output

Here's what a real session looks like when you run the agent. This is actual output from the code above — nothing cleaned up.

terminal output
Welcome to Bella Napoli Ristorante! How can I help you today?
(Type 'quit' to exit)

You: Hi, what do you have for mains?

Agent: Here are our main courses:

- Margherita Pizza - $22
- Rigatoni all'Amatriciana - $24
- Grilled Branzino - $34
- Osso Buco alla Milanese - $42

Our Osso Buco is especially popular — it's a slow-braised veal shank
with saffron risotto. Can I help you order, or would you like to see
our appetizers or desserts as well?

You: I'll take the Margherita Pizza and a glass of house red. Table 7.

Agent: Just to confirm your order for table 7:
- Margherita Pizza - $22
- House Red Wine (glass) - $12

Total: $34 before tax. Shall I go ahead and place that?

You: Yes please

Agent: Your order is confirmed! Order ORD-1001 is in for table 7.
Your Margherita Pizza and house red wine will be with you in about
20–25 minutes. Enjoy your meal! 🍷

You: Can I also book a table for next Friday for 4 people?

Agent: Of course! I just need a few details:
- What name should the reservation be under?
- What time works best for your party?
- A phone number for confirmation (optional but helpful)

You: Maria Rossi, 7pm, 555-0192

Agent: Perfect — reservation RES-2001 is confirmed for Maria Rossi,
party of 4 on 2026-06-12 at 19:00. We'll see you then!
Is there anything else I can help you with?

How It Works

Claude reads the conversation, decides which tool fits the customer's request, and calls it with structured JSON extracted directly from plain English. Your code executes the tool and returns a result string, and Claude weaves that into a natural response. The whole loop runs in milliseconds.

The system prompt is doing quiet but important work. By telling Claude it's a restaurant assistant at a specific Naples restaurant, it stays on-topic, uses the right tone, and knows to confirm orders before placing them. Specificity in the system prompt pays off.

The conversation history is the agent's memory. Every message — user, assistant, tool calls, tool results — gets appended to the same list and sent on every turn. That's how Claude knows a customer already ordered pizza when they ask about adding dessert.

Common Errors and Fixes

Error 1: AuthenticationError on startup

anthropic.AuthenticationError: 401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}

Fix: Your API key isn't being read correctly. Run echo $ANTHROPIC_API_KEY in your terminal. If it's blank, export it first: export ANTHROPIC_API_KEY="sk-ant-...". Alternatively, pass it directly in the client: anthropic.Anthropic(api_key="sk-ant-...") — just don't commit that to Git.

Error 2: KeyError when accessing tool input fields

KeyError: 'table_number'

Fix: Claude didn't extract a required field because the user didn't provide it. Add it to the required list in your tool schema — Claude will then ask the user for it before calling the tool. For optional fields, always use tool_input.get("field_name", "default") instead of direct bracket access.

Error 3: Infinite loop — agent never returns a text response

# The while True loop runs forever, no output appears

Fix: This usually means your tool result isn't being returned as a proper tool_result message. Double-check that your tool results list uses "type": "tool_result" and that tool_use_id matches block.id exactly. Also make sure max_tokens is high enough — if Claude runs out of tokens mid-response, stop_reason becomes max_tokens instead of end_turn, which the loop doesn't handle. Add an elif response.stop_reason == "max_tokens" branch if needed.

Next Steps

This agent works right now, but there's a lot of room to make it production-ready. Here are four directions worth exploring:

  • Connect a real database. Replace the in-memory lists with PostgreSQL or Supabase. Add a get_order_status tool so customers can check on their order by ID.
  • Add a web interface. Wrap the agent in a FastAPI app and stream responses to a chat widget on your restaurant's website. Claude's streaming API makes this feel instantaneous.
  • Integrate with a POS system. Most modern POS platforms (Square, Toast, Lightspeed) have REST APIs. Your tool functions become API calls instead of list appends.
  • Add multi-language support. Update the system prompt to detect the user's language and respond accordingly. In Southwest Florida, Spanish support alone can meaningfully expand your customer reach.

Frequently Asked Questions

How do I give Claude API tools in Python?

You pass a tools list to client.messages.create(). Each tool is a dictionary with a name, description, and input_schema following JSON Schema format. When response.stop_reason equals "tool_use", Claude is requesting a tool call — you run the function locally and return the result as a tool_result message in the conversation history.

What is the difference between a chatbot and an AI agent?

A chatbot responds to messages with text. An agent can take actions — it uses tools to read data, write to systems, call APIs, and make decisions based on the results. The agentic loop in this tutorial is what makes it an agent