← Back to Blog

What You'll Build

If you've been trying to build chatbot AI agent systems that actually handle real business logic — not just chitchat — this tutorial is exactly what you need. By the end, you'll have a fully working restaurant order agent in Python that can browse a menu, take orders, validate payments, and confirm purchases using Claude's tool-use API.

The whole thing runs in about 200 lines of code and handles multi-turn conversations the way a real order-taking system would. No toy demos — this is production-pattern code you can actually adapt.

Prerequisites

  • Python 3.10 or higher installed
  • An Anthropic API key (get one here)
  • Basic Python knowledge — functions, dictionaries, loops
  • anthropic SDK installed: pip install anthropic
  • A terminal and a text editor (VS Code works great)
📦 Full Source Code
The complete working code for this restaurant order agent is broken into steps below. Each snippet builds on the last, and the final section shows the full file assembled together. Copy each block in order and you'll have a running agent by the end of this tutorial.

Step 1: Set Up Your Claude API Project and Environment

Start by creating a project folder and setting your API key as an environment variable. Never hardcode API keys in source files — it's a habit worth building early.

terminal
mkdir restaurant-agent
cd restaurant-agent
touch agent.py
export ANTHROPIC_API_KEY="your-api-key-here"

Now open agent.py and add the initial imports and configuration. We're using claude-sonnet-4-6 specifically because it handles tool use reliably and responds fast enough for interactive order taking.

agent.py — imports and config
import os
import json
import anthropic

# Initialize the Anthropic client using the environment variable
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

MODEL_NAME = "claude-sonnet-4-6"

# In-memory order state — replace with a database in production
current_order = {
    "items": [],
    "total": 0.0,
    "status": "open"
}

Step 2: Define Your Restaurant Order Agent Tools

Claude's tool-use system lets the model call functions in your code. You define what the tools do, what parameters they accept, and Claude decides when to call them based on the conversation. This is the core pattern behind most real-world AI agents examples you'll find in production.

We're defining three tools: one to retrieve the menu, one to add items to an order, and one to process payment and confirm. Add this block right after your config.

agent.py — tool definitions
tools = [
    {
        "name": "get_menu",
        "description": (
            "Retrieves the current restaurant menu including all available items, "
            "descriptions, and prices. Call this when the customer asks what's available "
            "or wants to see the menu."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "category": {
                    "type": "string",
                    "description": (
                        "Optional menu category to filter by: "
                        "'appetizers', 'mains', 'desserts', 'drinks'. "
                        "Leave empty to get the full menu."
                    )
                }
            },
            "required": []
        }
    },
    {
        "name": "add_to_order",
        "description": (
            "Adds a menu item to the customer's current order. "
            "Validate that the item exists on the menu before calling this."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "item_name": {
                    "type": "string",
                    "description": "The exact name of the menu item to add."
                },
                "quantity": {
                    "type": "integer",
                    "description": "Number of this item to add. Must be 1 or greater."
                },
                "special_instructions": {
                    "type": "string",
                    "description": "Any special prep notes, e.g. 'no onions', 'extra sauce'."
                }
            },
            "required": ["item_name", "quantity"]
        }
    },
    {
        "name": "process_payment",
        "description": (
            "Validates the customer's payment details and confirms the order. "
            "Only call this after the customer explicitly says they want to pay "
            "and the order has at least one item."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "payment_method": {
                    "type": "string",
                    "description": "Payment type: 'credit_card', 'debit_card', or 'cash'."
                },
                "card_last_four": {
                    "type": "string",
                    "description": "Last four digits of the card (only required for card payments)."
                }
            },
            "required": ["payment_method"]
        }
    }
]
💡 Tip on Tool Descriptions
The description field is what Claude reads to decide when to call a tool. Be specific. "Call this when the customer asks what's available" is much more useful than "gets the menu." Bad descriptions are the #1 reason agents call the wrong tool.

Step 3: Build the Agent Loop with Anthropic SDK

