← Back to Blog

What You'll Build

By the end of this tutorial you'll have a working AI restaurant order agent that can look up menu items, validate customer orders, and submit them — all through a natural back-and-forth conversation. The agent runs in Python using Anthropic's Claude API and handles the full order flow without any hardcoded decision trees. You'll go from zero to a live demo in about 30 minutes.

📦 Full Source Code Note
The complete, working code is built up piece by piece in the steps below. Every snippet is copy-paste ready and syntactically correct. By Step 4 you'll have a single file you can run immediately.

Prerequisites

  • Python 3.9 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with Python functions and dictionaries
  • pip available in your terminal
  • About 30 minutes of uninterrupted time

Step 1: Set Up Claude API and Install Dependencies

First, install the Anthropic Python SDK. That's the only external dependency you actually need for this project.

terminal
pip install anthropic

Next, store your API key as an environment variable. Never hardcode it in your source file.

terminal
export ANTHROPIC_API_KEY="sk-ant-your-key-here"

Now create a new file called restaurant_agent.py and start with the class setup. This is the foundation everything else builds on.

restaurant_agent.py
import os
import json
import anthropic

class RestaurantOrderAgent:
    def __init__(self):
        # Initialize the Anthropic client using ANTHROPIC_API_KEY from environment
        self.client = anthropic.Anthropic()
        self.model = "claude-sonnet-4-6"
        self.conversation_history = []

        # Simple in-memory order state for this session
        self.current_order = []
        self.order_submitted = False

    def reset_order(self):
        self.current_order = []
        self.order_submitted = False
        self.conversation_history = []

The conversation_history list is what makes multi-turn conversation work. Every message — both from the user and from Claude — gets appended there so the model always has full context.

Step 2: Define Tool Functions for Menu Lookup and Order Creation

Claude can call tools, but it needs two things: a Python function that actually does the work, and a JSON schema that tells the model what the tool does and what arguments it takes. Let's build both.

Here are the three tool functions. I kept the menu realistic for a Naples-style casual restaurant.

restaurant_agent.py (add to class)
    # ── Tool Functions ──────────────────────────────────────────────────────

    def lookup_menu(self, category: str = "all") -> dict:
        """Return menu items, optionally filtered by category."""
        full_menu = {
            "appetizers": [
                {"name": "Shrimp Cocktail",   "price": 14.00, "id": "APP001"},
                {"name": "Calamari Fritti",   "price": 12.00, "id": "APP002"},
                {"name": "Bruschetta",        "price": 9.00,  "id": "APP003"},
            ],
            "mains": [
                {"name": "Grilled Snapper",   "price": 28.00, "id": "MAN001"},
                {"name": "Lobster Pasta",     "price": 34.00, "id": "MAN002"},
                {"name": "Filet Mignon 8oz",  "price": 46.00, "id": "MAN003"},
                {"name": "Margherita Pizza",  "price": 18.00, "id": "MAN004"},
            ],
            "drinks": [
                {"name": "House Wine (glass)","price": 9.00,  "id": "DRK001"},
                {"name": "Craft Beer",        "price": 7.00,  "id": "DRK002"},
                {"name": "Sparkling Water",   "price": 4.00,  "id": "DRK003"},
            ],
            "desserts": [
                {"name": "Key Lime Pie",      "price": 8.00,  "id": "DST001"},
                {"name": "Tiramisu",          "price": 9.00,  "id": "DST002"},
            ],
        }
        if category == "all":
            return full_menu
        return {category: full_menu.get(category, [])}

    def validate_and_add_item(self, item_id: str, quantity: int) -> dict:
        """Validate that an item exists and add it to the current order."""
        # Build a flat lookup dict from all menu items
        all_items = {}
        menu = self.lookup_menu()
        for category_items in menu.values():
            for item in category_items:
                all_items[item["id"]] = item

        if item_id not in all_items:
            return {"success": False, "error": f"Item ID '{item_id}' not found on menu."}

        item = all_items[item_id]
        order_line = {
            "item_id":   item_id,
            "name":      item["name"],
            "price":     item["price"],
            "quantity":  quantity,
            "subtotal":  round(item["price"] * quantity, 2),
        }
        self.current_order.append(order_line)
        return {"success": True, "added": order_line, "order_so_far": self.current_order}

    def submit_order(self, customer_name: str, table_number: int) -> dict:
        """Finalize and submit the order to the kitchen."""
        if not self.current_order:
            return {"success": False, "error": "Cannot submit an empty order."}

        total = round(sum(line["subtotal"] for line in self.current_order), 2)
        order_record = {
            "order_id":      f"ORD-{table_number:03d}-2026",
            "customer_name": customer_name,
            "table_number":  table_number,
            "items":         self.current_order,
            "total":         total,
            "status":        "sent_to_kitchen",
        }
        self.order_submitted = True
        # In production you'd persist this to a database or POS system
        return {"success": True, "order": order_record}

Now define the JSON tool schemas that Claude reads to understand how to call each function.

