← Back to Blog

What You'll Build

By the end of this tutorial, you'll have a fully working AI restaurant order agent that takes customer orders in plain English, validates them against a real menu, and returns structured order summaries with pricing. The agent uses Claude's tool use feature to call custom functions — the same pattern we use at Naples AI when building real automation for local restaurants and service businesses. You'll be done in about 30 minutes.

📦 Full Source Code
The complete, working code for this project is built up step by step in the sections below. Every snippet is copy-pasteable and syntactically correct. If you want to skip straight to a specific part, jump to Step 3 for the main agent class or Step 4 for the run loop.

Prerequisites

  • Python 3.9 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with Python classes and dictionaries
  • anthropic Python SDK installed (pip install anthropic)
  • A terminal and a text editor or IDE

Step 1: Set Up Your Claude API Credentials and Project Structure

First, let's get the project folder and environment ready. I like to keep API keys in a .env file so I never accidentally commit them to GitHub — a habit that's saved me more than once.

Create a project folder called restaurant_agent and set up the following file structure:

terminal
mkdir restaurant_agent
cd restaurant_agent
pip install anthropic python-dotenv
touch main.py menu.py agent.py .env

Now add your API key to the .env file:

.env
ANTHROPIC_API_KEY=your_api_key_here

That's all the setup you need. The Anthropic SDK handles everything else — no extra HTTP libraries, no manual auth headers.

Step 2: Define Restaurant Order Tools and Menu Data

This is where the agent gets its "brain" for the restaurant context. We define a menu as a Python dictionary and write two tool functions: one to look up menu items and one to validate and build an order. Claude will decide when to call these tools based on what the customer says.

menu.py
from typing import Optional

# Menu data — swap this out with a database call in production
MENU = {
    "margherita pizza": {"price": 14.99, "category": "pizza", "available": True},
    "pepperoni pizza": {"price": 16.99, "category": "pizza", "available": True},
    "bbq chicken pizza": {"price": 17.99, "category": "pizza", "available": True},
    "caesar salad": {"price": 9.99, "category": "salad", "available": True},
    "house salad": {"price": 8.99, "category": "salad", "available": True},
    "garlic bread": {"price": 4.99, "category": "side", "available": True},
    "mozzarella sticks": {"price": 7.99, "category": "appetizer", "available": True},
    "soda": {"price": 2.99, "category": "drink", "available": True},
    "water": {"price": 0.00, "category": "drink", "available": True},
    "tiramisu": {"price": 6.99, "category": "dessert", "available": False},
}


def lookup_menu_item(item_name: str) -> dict:
    """Look up a single menu item by name. Returns item details or a not-found message."""
    normalized = item_name.lower().strip()
    if normalized in MENU:
        item = MENU[normalized]
        return {
            "found": True,
            "name": item_name,
            "price": item["price"],
            "category": item["category"],
            "available": item["available"],
        }
    # Try partial match so "pepperoni" still finds "pepperoni pizza"
    for menu_key, details in MENU.items():
        if normalized in menu_key:
            return {
                "found": True,
                "name": menu_key,
                "price": details["price"],
                "category": details["category"],
                "available": details["available"],
            }
    return {"found": False, "name": item_name, "message": f"'{item_name}' is not on our menu."}


def validate_and_build_order(items: list[dict]) -> dict:
    """
    Validate a list of order items and return a structured order summary.
    Each item dict should have 'name' and 'quantity' keys.
    """
    validated_items = []
    unavailable_items = []
    not_found_items = []
    subtotal = 0.0

    for item in items:
        name = item.get("name", "").lower().strip()
        quantity = int(item.get("quantity", 1))
        lookup = lookup_menu_item(name)

        if not lookup["found"]:
            not_found_items.append(item.get("name"))
            continue

        if not lookup["available"]:
            unavailable_items.append(lookup["name"])
            continue

        line_total = lookup["price"] * quantity
        subtotal += line_total
        validated_items.append({
            "name": lookup["name"],
            "quantity": quantity,
            "unit_price": lookup["price"],
            "line_total": round(line_total, 2),
        })

    tax = round(subtotal * 0.07, 2)  # 7% Florida sales tax
    total = round(subtotal + tax, 2)

    return {
        "success": len(validated_items) > 0,
        "order_items": validated_items,
        "unavailable_items": unavailable_items,
        "not_found_items": not_found_items,
        "subtotal": round(subtotal, 2),
        "tax": tax,
        "total": total,
        "item_count": len(validated_items),
    }