The agent loop is the heart of this whole system. It sends messages to Claude, checks if Claude wants to use a tool, executes that tool, feeds the result back, and keeps going until Claude gives a final text response. This is the Claude API tutorial pattern you'll reuse in almost every agent you build.

agent.py — tool execution and agent loop
def execute_tool(tool_name: str, tool_input: dict) -> str:
    """Routes tool calls to the correct handler function."""
    if tool_name == "get_menu":
        return get_menu(tool_input.get("category", ""))
    elif tool_name == "add_to_order":
        return add_to_order(
            tool_input["item_name"],
            tool_input["quantity"],
            tool_input.get("special_instructions", "")
        )
    elif tool_name == "process_payment":
        return process_payment(
            tool_input["payment_method"],
            tool_input.get("card_last_four", "")
        )
    else:
        return json.dumps({"error": f"Unknown tool: {tool_name}"})


class RestaurantOrderAgent:
    def __init__(self):
        self.conversation_history = []
        self.system_prompt = (
            "You are a friendly and efficient restaurant order assistant. "
            "Help customers browse the menu, place orders, and complete payment. "
            "Always confirm item details before adding to an order. "
            "Be concise — this is a fast-paced restaurant environment. "
            "Never make up menu items. Always use the get_menu tool to check availability."
        )

    def tool_use_loop(self, user_message: str) -> str:
        """
        Sends a user message and runs the agent loop until Claude
        returns a final text response with no pending tool calls.
        """
        # Append the new user message to conversation history
        self.conversation_history.append({
            "role": "user",
            "content": user_message
        })

        while True:
            response = client.messages.create(
                model=MODEL_NAME,
                max_tokens=1024,
                system=self.system_prompt,
                tools=tools,
                messages=self.conversation_history
            )

            # Append Claude's full response to history (preserves tool_use blocks)
            self.conversation_history.append({
                "role": "assistant",
                "content": response.content
            })

            # If Claude is done (no more tool calls), return the final text
            if response.stop_reason == "end_turn":
                for block in response.content:
                    if hasattr(block, "text"):
                        return block.text
                return ""

            # Claude wants to use a tool — find and execute all tool_use blocks
            tool_results = []
            has_tool_use = False

            for block in response.content:
                if block.type == "tool_use":
                    has_tool_use = True
                    print(f"\n[Agent] Calling tool: {block.name}")
                    print(f"[Agent] Input: {json.dumps(block.input, indent=2)}")

                    result = execute_tool(block.name, block.input)
                    print(f"[Agent] Result: {result}")

                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result
                    })

            # If there were tool calls, feed results back into the conversation
            if has_tool_use:
                self.conversation_history.append({
                    "role": "user",
                    "content": tool_results
                })
            else:
                # No tool use and stop reason wasn't end_turn — extract any text
                for block in response.content:
                    if hasattr(block, "text"):
                        return block.text
                return ""


    def chat(self, user_message: str) -> str:
        """Public interface for sending messages to the agent."""
        return self.tool_use_loop(user_message)

Step 4: Add Order Processing and Validation Logic

Now we wire up the actual tool functions. These are plain Python — no magic here. The menu is a hardcoded dictionary for this tutorial, but in production you'd pull this from your POS system or database.

The payment validation is intentionally simple but shows the right structure: check inputs, update state, return a clear JSON result. Claude reads that JSON and decides what to tell the customer.