restaurant_agent.py (add to class)
    def _get_tool_definitions(self) -> list:
        """Return Anthropic-formatted tool definitions for all three tools."""
        return [
            {
                "name": "lookup_menu",
                "description": (
                    "Retrieve the restaurant menu. Call this whenever a customer asks "
                    "what's available, asks about prices, or needs to browse items. "
                    "Filter by category when possible to keep the response concise."
                ),
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "category": {
                            "type": "string",
                            "enum": ["all", "appetizers", "mains", "drinks", "desserts"],
                            "description": "Menu category to retrieve. Use 'all' for the full menu.",
                        }
                    },
                    "required": [],
                },
            },
            {
                "name": "validate_and_add_item",
                "description": (
                    "Validate a menu item by its ID and add it to the customer's order. "
                    "Always call lookup_menu first to get the correct item ID before calling this."
                ),
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "item_id": {
                            "type": "string",
                            "description": "The unique item ID from the menu (e.g. 'MAN001').",
                        },
                        "quantity": {
                            "type": "integer",
                            "description": "Number of this item to add. Must be 1 or greater.",
                            "minimum": 1,
                        },
                    },
                    "required": ["item_id", "quantity"],
                },
            },
            {
                "name": "submit_order",
                "description": (
                    "Submit the completed order to the kitchen. Only call this after the "
                    "customer has confirmed they are done ordering and you have added all items."
                ),
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "customer_name": {
                            "type": "string",
                            "description": "The customer's first name.",
                        },
                        "table_number": {
                            "type": "integer",
                            "description": "The table number for this order.",
                        },
                    },
                    "required": ["customer_name", "table_number"],
                },
            },
        ]
💡 Tool Description Quality Matters
Claude reads your tool descriptions literally. The more specific you are about when to call a tool, the fewer unnecessary API calls you'll see. I added explicit ordering instructions in the validate_and_add_item description so the model always looks up the menu first.

Step 3: Build the Agentic Loop with Prompt Engineering

This is the core of the agent. The agentic loop keeps running until Claude responds with a plain end_turn — meaning it's done reasoning and doesn't need any more tool calls. Here's what that looks like in code.

restaurant_agent.py (add to class)
    SYSTEM_PROMPT = """You are a friendly, efficient order-taking assistant for Marina Bay Grille, 
a waterfront restaurant in Naples, Florida. Your job is to help customers browse the menu, 
add items to their order, and confirm their final order.

Rules you must follow:
1. Always look up the menu before adding any item — never guess item IDs.
2. Confirm each item with the customer before calling validate_and_add_item.
3. Before submitting, read back the complete order and total, then ask for explicit confirmation.
4. Keep responses concise and warm. This is a dining experience, not a data entry form.
5. Ask for the customer's name and table number only when you are ready to submit.
"""

    def _process_tool_call(self, tool_name: str, tool_input: dict) -> str:
        """Route a tool call to the correct Python function and return JSON string result."""
        if tool_name == "lookup_menu":
            result = self.lookup_menu(**tool_input)
        elif tool_name == "validate_and_add_item":
            result = self.validate_and_add_item(**tool_input)
        elif tool_name == "submit_order":
            result = self.submit_order(**tool_input)
        else:
            result = {"error": f"Unknown tool: {tool_name}"}
        return json.dumps(result)

    def run(self, user_message: str) -> str:
        """
        Process one user turn through the full agentic loop.
        Returns Claude's final text response to the user.
        """
        # Append the new user message to the running history
        self.conversation_history.append({
            "role": "user",
            "content": user_message,
        })

        # Agentic loop — keeps running while Claude wants to use tools
        while True:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=1024,
                system=self.SYSTEM_PROMPT,
                tools=self._get_tool_definitions(),
                messages=self.conversation_history,
            )

            # Accumulate Claude's full response content into history
            self.conversation_history.append({
                "role": "assistant",
                "content": response.content,  # list of content blocks
            })

            # If Claude is done (no tool calls), return the text response
            if response.stop_reason == "end_turn":
                # Extract text from the content blocks
                for block in response.content:
                    if hasattr(block, "text"):
                        return block.text
                return ""  # Fallback — should not happen in normal flow

            # If Claude wants to use tools, process each tool call
            if response.stop_reason == "tool_use":
                tool_results = []
                for block in response.content:
                    if block.type == "tool_use":
                        print(f"  [agent] calling tool: {block.name}({block.input})")
                        result_str = self._process_tool_call(block.name, block.input)
                        tool_results.append({
                            "type":        "tool_result",
                            "tool_use_id": block.id,
                            "content":     result_str,
                        })

                # Append tool results as a user turn so Claude can read them
                self.conversation_history.append({
                    "role":    "user",
                    "content": tool_results,
                })
                # Loop continues — Claude will now process the tool results

The key insight here is that tool results go back in as a user role message. That's how the Anthropic API expects them — it's not obvious from the docs at first glance, so don't let that trip you up.

Step 4: Integrate Multi-Turn Conversation Handling

The last piece is a simple chat loop so you can actually talk to the agent in your terminal. Add this below the class definition.

