All articles

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
  • anthropic Python SDK installed (pip install anthropic)
  • A terminal and a text editor β€” that's genuinely all you need
πŸ“¦ Full Source Code
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.

terminal
pip install anthropic python-dotenv

Create a .env file in your project root with your key:

.env
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.py
import 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.py
import 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"]
        }
    }
]
πŸ’‘ Why schemas matter so much
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.py
import 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_text

Step 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.py
import 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:

example_conversation_output.txt
=======================================================
  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 structure
reservation-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)
πŸ”‘ Reminder
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