agent.py — menu, order, and payment tools
MENU = {
    "appetizers": [
        {"name": "Garlic Bread", "price": 5.99, "description": "Toasted with herb butter"},
        {"name": "Caesar Salad", "price": 8.99, "description": "Romaine, parmesan, house dressing"},
        {"name": "Mozzarella Sticks", "price": 7.49, "description": "Six pieces with marinara"},
    ],
    "mains": [
        {"name": "Margherita Pizza", "price": 14.99, "description": "Fresh mozzarella, basil, tomato"},
        {"name": "Spaghetti Bolognese", "price": 13.49, "description": "House-made meat sauce"},
        {"name": "Grilled Salmon", "price": 18.99, "description": "With lemon butter and seasonal veg"},
        {"name": "Cheeseburger", "price": 12.99, "description": "8oz beef, cheddar, brioche bun"},
    ],
    "desserts": [
        {"name": "Tiramisu", "price": 6.99, "description": "Classic Italian, serves one"},
        {"name": "Chocolate Lava Cake", "price": 7.49, "description": "Warm, with vanilla ice cream"},
    ],
    "drinks": [
        {"name": "Sparkling Water", "price": 2.99, "description": "500ml bottle"},
        {"name": "House Red Wine", "price": 8.99, "description": "Glass, rotating selection"},
        {"name": "Craft Lemonade", "price": 3.99, "description": "Fresh squeezed, mint garnish"},
    ]
}


def get_menu(category: str = "") -> str:
    """Returns menu items as a JSON string, optionally filtered by category."""
    if category and category in MENU:
        result = {category: MENU[category]}
    else:
        result = MENU

    return json.dumps(result)


def add_to_order(item_name: str, quantity: int, special_instructions: str = "") -> str:
    """Adds a validated item to the current order and recalculates total."""
    # Search all categories for the item
    found_item = None
    for category_items in MENU.values():
        for item in category_items:
            if item["name"].lower() == item_name.lower():
                found_item = item
                break
        if found_item:
            break

    if not found_item:
        return json.dumps({
            "success": False,
            "error": f"'{item_name}' not found on the menu. Please check the menu and try again."
        })

    if quantity < 1:
        return json.dumps({
            "success": False,
            "error": "Quantity must be at least 1."
        })

    line_total = found_item["price"] * quantity

    order_item = {
        "name": found_item["name"],
        "quantity": quantity,
        "unit_price": found_item["price"],
        "line_total": round(line_total, 2),
        "special_instructions": special_instructions
    }

    current_order["items"].append(order_item)
    current_order["total"] = round(current_order["total"] + line_total, 2)

    return json.dumps({
        "success": True,
        "added": order_item,
        "order_summary": {
            "items": current_order["items"],
            "running_total": current_order["total"]
        }
    })


def process_payment(payment_method: str, card_last_four: str = "") -> str:
    """Validates payment details and confirms the order if everything checks out."""
    valid_methods = ["credit_card", "debit_card", "cash"]

    if payment_method not in valid_methods:
        return json.dumps({
            "success": False,
            "error": f"Invalid payment method. Choose from: {', '.join(valid_methods)}"
        })

    if not current_order["items"]:
        return json.dumps({
            "success": False,
            "error": "Cannot process payment — the order is empty."
        })

    # Card payments require the last four digits
    if payment_method in ["credit_card", "debit_card"]:
        if not card_last_four or len(card_last_four) != 4 or not card_last_four.isdigit():
            return json.dumps({
                "success": False,
                "error": "Card payments require a valid 4-digit card number ending."
            })

    # Calculate tax (7% — Florida state sales tax)
    subtotal = current_order["total"]
    tax = round(subtotal * 0.07, 2)
    grand_total = round(subtotal + tax, 2)

    # Mark order as confirmed
    current_order["status"] = "confirmed"

    confirmation_number = "ORD-2026-" + str(abs(hash(str(current_order["items"]))))[:6]

    return json.dumps({
        "success": True,
        "confirmation_number": confirmation_number,
        "payment_method": payment_method,
        "subtotal": subtotal,
        "tax": tax,
        "grand_total": grand_total,
        "items_ordered": current_order["items"],
        "message": "Order confirmed! Your food will be ready in approximately 20-25 minutes."
    })

Full Source Code — Putting It All Together

Here's the main entry point that runs the conversation loop in your terminal. This is what ties everything together and lets you talk to the agent interactively.