restaurant_agent.py (add at bottom of file)
def main():
    print("=" * 55)
    print("  Marina Bay Grille — AI Order Assistant")
    print("  Powered by Claude  |  Type 'quit' to exit")
    print("=" * 55)

    agent = RestaurantOrderAgent()

    while True:
        try:
            user_input = input("\nYou: ").strip()
        except (KeyboardInterrupt, EOFError):
            print("\nGoodbye!")
            break

        if not user_input:
            continue
        if user_input.lower() in ("quit", "exit", "q"):
            print("Thanks for dining with us!")
            break

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

        # Stop the loop once the order has been successfully submitted
        if agent.order_submitted:
            print("\n[Order sent to kitchen. Starting new session.]\n")
            agent.reset_order()


if __name__ == "__main__":
    main()

Run it with python restaurant_agent.py and you'll see the full conversation in your terminal. Here's what a real session looks like.

example output
=======================================================
  Marina Bay Grille — AI Order Assistant
  Powered by Claude  |  Type 'quit' to exit
=======================================================

You: Hi, what do you have for mains tonight?

  [agent] calling tool: lookup_menu({'category': 'mains'})

Agent: Great choices tonight! Here are our main courses:

  • Grilled Snapper — $28.00
  • Lobster Pasta — $34.00
  • Filet Mignon 8oz — $46.00
  • Margherita Pizza — $18.00

Anything catch your eye, or would you like to hear about appetizers or drinks too?

You: I'll take the lobster pasta and a house wine please.

  [agent] calling tool: lookup_menu({'category': 'drinks'})
  [agent] calling tool: validate_and_add_item({'item_id': 'MAN002', 'quantity': 1})
  [agent] calling tool: validate_and_add_item({'item_id': 'DRK001', 'quantity': 1})

Agent: Perfect! I've added to your order:

  ✓ Lobster Pasta × 1 — $34.00
  ✓ House Wine (glass) × 1 — $9.00

Running total: $43.00. Would you like anything else, or are you ready to place the order?

You: That's everything, go ahead and send it.

Agent: Before I send this to the kitchen, let me confirm your order:

  • Lobster Pasta × 1 — $34.00
  • House Wine (glass) × 1 — $9.00
  Total: $43.00

Could I get your name and table number?

You: Sarah, table 7.

  [agent] calling tool: submit_order({'customer_name': 'Sarah', 'table_number': 7})

Agent: You're all set, Sarah! Your order has been sent to the kitchen.

  Order #ORD-007-2026
  Table 7 | Total: $43.00

Enjoy your Lobster Pasta and wine! 🍷

[Order sent to kitchen. Starting new session.]

How It Works: Agent Reasoning and Tool Use Flow

Claude doesn't execute your Python functions directly. Instead, when it decides a tool call is needed, it returns a structured tool_use block with the tool name and arguments it wants to pass. Your code reads that block, runs the actual function, and sends the result back.

The loop only exits when Claude returns stop_reason: "end_turn", which means it has enough information to write a final response. On a simple lookup this might be one cycle. For an order with multiple items it might loop four or five times before Claude has everything it needs.

The conversation_history list is the memory. Every user message, every assistant response, and every tool result gets appended there, so by the time you submit the order Claude has seen the entire session and can reason about what was ordered, what was confirmed, and what the total is.

Common Errors and Fixes

Error: anthropic.AuthenticationError: 401 {"type":"error","error":{"type":"authentication_error"}}

Your API key isn't being found. Check that you exported it correctly: run echo $ANTHROPIC_API_KEY in your terminal. If it's blank, re-run the export command. If you're on Windows use set ANTHROPIC_API_KEY=sk-ant-... instead.

Error: anthropic.BadRequestError: messages: roles must alternate between "user" and "assistant"

This happens when you accidentally append two user messages or two assistant messages in a row to conversation_history. The most common cause is calling agent.run() twice before the loop finishes. Make sure each call to run() completes fully before the next one starts, and that tool results are appended as a single user block, not multiple separate entries.

Error: KeyError or tool returns wrong result after adding items

Claude occasionally passes an item ID it invented rather than one from the menu. This is why the validate_and_add_item function checks whether the ID exists and returns a structured error. If you're seeing this frequently, tighten the system prompt: add a line like "You must call lookup_menu and use only IDs returned from that call. Do not construct item IDs yourself."

Next Steps: Deploy to Production and Add Payment Processing

This terminal demo is a solid foundation. Here's where to take it next.

  • Add a web interface. Wrap the agent in a FastAPI endpoint and connect it to a React or plain-HTML chat UI. Each POST to /chat passes the user message and gets back the agent's response.
  • Persist orders to a real database. Swap out the in-memory current_order list for a Postgres or Supabase table. You'll want order history for reporting, refunds, and kitchen display systems.
  • Add Stripe payment processing. Create a fourth tool called create_payment_link that calls the Stripe API with the order total and returns a checkout URL. Claude can drop that link into the conversation naturally.
  • Connect to your POS system. Toast, Square, and Lightspeed all have REST APIs. Replace the submit_order function with a real API call and your agent becomes a live front-of-house automation tool.

FAQ

How do I build a chatbot with Claude API in Python?

Install the anthropic package, initialize