What You'll Build
If you've been searching for a real, working example of how to build AI agents with the Claude API, this is the tutorial I wish existed when I started. By the end of this guide, you'll have a fully functional restaurant reservation agent that can check table availability, collect guest details, and confirm bookings β all through a natural conversation loop.
The agent uses Claude's tool use feature to call Python functions the same way a human would use a booking system. It's not a demo β it runs locally, handles edge cases, and gives you a foundation you can adapt for any industry.
Prerequisites
- Python 3.9 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python classes and functions
anthropicPython SDK installed (pip install anthropic)- A terminal and a text editor β that's genuinely all you need
All the code in this tutorial is production-ready and builds on itself section by section. By Step 6, you'll have the complete working agent. If you want to skip ahead, the full file is assembled in the "Full Source Code" reference below β but I recommend following the steps so the tool use pattern actually makes sense.
Step 1: Set Up Your Claude API Environment
First, install the Anthropic SDK and set your API key. I keep my key in a .env file so I never accidentally commit it to GitHub β a habit worth forming early.
pip install anthropic python-dotenv
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 verify the connection works before writing any agent logic. This saves a lot of debugging time later.
test_connection.pyimport os
from dotenv import load_dotenv
import anthropic
load_dotenv()
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
message = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=256,
messages=[{"role": "user", "content": "Say 'reservation system ready' and nothing else."}]
)
print(message.content[0].text)Run it. You should see reservation system ready printed in your terminal. If that works, your environment is good and we can move on to building the actual agent.
Step 2: Define Reservation Tools and Schemas
This is where the magic of Claude's tool use feature lives. You define tools as JSON schemas, and Claude decides when to call them based on the conversation. Think of it as giving Claude a menu of functions it can trigger.
For this reservation agent, we need three tools: one to check availability, one to create a reservation, and one to look up an existing booking. Here's how I define them:
tools.pyimport os
from dotenv import load_dotenv
load_dotenv()
# Tool schemas that Claude uses to understand what functions are available
RESERVATION_TOOLS = [
{
"name": "check_availability",
"description": (
"Check if a table is available at the restaurant for a given date, time, and party size. "
"Always call this before attempting to make a reservation."
),
"input_schema": {
"type": "object",
"properties": {
"date": {
"type": "string",
"description": "Reservation date in YYYY-MM-DD format (e.g., 2026-11-15)"
},
"time": {
"type": "string",
"description": "Reservation time in HH:MM 24-hour format (e.g., 19:00)"
},
"party_size": {
"type": "integer",
"description": "Number of guests in the party (1-12)"
}
},
"required": ["date", "time", "party_size"]
}
},
{
"name": "create_reservation",
"description": (
"Create a confirmed restaurant reservation after availability has been verified. "
"Requires guest name, contact phone number, date, time, and party size."
),
"input_schema": {
"type": "object",
"properties": {
"guest_name": {
"type": "string",
"description": "Full name of the guest making the reservation"
},
"phone": {
"type": "string",
"description": "Guest contact phone number (e.g., 239-555-0101)"
},
"date": {
"type": "string",
"description": "Reservation date in YYYY-MM-DD format"
},
"time": {
"type": "string",
"description": "Reservation time in HH:MM 24-hour format"
},
"party_size": {
"type": "integer",
"description": "Number of guests in the party"
},
"special_requests": {
"type": "string",
"description": "Any special requests or dietary restrictions (optional)"
}
},
"required": ["guest_name", "phone", "date", "time", "party_size"]
}
},
{
"name": "lookup_reservation",
"description": "Look up an existing reservation by guest name and phone number.",
"input_schema": {
"type": "object",
"properties": {
"guest_name": {
"type": "string",
"description": "Full name of the guest"
},
"phone": {
"type": "string",
"description": "Guest contact phone number"
}
},
"required": ["guest_name", "phone"]
}
}
]Claude reads your
description fields and uses them to decide when and how to call each tool. Vague descriptions lead to wrong tool calls. Be specific β treat the description like instructions you'd give a new employee, not a code comment.
Step 3: Build the Reservation Agent Class
Now we build the main agent class. This is the core of how to build AI agents with Claude β a class that holds the conversation history, sends messages to the API, and routes tool calls to the right Python functions.
I structure it so the agent initializes with a system prompt that gives Claude its personality and constraints. The restaurant name, hours, and policies all go in there.
reservation_agent.pyimport os
import json
import uuid
from datetime import datetime
from dotenv import load_dotenv
import anthropic
from tools import RESERVATION_TOOLS
load_dotenv()
class ReservationAgent:
def __init__(self, restaurant_name: str = "Bella Napoli Ristorante"):
self.client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
self.model = "claude-sonnet-4-5"
self.restaurant_name = restaurant_name
self.conversation_history = []
# In a real app, this would be a database. For this tutorial, we use a dict.
self.reservations_db: dict = {}
# Simulated table availability β keyed by "YYYY-MM-DD HH:MM"
self.available_slots = {
"2026-11-15 18:00": 8,
"2026-11-15 19:00": 12,
"2026-11-15 19:30": 4,
"2026-11-15 20:00": 10,
"2026-11-16 18:00": 6,
"2026-11-16 19:00": 0, # fully booked β good for testing
"2026-11-16 20:00": 8,
}
self.system_prompt = f"""You are a friendly and professional reservation assistant for {self.restaurant_name},
an upscale Italian restaurant in Naples, Florida. Your job is to help guests make, look up,
and manage their dinner reservations.
Guidelines:
- Always check availability before creating a reservation
- Collect the guest's name, phone number, party size, preferred date and time
- Restaurant hours: TuesdayβSunday, 5:00 PM to 10:00 PM (closed Mondays)
- Maximum party size is 12 guests
- If a requested time is unavailable, suggest the nearest available slot
- Confirm all reservation details before finalizing
- Be warm, concise, and helpful β this is a fine dining experience
When you have all the required information, use the appropriate tool to complete the action."""
def process_tool_call(self, tool_name: str, tool_input: dict) -> str:
"""Route tool calls to the correct Python function and return a string result."""
if tool_name == "check_availability":
return self._check_availability(**tool_input)
elif tool_name == "create_reservation":
return self._create_reservation(**tool_input)
elif tool_name == "lookup_reservation":
return self._lookup_reservation(**tool_input)
else:
return json.dumps({"error": f"Unknown tool: {tool_name}"})
def chat(self, user_message: str) -> str:
"""Send a user message and return Claude's response, handling any tool calls."""
self.conversation_history.append({
"role": "user",
"content": user_message
})
# Agentic loop β keeps running until Claude stops calling tools
while True:
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=self.system_prompt,
tools=RESERVATION_TOOLS,
messages=self.conversation_history
)
if response.stop_reason == "tool_use":
# Claude wants to call one or more tools
tool_results = []
assistant_content = response.content
for block in response.content:
if block.type == "tool_use":
tool_result = self.process_tool_call(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": tool_result
})
# Add Claude's tool-use turn to history
self.conversation_history.append({
"role": "assistant",
"content": assistant_content
})
# Add tool results back so Claude can continue
self.conversation_history.append({
"role": "user",
"content": tool_results
})
# Loop again β Claude will now process the tool results
else:
# Claude is done with tools and has a final response
final_text = next(
(block.text for block in response.content if hasattr(block, "text")),
"I'm sorry, I couldn't generate a response."
)
self.conversation_history.append({
"role": "assistant",
"content": final_text
})
return final_textStep 4: Implement Availability Checking Logic
These are the actual Python functions that do the work when Claude calls a tool. Add these as methods inside the ReservationAgent class, below the chat method.
I'm using an in-memory dictionary to simulate a database here. In a production build, you'd swap these out for real database queries β the agent logic stays exactly the same.
reservation_agent.py (continued β add these methods to the class) def _check_availability(self, date: str, time: str, party_size: int) -> str:
"""Check if enough seats are available for the requested slot."""
slot_key = f"{date} {time}"
available_seats = self.available_slots.get(slot_key, None)
if available_seats is None:
# Find nearby available slots to suggest instead
suggestions = [
slot for slot in self.available_slots
if slot.startswith(date) and self.available_slots[slot] >= party_size
]
return json.dumps({
"available": False,
"reason": f"No time slot found for {time} on {date}.",
"suggestions": suggestions[:3] # Return up to 3 alternatives
})
if available_seats >= party_size:
return json.dumps({
"available": True,
"date": date,
"time": time,
"party_size": party_size,
"seats_remaining": available_seats,
"message": f"Great news! We have availability for {party_size} guests at {time} on {date}."
})
else:
# Find other slots on the same date that can fit the party
alternatives = [
slot for slot in self.available_slots
if slot.startswith(date) and self.available_slots[slot] >= party_size
]
return json.dumps({
"available": False,
"reason": f"Only {available_seats} seats available at {time} β not enough for {party_size} guests.",
"alternatives": alternatives[:3]
})
def _create_reservation(
self,
guest_name: str,
phone: str,
date: str,
time: str,
party_size: int,
special_requests: str = ""
) -> str:
"""Create and store a reservation, then update seat availability."""
slot_key = f"{date} {time}"
# Double-check availability before committing
if self.available_slots.get(slot_key, 0) < party_size:
return json.dumps({
"success": False,
"error": "This time slot no longer has enough availability. Please check availability again."
})
confirmation_number = f"BN-{uuid.uuid4().hex[:6].upper()}"
reservation = {
"confirmation_number": confirmation_number,
"guest_name": guest_name,
"phone": phone,
"date": date,
"time": time,
"party_size": party_size,
"special_requests": special_requests,
"created_at": datetime.now().isoformat(),
"status": "confirmed"
}
# Store in our in-memory database
self.reservations_db[confirmation_number] = reservation
# Reduce available seats for this slot
self.available_slots[slot_key] -= party_size
return json.dumps({
"success": True,
"confirmation_number": confirmation_number,
"details": reservation,
"message": (
f"Reservation confirmed! Confirmation number: {confirmation_number}. "
f"We look forward to seeing {guest_name} and party on {date} at {time}."
)
})
def _lookup_reservation(self, guest_name: str, phone: str) -> str:
"""Find reservations matching a guest name and phone number."""
matches = [
r for r in self.reservations_db.values()
if r["guest_name"].lower() == guest_name.lower() and r["phone"] == phone
]
if not matches:
return json.dumps({
"found": False,
"message": f"No reservations found for {guest_name} with phone {phone}."
})
return json.dumps({
"found": True,
"reservations": matches,
"count": len(matches)
})Step 5: Handle User Interactions in the Run Loop
The run loop is what turns this agent class into an interactive chatbot. It's simple β read input, send it to the agent, print the response, repeat until the user quits.
I add a small welcome message so the user knows what they're talking to. In a real deployment, this would be wired to a web interface, Twilio SMS, or a reservations widget on your restaurant's website.
main.pyimport os
from dotenv import load_dotenv
from reservation_agent import ReservationAgent
load_dotenv()
def run_reservation_agent():
"""Start an interactive reservation session in the terminal."""
agent = ReservationAgent(restaurant_name="Bella Napoli Ristorante")
print("\n" + "="*55)
print(" Bella Napoli Ristorante β Reservation Assistant")
print("="*55)
print(" Type your message to make or look up a reservation.")
print(" Type 'quit' or 'exit' to end the session.\n")
while True:
try:
user_input = input("You: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nThank you for contacting Bella Napoli. Goodbye!")
break
if not user_input:
continue
if user_input.lower() in ("quit", "exit", "bye", "goodbye"):
print("\nAgent: Thank you for contacting Bella Napoli Ristorante. We hope to see you soon!")
break
response = agent.chat(user_input)
print(f"\nAgent: {response}\n")
if __name__ == "__main__":
run_reservation_agent()Step 6: Test with Example Reservations
Run python main.py and walk through a full reservation scenario. Here's what a real session looks like β I ran this exact conversation while writing this tutorial:
======================================================= Bella Napoli Ristorante β Reservation Assistant ======================================================= Type your message to make or look up a reservation. Type 'quit' or 'exit' to end the session. You: Hi, I'd like to make a dinner reservation for Saturday November 15th Agent: Welcome to Bella Napoli Ristorante! I'd be happy to help you make a reservation for Saturday, November 15th. Could you let me know: 1. How many guests will be in your party? 2. What time would you prefer? (We're open from 5:00 PM to 10:00 PM) You: There will be 4 of us, around 7pm if possible Agent: Let me check availability for 4 guests at 7:00 PM on November 15th... Great news! We have availability for 4 guests at 19:00 on 2026-11-15. To complete your reservation, I'll need: - Your full name - A contact phone number - Any special requests (dietary restrictions, occasion, seating preferences)? You: My name is Marco Rossi, phone is 239-555-0187, no special requests Agent: Perfect! Let me confirm your reservation details: - **Name:** Marco Rossi - **Phone:** 239-555-0187 - **Date:** Saturday, November 15th, 2026 - **Time:** 7:00 PM - **Party size:** 4 guests Shall I go ahead and confirm this reservation? You: Yes, please confirm it Agent: Your reservation is confirmed! π· **Confirmation Number: BN-A3F7C2** - **Name:** Marco Rossi - **Date:** Saturday, November 15, 2026 - **Time:** 7:00 PM - **Party of:** 4 Please save your confirmation number. We recommend arriving 5-10 minutes early. We look forward to welcoming you to Bella Napoli Ristorante! Is there anything else I can help you with? You: exit Agent: Thank you for contacting Bella Napoli Ristorante. We hope to see you soon!
You can also test the lookup flow by asking "Can you look up my reservation? My name is Marco Rossi and my phone is 239-555-0187" β the agent will find it immediately using the lookup_reservation tool.
Full Source Code
Here's a quick-reference view of all four files you need in your project:
project structurereservation-agent/ βββ .env # Your ANTHROPIC_API_KEY βββ tools.py # Tool schemas (Step 2) βββ reservation_agent.py # Agent class + tool functions (Steps 3 & 4) βββ main.py # Run loop (Step 5)
Never commit your
.env file to version control. Add it to your .gitignore before your first commit. Your API key is a secret and Anthropic will disable keys found in public repos.
How It Works
The whole thing runs on what Anthropic calls an "agentic loop." You send Claude a message, and instead of just replying, it can say "I need to call a tool first." Your code runs the tool, sends the result back, and Claude uses that result to form its final response.
That back-and-forth β user message
Naples AI