agent.py — main entry point
def main():
    print("=== Naples AI Restaurant Order Agent ===")
    print("Type 'quit' to exit\n")

    agent = RestaurantOrderAgent()

    while True:
        user_input = input("You: ").strip()
        if user_input.lower() in ["quit", "exit", "q"]:
            print("Thanks for dining with us!")
            break
        if not user_input:
            continue

        response = agent.chat(user_input)
        print(f"\nAgent: {response}\n")


if __name__ == "__main__":
    main()

Example Conversation Output — Multiple Agent Turns

Here's what an actual session looks like when you run the agent. The [Agent] lines are the internal tool calls being logged — in a production UI you'd hide those from the customer.

terminal output
=== Naples AI Restaurant Order Agent ===
Type 'quit' to exit

You: Hi! What do you have on the menu tonight?

[Agent] Calling tool: get_menu
[Agent] Input: {}
[Agent] Result: {"appetizers": [{"name": "Garlic Bread", "price": 5.99, ...}, ...], ...}

Agent: Here's what we have tonight:

🥗 **Appetizers**
- Garlic Bread — $5.99 (Toasted with herb butter)
- Caesar Salad — $8.99 (Romaine, parmesan, house dressing)
- Mozzarella Sticks — $7.49 (Six pieces with marinara)

🍕 **Mains**
- Margherita Pizza — $14.99
- Spaghetti Bolognese — $13.49
- Grilled Salmon — $18.99
- Cheeseburger — $12.99

🍰 **Desserts** & 🥤 **Drinks** also available — just ask!

What can I get started for you?

You: I'll take a Cheeseburger and two Craft Lemonades please

[Agent] Calling tool: add_to_order
[Agent] Input: {"item_name": "Cheeseburger", "quantity": 1}
[Agent] Result: {"success": true, "added": {"name": "Cheeseburger", "quantity": 1, "unit_price": 12.99, "line_total": 12.99}, "order_summary": {"running_total": 12.99}}

[Agent] Calling tool: add_to_order
[Agent] Input: {"item_name": "Craft Lemonade", "quantity": 2}
[Agent] Result: {"success": true, "added": {"name": "Craft Lemonade", "quantity": 2, "unit_price": 3.99, "line_total": 7.98}, "order_summary": {"running_total": 20.97}}

Agent: Got it! Here's your order so far:
- 1x Cheeseburger — $12.99
- 2x Craft Lemonade — $7.98

Running total: **$20.97** (+ 7% FL tax at checkout)

Anything else, or are you ready to pay?

You: That's everything. I'll pay by credit card, last four digits 4242

[Agent] Calling tool: process_payment
[Agent] Input: {"payment_method": "credit_card", "card_last_four": "4242"}
[Agent] Result: {"success": true, "confirmation_number": "ORD-2026-847291", "subtotal": 20.97, "tax": 1.47, "grand_total": 22.44, "message": "Order confirmed! Your food will be ready in approximately 20-25 minutes."}

Agent: Your order is confirmed! 🎉

**Order #ORD-2026-847291**
- 1x Cheeseburger
- 2x Craft Lemonade
- Subtotal: $20.97
- Tax: $1.47
- **Total charged to card ending 4242: $22.44**

Your food will be ready in approximately 20-25 minutes. Enjoy your meal!

How It Works

Claude doesn't execute code directly — it reads your tool definitions and decides which ones to call based on the conversation. When it wants a tool, it returns a tool_use block instead of text, and your loop catches that, runs the real Python function, and sends the result back as a tool_result.

The loop keeps running until Claude's stop_reason is end_turn, which means it's satisfied and ready to respond to the customer. Multi-step operations — like adding two items in one message — result in multiple tool calls in a single turn, which the loop handles by batching all results before the next API call.

The conversation history grows with each turn, giving Claude the full context of everything said and every tool result seen. This is what makes it feel like a real conversation rather than a one-shot command.

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. Double-check that you set the environment variable in the same terminal session you're running the script from. Run echo $ANTHROPIC_API_KEY to confirm it's set. If you restarted your terminal, you need to export it again or add it to your .bash