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
anthropicSDK installed:pip install anthropic- A terminal and a text editor (VS Code works great)
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.
terminalmkdir 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.
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 definitionstools = [
{
"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"]
}
}
]
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 loopdef 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 toolsMENU = {
"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 pointdef 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.
=== 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