Notice the partial match logic in lookup_menu_item — that's what lets a customer say "pepperoni" and still get the right result. Real customers don't type exact menu names, and your agent needs to handle that gracefully.

Step 3: Create the Main AI Agent Class with Tool Use

Here's the core of the project. The RestaurantOrderAgent class wires together Claude and the tool functions we just wrote. The key pattern is the tool use loop: Claude returns a tool_use block, we run the actual Python function, then send the result back so Claude can form a response. This back-and-forth is what makes it an agent instead of just a chatbot.

agent.py
import json
import os
from dotenv import load_dotenv
import anthropic
from menu import lookup_menu_item, validate_and_build_order

load_dotenv()


class RestaurantOrderAgent:
    def __init__(self):
        self.client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
        self.model = "claude-sonnet-4-6"
        self.conversation_history = []

        # Tool definitions tell Claude what functions it can call and how
        self.tools = [
            {
                "name": "lookup_menu_item",
                "description": (
                    "Look up a specific item on the restaurant menu. "
                    "Use this to check if an item exists and get its price before adding it to an order."
                ),
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "item_name": {
                            "type": "string",
                            "description": "The name of the menu item to look up, e.g. 'pepperoni pizza'",
                        }
                    },
                    "required": ["item_name"],
                },
            },
            {
                "name": "validate_and_build_order",
                "description": (
                    "Validate a complete list of order items and build a structured order summary "
                    "with pricing and tax. Call this when the customer is ready to finalize their order."
                ),
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "items": {
                            "type": "array",
                            "description": "List of items the customer wants to order",
                            "items": {
                                "type": "object",
                                "properties": {
                                    "name": {"type": "string", "description": "Menu item name"},
                                    "quantity": {"type": "integer", "description": "How many of this item"},
                                },
                                "required": ["name", "quantity"],
                            },
                        }
                    },
                    "required": ["items"],
                },
            },
        ]

        self.system_prompt = """You are a friendly and efficient order-taking assistant for Naples Pizza & Grill, 
a local restaurant in Naples, Florida. Help customers browse the menu and place orders.

When a customer wants to order something:
1. Use lookup_menu_item to verify items exist and check availability
2. Once the customer confirms their full order, call validate_and_build_order with all items
3. Present the order summary clearly, including itemized pricing and the total with tax
4. If an item isn't available or isn't on the menu, suggest an alternative

Keep responses warm, concise, and helpful. Don't list the full menu unless asked."""

    def _process_tool_call(self, tool_name: str, tool_input: dict) -> str:
        """Execute the requested tool and return its result as a JSON string."""
        if tool_name == "lookup_menu_item":
            result = lookup_menu_item(tool_input["item_name"])
        elif tool_name == "validate_and_build_order":
            result = validate_and_build_order(tool_input["items"])
        else:
            result = {"error": f"Unknown tool: {tool_name}"}
        return json.dumps(result)

    def chat(self, user_message: str) -> str:
        """Send a message and run the tool use loop until Claude gives a final text response."""
        # Append the new user message to the running conversation history
        self.conversation_history.append({"role": "user", "content": user_message})

        # Keep looping as long as Claude wants to use tools
        while True:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=1024,
                system=self.system_prompt,
                tools=self.tools,
                messages=self.conversation_history,
            )

            # If Claude is done thinking and just wants to talk, return the text
            if response.stop_reason == "end_turn":
                assistant_text = response.content[0].text
                self.conversation_history.append({"role": "assistant", "content": response.content})
                return assistant_text

            # Claude wants to use one or more tools — process each one
            if response.stop_reason == "tool_use":
                # Add Claude's tool-use request to conversation history
                self.conversation_history.append({"role": "assistant", "content": response.content})

                # Build tool results for every tool call in this response
                tool_results = []
                for block in response.content:
                    if block.type == "tool_use":
                        tool_output = self._process_tool_call(block.name, block.input)
                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": tool_output,
                        })

                # Send all tool results back in a single user message
                self.conversation_history.append({"role": "user", "content": tool_results})
                # Loop again so Claude can respond with the next step or final answer

    def reset_conversation(self):
        """Clear conversation history to start a fresh session."""
        self.conversation_history = []
