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.
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
anthropicPython 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:
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:
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.pyfrom 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.
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 = []
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.
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:
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:
=== 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.