If you've been searching for a real Claude API tutorial that goes beyond "hello world," you're in the right place. Most examples online show you a single agent talking to a model — but production systems need multiple agents working together, handing off tasks, and making decisions in a loop. That's exactly what we're building here.
This tutorial walks you through building a multi-agent restaurant order system using the Anthropic Python SDK. You'll have working code by the end, not just concepts.
What You'll Build
You're going to build a three-agent system that handles restaurant orders end-to-end. An order agent takes the customer's request, an inventory agent checks if the items are in stock, and a payment agent verifies the transaction before confirming the order.
All three agents coordinate through an orchestration loop using Claude's tool_use feature. The output is a confirmed order with inventory validation and payment status — fully automated, no human in the loop required.
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 install anthropic python-dotenvready to run- A terminal and a code editor (VS Code works great)
Step 1: Set Up Your Claude API Credentials and Environment
First, let's get your environment configured. Create a project folder, set up a virtual environment, and store your API key safely in a .env file so it never ends up in your code.
Run these commands in your terminal before writing any Python:
terminalmkdir restaurant-agent-system cd restaurant-agent-system python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate pip install anthropic python-dotenv
Now create a .env file in your project root with your key:
ANTHROPIC_API_KEY=sk-ant-your-key-here
Now create your main file and set up the Anthropic client. This is the foundation everything else builds on:
restaurant_agents.pyimport os
import json
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv()
# Initialize the Anthropic client — it auto-reads ANTHROPIC_API_KEY from env
client = Anthropic()
# We'll use claude-sonnet-4-6 for all three agents in this system
MODEL = "claude-sonnet-4-6"
print("Client initialized. Ready to build agents.")
Run this file with python restaurant_agents.py to confirm your key is working. You should see the print statement without any auth errors. If you get an AuthenticationError, double-check the key in your .env file.
Step 2: Define Your Tool Schemas (Order Processing, Inventory Check, Payment)
Tools are how Claude takes action in the real world. You define them as JSON schemas, and Claude decides when to call them based on the conversation. Think of them as functions Claude can choose to run.
We need three tools: one to process an order, one to check inventory, and one to verify payment. Add this to your file right after the client setup:
restaurant_agents.py (continued)# Tool definitions tell Claude what actions are available and what inputs they need
ORDER_TOOLS = [
{
"name": "process_order",
"description": "Processes a customer's food order and returns an order ID with itemized details.",
"input_schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {"type": "string"},
"description": "List of menu items the customer wants to order"
},
"table_number": {
"type": "integer",
"description": "The table number placing the order"
},
"special_instructions": {
"type": "string",
"description": "Any special dietary notes or customizations"
}
},
"required": ["items", "table_number"]
}
}
]
INVENTORY_TOOLS = [
{
"name": "check_inventory",
"description": "Checks whether menu items are currently in stock in the kitchen inventory.",
"input_schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {"type": "string"},
"description": "List of menu items to check availability for"
}
},
"required": ["items"]
}
}
]
PAYMENT_TOOLS = [
{
"name": "verify_payment",
"description": "Verifies that a payment method is valid and the customer has sufficient funds.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique order ID returned by process_order"
},
"payment_method": {
"type": "string",
"enum": ["credit_card", "debit_card", "cash", "mobile_pay"],
"description": "The payment method the customer is using"
},
"amount": {
"type": "number",
"description": "Total dollar amount to charge"
}
},
"required": ["order_id", "payment_method", "amount"]
}
}
]
description fields in your tool schemas aren't just documentation — Claude reads them to decide when and how to use each tool. Be specific about what each tool does and what its inputs mean. Vague descriptions lead to wrong tool calls.
Step 3: Build the Order Agent with tool_use
Now we build the first agent. The order agent receives the customer's request and uses the process_order tool to create a structured order. This is where the Claude API's agentic loop pattern comes in.
We also need the mock backend functions that simulate what a real database or POS system would do. In production, these would be API calls to your actual systems:
restaurant_agents.py (continued)# Simulated backend functions — replace these with real API calls in production
def process_order(items: list, table_number: int, special_instructions: str = "") -> dict:
"""Simulates creating an order in the POS system."""
order_id = f"ORD-{table_number}-{abs(hash(str(items))) % 10000:04d}"
price_map = {
"margherita pizza": 14.99,
"caesar salad": 9.99,
"garlic bread": 5.99,
"tiramisu": 7.99,
"sparkling water": 3.99,
"grilled salmon": 22.99,
"pasta carbonara": 16.99,
}
# Calculate total using known prices, default to 12.99 for unlisted items
total = sum(price_map.get(item.lower(), 12.99) for item in items)
return {
"order_id": order_id,
"table_number": table_number,
"items": items,
"special_instructions": special_instructions,
"total_amount": round(total, 2),
"status": "pending"
}
def run_order_agent(customer_request: str) -> dict:
"""Order agent that parses customer request and creates a structured order."""
messages = [
{
"role": "user",
"content": customer_request
}
]
print("\n[ORDER AGENT] Processing customer request...")
# First API call — Claude reads the request and decides to call process_order
response = client.messages.create(
model=MODEL,
max_tokens=1024,
system="You are a restaurant order agent. Parse the customer's request and use the process_order tool to create their order. Always extract the table number, items, and any special instructions from the message.",
tools=ORDER_TOOLS,
messages=messages
)
order_result = None
# Handle the agentic loop — keep going until Claude stops calling tools
while response.stop_reason == "tool_use":
tool_use_block = next(b for b in response.content if b.type == "tool_use")
tool_name = tool_use_block.name
tool_input = tool_use_block.input
print(f"[ORDER AGENT] Calling tool: {tool_name}")
print(f"[ORDER AGENT] Tool input: {json.dumps(tool_input, indent=2)}")
# Execute the tool and capture the result
if tool_name == "process_order":
tool_result = process_order(**tool_input)
order_result = tool_result
# Send the tool result back to Claude to continue the conversation
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": json.dumps(tool_result)
}
]
})
# Get Claude's next response after seeing the tool result
response = client.messages.create(
model=MODEL,
max_tokens=1024,
system="You are a restaurant order agent. Parse the customer's request and use the process_order tool to create their order.",
tools=ORDER_TOOLS,
messages=messages
)
print(f"[ORDER AGENT] Order created: {order_result}")
return order_result
Step 4: Build the Inventory Agent and Payment Verification Agent
The inventory agent takes the order output and checks whether the kitchen has everything in stock. The payment agent then takes the confirmed order and validates the payment method. Both follow the same agentic loop pattern as the order agent.
restaurant_agents.py (continued)def check_inventory(items: list) -> dict:
"""Simulates checking a kitchen inventory database."""
# Simulated stock levels — in production this hits your inventory API
stock = {
"margherita pizza": True,
"caesar salad": True,
"garlic bread": True,
"tiramisu": False, # Out of stock for demo purposes
"sparkling water": True,
"grilled salmon": True,
"pasta carbonara": True,
}
availability = {}
all_available = True
for item in items:
is_available = stock.get(item.lower(), True)
availability[item] = is_available
if not is_available:
all_available = False
return {
"availability": availability,
"all_items_available": all_available,
"out_of_stock": [item for item, avail in availability.items() if not avail]
}
def run_inventory_agent(order: dict) -> dict:
"""Inventory agent that checks stock for all items in the order."""
items_list = ", ".join(order["items"])
messages = [
{
"role": "user",
"content": f"Check inventory for this order. Items needed: {items_list}. Order ID: {order['order_id']}"
}
]
print("\n[INVENTORY AGENT] Checking stock levels...")
response = client.messages.create(
model=MODEL,
max_tokens=1024,
system="You are a kitchen inventory agent. Use the check_inventory tool to verify all items in an order are available. Report clearly which items are in stock and which are not.",
tools=INVENTORY_TOOLS,
messages=messages
)
inventory_result = None
while response.stop_reason == "tool_use":
tool_use_block = next(b for b in response.content if b.type == "tool_use")
tool_name = tool_use_block.name
tool_input = tool_use_block.input
print(f"[INVENTORY AGENT] Calling tool: {tool_name}")
if tool_name == "check_inventory":
tool_result = check_inventory(**tool_input)
inventory_result = tool_result
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": json.dumps(tool_result)
}
]
})
response = client.messages.create(
model=MODEL,
max_tokens=1024,
system="You are a kitchen inventory agent. Use the check_inventory tool to verify all items in an order are available.",
tools=INVENTORY_TOOLS,
messages=messages
)
print(f"[INVENTORY AGENT] Inventory result: {inventory_result}")
return inventory_result
def verify_payment(order_id: str, payment_method: str, amount: float) -> dict:
"""Simulates payment verification with a payment processor."""
# Simulate payment gateway response — always approves for demo
return {
"order_id": order_id,
"payment_method": payment_method,
"amount": amount,
"approved": True,
"transaction_id": f"TXN-{abs(hash(order_id)) % 100000:05d}",
"status": "confirmed"
}
def run_payment_agent(order: dict, payment_method: str) -> dict:
"""Payment agent that verifies payment for a confirmed order."""
messages = [
{
"role": "user",
"content": f"Verify payment for order {order['order_id']}. Total amount: ${order['total_amount']}. Payment method: {payment_method}."
}
]
print("\n[PAYMENT AGENT] Verifying payment...")
response = client.messages.create(
model=MODEL,
max_tokens=1024,
system="You are a payment verification agent. Use the verify_payment tool to confirm that the customer's payment is valid and the order can proceed.",
tools=PAYMENT_TOOLS,
messages=messages
)
payment_result = None
while response.stop_reason == "tool_use":
tool_use_block = next(b for b in response.content if b.type == "tool_use")
tool_name = tool_use_block.name
tool_input = tool_use_block.input
print(f"[PAYMENT AGENT] Calling tool: {tool_name}")
if tool_name == "verify_payment":
tool_result = verify_payment(**tool_input)
payment_result = tool_result
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": json.dumps(tool_result)
}
]
})
response = client.messages.create(
model=MODEL,
max_tokens=1024,
system="You are a payment verification agent. Use the verify_payment tool to confirm payment.",
tools=PAYMENT_TOOLS,
messages=messages
)
print(f"[PAYMENT AGENT] Payment result: {payment_result}")
return payment_result
Step 5: Create the Orchestration Loop
This is where everything comes together. The orchestrator calls each agent in sequence, passes outputs between them, and handles edge cases like out-of-stock items. It's the brain that decides what happens next based on each agent's response.
restaurant_agents.py (continued)def orchestrate_restaurant_order(customer_request: str, payment_method: str = "credit_card") -> dict:
"""
Main orchestration loop that coordinates all three agents.
Runs: Order Agent → Inventory Agent → Payment Agent
"""
print("=" * 60)
print("RESTAURANT ORDER SYSTEM — MULTI-AGENT ORCHESTRATION")
print("=" * 60)
print(f"Customer request: {customer_request}")
# Stage 1: Order agent creates the structured order
order = run_order_agent(customer_request)
if not order:
return {"success": False, "error": "Order agent failed to parse customer request"}
# Stage 2: Inventory agent checks all items are available
inventory = run_inventory_agent(order)
if not inventory:
return {"success": False, "error": "Inventory agent failed to check stock"}
# If items are out of stock, halt the order and notify
if not inventory["all_items_available"]:
out_of_stock = inventory["out_of_stock"]
print(f"\n[ORCHESTRATOR] ❌ Order halted — items out of stock: {out_of_stock}")
return {
"success": False,
"error": "Some items are out of stock",
"out_of_stock_items": out_of_stock,
"order_id": order["order_id"],
"message": f"Sorry, we're currently out of: {', '.join(out_of_stock)}. Please update your order."
}
print("\n[ORCHESTRATOR] ✅ All items in stock — proceeding to payment")
# Stage 3: Payment agent verifies the transaction
payment = run_payment_agent(order, payment_method)
if not payment or not payment.get("approved"):
return {"success": False, "error": "Payment verification failed", "order_id": order["order_id"]}
# All three stages passed — build the final confirmed order summary
final_result = {
"success": True,
"order_id": order["order_id"],
"table_number": order["table_number"],
"items": order["items"],
"special_instructions": order.get("special_instructions", ""),
"total_amount": order["total_amount"],
"payment_method": payment["payment_method"],
"transaction_id": payment["transaction_id"],
"status": "confirmed",
"message": f"Order {order['order_id']} confirmed! Your food is being prepared."
}
print("\n" + "=" * 60)
print("ORDER CONFIRMED ✅")
print(json.dumps(final_result, indent=2))
print("=" * 60)
return final_result
# Entry point — run this to test the full system
if __name__ == "__main__":
# Test Case 1: Normal order that goes through successfully
result = orchestrate_restaurant_order(
customer_request="Hi, table 7 here. We'd like a margherita pizza, two caesar salads, garlic bread, and two sparkling waters. No onions on the salads please.",
payment_method="credit_card"
)
print("\n\nFINAL RESULT:")
print(json.dumps(result, indent=2))
print("\n\n" + "=" * 60)
print("TEST 2: Order with out-of-stock item")
print("=" * 60)
# Test Case 2: Order containing an out-of-stock item (tiramisu)
result2 = orchestrate_restaurant_order(
customer_request="Table 3 wants pasta carbonara, tiramisu for dessert, and sparkling water.",
payment_method="mobile_pay"
)
Run the full file now with python restaurant_agents.py. You should see all three agents activate in sequence, with tool calls logged to your terminal in real time.
Example Output: What You'll See in Your Terminal
Here's what the actual output looks like when you run the system. The first test passes through all three agents. The second one gets caught by the inventory agent because tiramisu is out of stock.
terminal output============================================================
RESTAURANT ORDER SYSTEM — MULTI-AGENT ORCHESTRATION
============================================================
Customer request: Hi, table 7 here. We'd like a margherita pizza, two caesar salads, garlic bread, and two sparkling waters. No onions on the salads please.
[ORDER AGENT] Processing customer request...
[ORDER AGENT] Calling tool: process_order
[ORDER AGENT] Tool input: {
"items": ["margherita pizza", "caesar salad", "caesar salad", "garlic bread", "sparkling water", "sparkling water"],
"table_number": 7,
"special_instructions": "No onions on the salads"
}
[ORDER AGENT] Order created: {'order_id': 'ORD-7-3421', 'table_number': 7, 'items': [...], 'total_amount': 53.94, 'status': 'pending'}
[INVENTORY AGENT] Checking stock levels