⚡ Why the while loop?
Claude might need to call multiple tools in sequence — for example, looking up two items before building the order. The while True loop keeps running until stop_reason is "end_turn", which means Claude has finished all its tool calls and is ready to respond to the customer.

Step 4: Implement the Agent Run Loop and User Interaction

Now let's wire everything together in a main.py file that runs an interactive command-line session. This is the entry point that ties the agent to a real user conversation, and it's also where you'd swap in a web API or messaging webhook in production.

main.py
import os
from dotenv import load_dotenv
from agent import RestaurantOrderAgent

load_dotenv()


def print_separator():
    print("\n" + "─" * 60 + "\n")


def run_demo_session(agent: RestaurantOrderAgent):
    """
    Run a scripted demo to show the agent working end-to-end.
    Replace this with input() calls for a live interactive session.
    """
    demo_conversation = [
        "Hi! What pizzas do you have?",
        "I'll take 2 pepperoni pizzas and a caesar salad please.",
        "Can I also add a tiramisu?",
        "Okay, skip the tiramisu. Can I get a soda instead? That's my full order.",
    ]

    print("=== Naples Pizza & Grill — AI Order Agent Demo ===")
    print_separator()

    for user_message in demo_conversation:
        print(f"Customer: {user_message}")
        response = agent.chat(user_message)
        print(f"\nAgent: {response}")
        print_separator()


def run_interactive_session(agent: RestaurantOrderAgent):
    """Run a live interactive session where a real user types their order."""
    print("=== Naples Pizza & Grill — AI Order Agent ===")
    print("Type 'quit' to exit or 'reset' to start a new order.\n")

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

        if not user_input:
            continue
        if user_input.lower() == "quit":
            print("Thanks for visiting Naples Pizza & Grill! Goodbye.")
            break
        if user_input.lower() == "reset":
            agent.reset_conversation()
            print("Order cleared. Starting fresh!\n")
            continue

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


if __name__ == "__main__":
    agent = RestaurantOrderAgent()

    # Toggle between demo and interactive mode
    DEMO_MODE = True

    if DEMO_MODE:
        run_demo_session(agent)
    else:
        run_interactive_session(agent)

To run the demo, just execute python main.py from your project folder. You should see the full conversation play out in your terminal within a few seconds.

Step 5: Add Order Validation and Error Handling

The code above handles happy-path orders, but real customers do things like order items that are out of stock, misspell names, or ask for things you don't serve. Our validate_and_build_order function already catches most of this, but let's add a wrapper to handle API errors gracefully too.

Update the chat method in agent.py to wrap the API call in a try/except:

agent.py (updated chat method)
import json
import os
import anthropic
from dotenv import load_dotenv
from menu import lookup_menu_item, validate_and_build_order

load_dotenv()


