What You'll Build
If you've ever tried to automate a restaurant's order flow and ended up with a tangled mess of conditionals and brittle API calls, this tutorial is for you. You're going to build a production-ready multi-agent restaurant order system using Python and the Anthropic Claude API, where one orchestrator agent coordinates three specialized sub-agents to validate orders, check inventory, and send confirmations. By the end, you'll have a working system you can plug into a real restaurant workflow, a POS integration, or an online ordering interface.
Prerequisites
- Python 3.10 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python functions and dictionaries
anthropicSDK installed:pip install anthropic- A
.envfile or environment variable set forANTHROPIC_API_KEY
Full Source Code
The code below is broken into four logical sections across the steps. Step 1 sets up your client. Step 2 defines the tools. Step 3 builds the orchestrator. Step 4 creates the sub-agents. Step 5 wires it all together into a working run loop. Follow along in order and you'll have a running system by the end.
Step 1: Set Up Your Claude API Client and Environment
Start by creating a file called restaurant_agent.py. The first thing you need is a properly configured Anthropic client and a simple in-memory inventory store to simulate a real database.
In a production system you'd swap the inventory dictionary for a database call, but for this tutorial the dictionary keeps things readable and focused on the agent logic.
restaurant_agent.pyimport os
import json
import anthropic
# Initialize the Anthropic client using the ANTHROPIC_API_KEY env variable
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"
# Simulated inventory database — swap this for a real DB call in production
INVENTORY = {
"burger": {"quantity": 10, "price": 12.99},
"fries": {"quantity": 25, "price": 3.99},
"salad": {"quantity": 8, "price": 9.49},
"soda": {"quantity": 50, "price": 2.49},
"pasta": {"quantity": 5, "price": 14.99},
"pizza": {"quantity": 0, "price": 16.99}, # Out of stock example
}
# Order storage — in production this would be a database table
ORDERS = {}
ORDER_COUNTER = 1000Notice that pizza is intentionally set to zero quantity. This lets the inventory agent demonstrate a real rejection scenario when we test the system later.
Step 2: Define Tool Functions for Order Processing
Claude's tool use feature lets the model call your Python functions directly. You define the tools in two places: as actual Python functions that do the work, and as JSON schemas that tell Claude what the tool does and what arguments it expects.
You need three tools here. validate_order checks that the order is structurally correct. check_inventory confirms items are in stock. send_confirmation finalizes and records the order.
def validate_order(order_items: list, customer_name: str, table_number: int) -> dict:
"""Validate that an order has required fields and reasonable values."""
errors = []
if not customer_name or not customer_name.strip():
errors.append("Customer name is required.")
if table_number < 1 or table_number > 50:
errors.append(f"Table number {table_number} is out of range (1-50).")
if not order_items or len(order_items) == 0:
errors.append("Order must contain at least one item.")
for item in order_items:
if "name" not in item or "quantity" not in item:
errors.append(f"Each item must have 'name' and 'quantity' fields. Got: {item}")
elif item["quantity"] < 1:
errors.append(f"Quantity for {item['name']} must be at least 1.")
if errors:
return {"valid": False, "errors": errors}
return {"valid": True, "message": f"Order for {customer_name} at table {table_number} passed validation."}
def check_inventory(order_items: list) -> dict:
"""Check if all ordered items are available in sufficient quantity."""
unavailable = []
available = []
total_cost = 0.0
for item in order_items:
name = item["name"].lower()
qty = item["quantity"]
if name not in INVENTORY:
unavailable.append({"item": name, "reason": "Item not on menu"})
continue
stock = INVENTORY[name]
if stock["quantity"] < qty:
unavailable.append({
"item": name,
"reason": f"Only {stock['quantity']} in stock, {qty} requested"
})
else:
line_total = stock["price"] * qty
total_cost += line_total
available.append({"item": name, "quantity": qty, "line_total": round(line_total, 2)})
if unavailable:
return {
"in_stock": False,
"unavailable_items": unavailable,
"available_items": available,
"message": "Some items are not available."
}
return {
"in_stock": True,
"available_items": available,
"total_cost": round(total_cost, 2),
"message": f"All items are in stock. Total: ${total_cost:.2f}"
}
def send_confirmation(
customer_name: str,
table_number: int,
order_items: list,
total_cost: float
) -> dict:
"""Finalize the order, deduct inventory, and return a confirmation number."""
global ORDER_COUNTER
# Deduct items from inventory
for item in order_items:
name = item["name"].lower()
qty = item["quantity"]
if name in INVENTORY:
INVENTORY[name]["quantity"] -= qty
# Generate a confirmation number and store the order
ORDER_COUNTER += 1
confirmation_number = f"ORD-{ORDER_COUNTER}"
ORDERS[confirmation_number] = {
"customer_name": customer_name,
"table_number": table_number,
"items": order_items,
"total_cost": total_cost,
"status": "confirmed"
}
return {
"success": True,
"confirmation_number": confirmation_number,
"message": (
f"Order {confirmation_number} confirmed for {customer_name} "
f"at table {table_number}. Total: ${total_cost:.2f}"
)
}Now define the JSON schemas that Claude reads to understand how to call each function. These schemas are passed directly to the API on every request.
restaurant_agent.py (continued)# Tool schemas passed to Claude so it knows when and how to call each function
TOOLS = [
{
"name": "validate_order",
"description": (
"Validates a restaurant order to ensure it has all required fields, "
"a valid table number (1-50), a customer name, and at least one item "
"with a positive quantity."
),
"input_schema": {
"type": "object",
"properties": {
"order_items": {
"type": "array",
"description": "List of items being ordered.",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Item name"},
"quantity": {"type": "integer", "description": "Number of this item"}
},
"required": ["name", "quantity"]
}
},
"customer_name": {
"type": "string",
"description": "Name of the customer placing the order."
},
"table_number": {
"type": "integer",
"description": "Restaurant table number (1-50)."
}
},
"required": ["order_items", "customer_name", "table_number"]
}
},
{
"name": "check_inventory",
"description": (
"Checks whether the requested items are available in inventory "
"and calculates the total order cost."
),
"input_schema": {
"type": "object",
"properties": {
"order_items": {
"type": "array",
"description": "List of items to check in inventory.",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"quantity": {"type": "integer"}
},
"required": ["name", "quantity"]
}
}
},
"required": ["order_items"]
}
},
{
"name": "send_confirmation",
"description": (
"Finalizes the order, deducts items from inventory, and returns "
"a confirmation number. Only call this after validation and inventory "
"checks have passed."
),
"input_schema": {
"type": "object",
"properties": {
"customer_name": {"type": "string", "description": "Customer name"},
"table_number": {"type": "integer", "description": "Table number"},
"order_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"quantity": {"type": "integer"}
},
"required": ["name", "quantity"]
}
},
"total_cost": {
"type": "number",
"description": "Total cost from the inventory check result."
}
},
"required": ["customer_name", "table_number", "order_items", "total_cost"]
}
}
]Step 3: Create the Orchestrator Agent
The orchestrator is the brain of the system. It receives the raw order request, decides which tools to call and in what sequence, and coordinates the results from each sub-agent. The system prompt is where you define its decision-making rules.
A good system prompt is specific about order of operations. You don't want Claude skipping validation and jumping straight to confirmation.
restaurant_agent.py (continued)ORCHESTRATOR_SYSTEM_PROMPT = """You are the Order Orchestrator for a busy restaurant.
Your job is to process customer orders by coordinating three specialized tools in the correct sequence.
REQUIRED WORKFLOW — follow this exact order every time:
1. Call validate_order first to check the order structure and table number.
2. If validation fails, stop and report the errors clearly to the customer.
3. If validation passes, call check_inventory to confirm all items are available.
4. If inventory check fails, inform the customer which items are unavailable and suggest they reorder without those items.
5. If inventory check passes, call send_confirmation to finalize the order.
6. Return a clear, friendly summary to the customer including their confirmation number and total.
Rules:
- Never skip validation.
- Never call send_confirmation before check_inventory passes.
- Be concise but friendly in your final response.
- If any tool returns an error, explain it in plain English — no technical jargon.
"""
def run_orchestrator(order_request: str) -> str:
"""
Run the orchestrator agent for a single order request.
Returns the final text response after all tool calls complete.
"""
messages = [{"role": "user", "content": order_request}]
print(f"\n{'='*60}")
print(f"ORDER REQUEST: {order_request}")
print(f"{'='*60}")
# Agentic loop — keeps running until Claude stops calling tools
while True:
response = client.messages.create(
model=MODEL,
max_tokens=4096,
system=ORCHESTRATOR_SYSTEM_PROMPT,
tools=TOOLS,
messages=messages
)
# Add Claude's response to the message history
messages.append({"role": "assistant", "content": response.content})
# If Claude is done (no more tool calls), return the final text
if response.stop_reason == "end_turn":
final_text = next(
(block.text for block in response.content if hasattr(block, "text")),
"Order processing complete."
)
print(f"\nFINAL RESPONSE:\n{final_text}")
return final_text
# If Claude wants to use tools, process each tool call
if response.stop_reason == "tool_use":
tool_results = process_tool_calls(response.content)
# Append all tool results back to the conversation
messages.append({"role": "user", "content": tool_results})
else:
# Unexpected stop reason — break to avoid infinite loop
print(f"Unexpected stop reason: {response.stop_reason}")
break
return "An unexpected error occurred while processing the order."Step 4: Create Specialized Sub-Agents (Order Validator, Inventory Manager, Confirmation Handler)
In this architecture, the sub-agents are the tool execution layer. When Claude calls a tool, you dispatch the call to the matching Python function and package the result in the format the API expects. This function handles all three tools in one place.
restaurant_agent.py (continued)def process_tool_calls(content_blocks: list) -> list:
"""
Process all tool_use blocks from Claude's response.
Returns a list of tool_result blocks to send back to the API.
"""
tool_results = []
for block in content_blocks:
if block.type != "tool_use":
continue
tool_name = block.name
tool_input = block.input
tool_use_id = block.id
print(f"\n TOOL CALL: {tool_name}")
print(f" INPUT: {json.dumps(tool_input, indent=2)}")
# Dispatch to the correct sub-agent function
if tool_name == "validate_order":
result = validate_order(
order_items = tool_input["order_items"],
customer_name = tool_input["customer_name"],
table_number = tool_input["table_number"]
)
elif tool_name == "check_inventory":
result = check_inventory(
order_items = tool_input["order_items"]
)
elif tool_name == "send_confirmation":
result = send_confirmation(
customer_name = tool_input["customer_name"],
table_number = tool_input["table_number"],
order_items = tool_input["order_items"],
total_cost = tool_input["total_cost"]
)
else:
result = {"error": f"Unknown tool: {tool_name}"}
print(f" RESULT: {json.dumps(result, indent=2)}")
# Package the result in the format the Anthropic API expects
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": json.dumps(result)
})
return tool_resultsvalidate_order for a full schema validator, replace check_inventory with a live database query, and hook send_confirmation into a POS system, all without touching the orchestrator logic or the run loop.
Step 5: Implement the Multi-Agent Run Loop
Now add the entry point that ties everything together and runs a few realistic test scenarios. This shows the system handling a clean order, an order with an out-of-stock item, and an order with a validation error.
restaurant_agent.py (continued)def main():
print("Naples AI — Restaurant Order System Demo")
print("Powered by Claude API Multi-Agent Automation\n")
# Scenario 1: Valid order that should complete successfully
run_orchestrator(
"Hi, I'd like to place an order for Sarah at table 7. "
"We want 2 burgers, 2 fries, and 3 sodas please."
)
# Scenario 2: Order containing an out-of-stock item (pizza)
run_orchestrator(
"Table 12, customer name is Mike. "
"Order: 1 pizza, 1 salad, 2 sodas."
)
# Scenario 3: Invalid order — table number out of range
run_orchestrator(
"Order for James at table 99. Just 1 pasta please."
)
if __name__ == "__main__":
main()Example Conversation Flow and Output
When you run python restaurant_agent.py, here's what the actual terminal output looks like for the three scenarios:
Naples AI — Restaurant Order System Demo
Powered by Claude API Multi-Agent Automation
============================================================
ORDER REQUEST: Hi, I'd like to place an order for Sarah at table 7. We want 2 burgers, 2 fries, and 3 sodas please.
============================================================
TOOL CALL: validate_order
INPUT: {
"order_items": [
{"name": "burger", "quantity": 2},
{"name": "fries", "quantity": 2},
{"name": "soda", "quantity": 3}
],
"customer_name": "Sarah",
"table_number": 7
}
RESULT: {
"valid": true,
"message": "Order for Sarah at table 7 passed validation."
}
TOOL CALL: check_inventory
INPUT: {
"order_items": [
{"name": "burger", "quantity": 2},
{"name": "fries", "quantity": 2},
{"name": "soda", "quantity": 3}
]
}
RESULT: {
"in_stock": true,
"available_items": [
{"item": "burger", "quantity": 2, "line_total": 25.98},
{"item": "fries", "quantity": 2, "line_total": 7.98},
{"item": "soda", "quantity": 3, "line_total": 7.47}
],
"total_cost": 41.43,
"message": "All items are in stock. Total: $41.43"
}
TOOL CALL: send_confirmation
INPUT: {
"customer_name": "Sarah",
"table_number": 7,
"order_items": [
{"name": "burger", "quantity": 2},
{"name": "fries", "quantity": 2},
{"name": "soda", "quantity": 3}
],
"total_cost": 41.43
}
RESULT: {
"success": true,
"confirmation_number": "ORD-1001",
"message": "Order ORD-1001 confirmed for Sarah at table 7. Total: $41.43"
}
FINAL RESPONSE:
Great news, Sarah! Your order has been confirmed.
Confirmation #: ORD-1001
Table: 7
Items: 2 Burgers, 2 Fries, 3 Sodas
Total: $41.43
Your food will be out shortly. Enjoy your meal!
============================================================
ORDER REQUEST: Table 12, customer name is Mike. Order: 1 pizza, 1 salad, 2 sodas.
============================================================
TOOL CALL: validate_order
...validation passes...
TOOL CALL: check_inventory
RESULT: {
"in_stock": false,
"unavailable_items": [
{"item": "pizza", "reason": "Only 0 in stock, 1 requested"}
],
"available_items": [
{"item": "salad", "quantity": 1, "line_total": 9.49},
{"item": "soda", "quantity": 2, "line_total": 4.98}
],
"message": "Some items are not available."
}
FINAL RESPONSE:
Sorry, Mike! Unfortunately we're out of pizza right now.
Your salad and sodas are available (subtotal: $14.47), but we can't complete
the order with pizza included. Would you like to reorder without the pizza?
============================================================
ORDER REQUEST: Order for James at table 99. Just 1 pasta please.
============================================================
TOOL CALL: validate_order
RESULT: {
"valid": false,
"errors": ["Table number 99 is out of range (1-50)."]
}
FINAL RESPONSE:
Hi James!
Naples AI