class RestaurantOrderAgent:
    def __init__(self):
        self.client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
        self.model = "claude-sonnet-4-6"
        self.conversation_history = []
        self.tools = [
            {
                "name": "lookup_menu_item",
                "description": (
                    "Look up a specific item on the restaurant menu. "
                    "Use this to check if an item exists and get its price before adding it to an order."
                ),
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "item_name": {
                            "type": "string",
                            "description": "The name of the menu item to look up, e.g. 'pepperoni pizza'",
                        }
                    },
                    "required": ["item_name"],
                },
            },
            {
                "name": "validate_and_build_order",
                "description": (
                    "Validate a complete list of order items and build a structured order summary "
                    "with pricing and tax. Call this when the customer is ready to finalize their order."
                ),
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "items": {
                            "type": "array",
                            "description": "List of items the customer wants to order",
                            "items": {
                                "type": "object",
                                "properties": {
                                    "name": {"type": "string", "description": "Menu item name"},
                                    "quantity": {"type": "integer", "description": "How many of this item"},
                                },
                                "required": ["name", "quantity"],
                            },
                        }
                    },
                    "required": ["items"],
                },
            },
        ]
        self.system_prompt = """You are a friendly and efficient order-taking assistant for Naples Pizza & Grill, 
a local restaurant in Naples, Florida. Help customers browse the menu and place orders.

When a customer wants to order something:
1. Use lookup_menu_item to verify items exist and check availability
2. Once the customer confirms their full order, call validate_and_build_order with all items
3. Present the order summary clearly, including itemized pricing and the total with tax
4. If an item isn't available or isn't on the menu, suggest an alternative

Keep responses warm, concise, and helpful. Don't list the full menu unless asked."""

    def _process_tool_call(self, tool_name: str, tool_input: dict) -> str:
        """Execute the requested tool and return its result as a JSON string."""
        if tool_name == "lookup_menu_item":
            result = lookup_menu_item(tool_input["item_name"])
        elif tool_name == "validate_and_build_order":
            result = validate_and_build_order(tool_input["items"])
        else:
            result = {"error": f"Unknown tool: {tool_name}"}
        return json.dumps(result)

    def chat(self, user_message: str) -> str:
        """Send a message and run the tool use loop until Claude gives a final text response."""
        self.conversation_history.append({"role": "user", "content": user_message})

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

                if response.stop_reason == "end_turn":
                    assistant_text = response.content[0].text
                    self.conversation_history.append({"role": "assistant", "content": response.content})
                    return assistant_text

                if response.stop_reason == "tool_use":
                    self.conversation_history.append({"role": "assistant", "content": response.content})
                    tool_results = []
                    for block in response.content:
                        if block.type == "tool_use":
                            tool_output = self._process_tool_call(block.name, block.input)
                            tool_results.append({
                                "type": "tool_result",
                                "tool_use_id": block.id,
                                "content": tool_output,
                            })
                    self.conversation_history.append({"role": "user", "content": tool_results})

        except anthropic.AuthenticationError:
            return "Authentication failed. Please check your ANTHROPIC_API_KEY in the .env file."
        except anthropic.RateLimitError:
            return "We're experiencing high demand right now. Please try again in a moment."
        except anthropic.APIConnectionError:
            return "Couldn't connect to the ordering system. Please check your internet connection."
        except Exception as e:
            # Log unexpected errors but don't expose internals to the customer
            print(f"[ERROR] Unexpected error in agent.chat(): {e}")
            return "Something went wrong on our end. Please try again or speak with a staff member."

    def reset_conversation(self):
        """Clear conversation history to start a fresh session."""
        self.conversation_history = []

Example Order Processing Output and Transcript

Here's what the actual terminal output looks like when you run python main.py with DEMO_MODE = True:

sample output
=== Naples Pizza & Grill — AI Order Agent Demo ===

────────────────────────────────────────────────────────────

Customer: Hi! What pizzas do you have?

Agent: Hey there! Welcome to Naples Pizza & Grill! 🍕

We've got three great pizzas on the menu:

• **Margherita Pizza** — $14.99
• **Pepperoni Pizza** — $16.99
• **BBQ Chicken Pizza** — $17.