What You'll Build
If you've been searching for a real how to build AI agents tutorial that goes beyond toy examples, this is it. You're going to build a production-ready multi-agent system in Python that schedules healthcare appointments, checks provider availability, and fires off confirmation messages — all coordinated by Claude API.
By the end, you'll have three specialized agents (Scheduler, Availability Checker, and Confirmation Agent) that hand off work to each other through an orchestration loop. This is the same pattern we use at Naples AI when building healthcare automation for Southwest Florida clinics.
Prerequisites
- Python 3.10 or higher installed
- An Anthropic API key (get one here)
- Basic familiarity with Python classes and functions
anthropicSDK installed:pip install anthropicpython-dotenvfor environment variables:pip install python-dotenv- No healthcare database required — we simulate one in-memory for this tutorial
Step 1: Set Up Your Claude API Client and Environment
First, let's get the environment wired up. Create a .env file in your project root and add your API key. Never hardcode credentials — you'll regret it the first time you push to GitHub.
ANTHROPIC_API_KEY=your_api_key_here
Now create your main project file and set up the base client. This is the foundation every agent in the system will share.
healthcare_scheduler.pyimport os
import json
import datetime
from dotenv import load_dotenv
import anthropic
load_dotenv()
# Initialize the shared Anthropic client once — all agents use this instance
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"
# In-memory simulated database — replace with your real DB in production
PROVIDER_SCHEDULES = {
"dr_martinez": {
"name": "Dr. Elena Martinez",
"specialty": "Family Medicine",
"available_slots": [
"2026-08-10 09:00", "2026-08-10 10:00", "2026-08-10 14:00",
"2026-08-11 11:00", "2026-08-11 15:00", "2026-08-12 09:30"
]
},
"dr_chen": {
"name": "Dr. Kevin Chen",
"specialty": "Cardiology",
"available_slots": [
"2026-08-10 13:00", "2026-08-11 09:00", "2026-08-12 14:00"
]
}
}
APPOINTMENTS = {} # Stores confirmed bookings keyed by appointment_id
PATIENT_RECORDS = {
"P001": {"name": "Maria Santos", "email": "[email protected]", "phone": "239-555-0142"},
"P002": {"name": "James Kowalski", "email": "[email protected]", "phone": "239-555-0198"}
}
print("✅ Environment loaded. Claude client ready.")
print(f" Model: {MODEL}")
print(f" Providers loaded: {len(PROVIDER_SCHEDULES)}")
print(f" Patients loaded: {len(PATIENT_RECORDS)}")
Run this file with python healthcare_scheduler.py and you should see the confirmation output. If you get an AuthenticationError, double-check your .env file is in the same directory as your script.
Step 2: Define Your Agent Roles
Each agent gets its own system prompt that locks it into a specific role. This is what makes multi-agent systems so powerful — you're not asking one model to do everything, you're routing tasks to specialists. Think of it like a real medical office with a receptionist, a scheduler, and a coordinator.
Add these role definitions to your file. The system prompts are intentionally tight and focused — a confused agent produces confused results.
healthcare_scheduler.py (continued)AGENT_ROLES = {
"availability_checker": {
"name": "Availability Checker",
"system_prompt": (
"You are a medical scheduling availability agent. Your only job is to check "
"whether a specific provider has open appointment slots on a requested date or date range. "
"Always use the check_availability tool before responding. "
"Return results in a clear, structured format listing available times. "
"Never book appointments — only report availability."
)
},
"scheduler": {
"name": "Appointment Scheduler",
"system_prompt": (
"You are a medical appointment scheduling agent. Your job is to book appointments "
"for patients with the correct provider at the correct time. "
"Always verify the slot is available using check_availability before calling schedule_appointment. "
"Confirm the patient ID exists before booking. "
"If a slot is unavailable, suggest the next available time instead of failing silently."
)
},
"confirmation_agent": {
"name": "Confirmation Agent",
"system_prompt": (
"You are a patient communication agent responsible for sending appointment confirmations. "
"After a successful booking, use the send_confirmation tool to notify the patient. "
"Include the provider name, appointment date and time, and any preparation instructions "
"relevant to the specialty. Keep messages friendly and clear."
)
}
}
print("\n✅ Agent roles defined:")
for role_key, role_data in AGENT_ROLES.items():
print(f" - {role_data['name']}")
Step 3: Create Tool Definitions for Calendar and Patient Database Access
Tools are how Claude agents interact with your real systems. Each tool definition tells Claude exactly what parameters it needs, what types they are, and what the tool does. Getting this right is the difference between an agent that works and one that hallucinates arguments.
Here are the three core tools — check_availability, schedule_appointment, and send_confirmation — plus the Python functions that actually execute them when Claude calls them.
TOOL_DEFINITIONS = [
{
"name": "check_availability",
"description": (
"Check available appointment slots for a specific healthcare provider. "
"Returns a list of open time slots or an empty list if none are available."
),
"input_schema": {
"type": "object",
"properties": {
"provider_id": {
"type": "string",
"description": "The provider's system ID, e.g. 'dr_martinez' or 'dr_chen'"
},
"date_filter": {
"type": "string",
"description": "Optional date string in YYYY-MM-DD format to filter results. Leave empty for all slots."
}
},
"required": ["provider_id"]
}
},
{
"name": "schedule_appointment",
"description": (
"Book an appointment slot for a patient with a provider. "
"The slot must be currently available. Returns a confirmation ID on success."
),
"input_schema": {
"type": "object",
"properties": {
"provider_id": {
"type": "string",
"description": "The provider's system ID"
},
"patient_id": {
"type": "string",
"description": "The patient's system ID, e.g. 'P001'"
},
"appointment_slot": {
"type": "string",
"description": "The exact slot string from check_availability, e.g. '2026-08-10 09:00'"
}
},
"required": ["provider_id", "patient_id", "appointment_slot"]
}
},
{
"name": "send_confirmation",
"description": (
"Send an appointment confirmation to a patient via email and SMS. "
"Requires a valid appointment ID returned from schedule_appointment."
),
"input_schema": {
"type": "object",
"properties": {
"appointment_id": {
"type": "string",
"description": "The confirmation ID returned from schedule_appointment"
},
"preparation_notes": {
"type": "string",
"description": "Any special preparation instructions for the patient"
}
},
"required": ["appointment_id"]
}
}
]
def execute_tool(tool_name: str, tool_input: dict) -> dict:
"""Routes tool calls from Claude to the correct Python function."""
if tool_name == "check_availability":
return tool_check_availability(tool_input)
elif tool_name == "schedule_appointment":
return tool_schedule_appointment(tool_input)
elif tool_name == "send_confirmation":
return tool_send_confirmation(tool_input)
else:
return {"error": f"Unknown tool: {tool_name}"}
def tool_check_availability(params: dict) -> dict:
provider_id = params.get("provider_id")
date_filter = params.get("date_filter", "")
if provider_id not in PROVIDER_SCHEDULES:
return {"error": f"Provider '{provider_id}' not found in system."}
provider = PROVIDER_SCHEDULES[provider_id]
slots = provider["available_slots"]
# Filter by date if provided
if date_filter:
slots = [s for s in slots if s.startswith(date_filter)]
return {
"provider_name": provider["name"],
"specialty": provider["specialty"],
"available_slots": slots,
"total_available": len(slots)
}
def tool_schedule_appointment(params: dict) -> dict:
provider_id = params.get("provider_id")
patient_id = params.get("patient_id")
slot = params.get("appointment_slot")
if provider_id not in PROVIDER_SCHEDULES:
return {"error": f"Provider '{provider_id}' not found."}
if patient_id not in PATIENT_RECORDS:
return {"error": f"Patient '{patient_id}' not found."}
provider = PROVIDER_SCHEDULES[provider_id]
if slot not in provider["available_slots"]:
return {"error": f"Slot '{slot}' is no longer available. Please check availability again."}
# Remove booked slot from availability
provider["available_slots"].remove(slot)
# Generate a confirmation ID and store the appointment
appt_id = f"APT-{len(APPOINTMENTS) + 1001}"
APPOINTMENTS[appt_id] = {
"appointment_id": appt_id,
"patient_id": patient_id,
"patient_name": PATIENT_RECORDS[patient_id]["name"],
"provider_id": provider_id,
"provider_name": provider["name"],
"specialty": provider["specialty"],
"slot": slot,
"booked_at": datetime.datetime.now().isoformat(),
"confirmation_sent": False
}
return {
"success": True,
"appointment_id": appt_id,
"patient_name": PATIENT_RECORDS[patient_id]["name"],
"provider_name": provider["name"],
"slot": slot
}
def tool_send_confirmation(params: dict) -> dict:
appt_id = params.get("appointment_id")
prep_notes = params.get("preparation_notes", "No special preparation required.")
if appt_id not in APPOINTMENTS:
return {"error": f"Appointment ID '{appt_id}' not found."}
appt = APPOINTMENTS[appt_id]
patient = PATIENT_RECORDS[appt["patient_id"]]
# In production, this would call your email/SMS API
confirmation_message = (
f"Dear {appt['patient_name']}, your appointment with {appt['provider_name']} "
f"({appt['specialty']}) is confirmed for {appt['slot']}. "
f"Preparation notes: {prep_notes}"
)
APPOINTMENTS[appt_id]["confirmation_sent"] = True
return {
"success": True,
"appointment_id": appt_id,
"email_sent_to": patient["email"],
"sms_sent_to": patient["phone"],
"confirmation_message": confirmation_message
}
print("\n✅ Tool definitions and handlers ready.")
input_schema descriptions aren't just documentation — Claude reads them to decide what values to pass. The more specific your descriptions, the more accurately Claude will call your tools. Vague descriptions lead to wrong arguments.
Step 4: Build the Orchestration Loop for Multi-Agent Coordination
This is the core of the whole system. The orchestrator decides which agent handles each task, runs that agent's tool loop until it's done, then passes the result to the next agent. It's a clean handoff pattern — each agent gets a focused job and returns a structured result.
The run_agent method handles the agentic loop: it sends a message, checks if Claude wants to call a tool, executes that tool, feeds the result back, and keeps going until Claude returns a final text response. This loop is what makes it truly agentic.
class HealthcareSchedulingOrchestrator:
"""
Coordinates multiple Claude agents to handle the full appointment scheduling workflow.
Each agent is isolated with its own role and only sees the context it needs.
"""
def __init__(self):
self.client = client
self.model = MODEL
self.tools = TOOL_DEFINITIONS
self.conversation_log = []
def run_agent(self, role_key: str, user_message: str) -> str:
"""
Runs a single agent through its full agentic tool-use loop.
Returns the agent's final text response when it stops calling tools.
"""
role = AGENT_ROLES[role_key]
messages = [{"role": "user", "content": user_message}]
print(f"\n{'='*55}")
print(f" AGENT: {role['name']}")
print(f" TASK: {user_message[:80]}...")
print(f"{'='*55}")
# Agentic loop — keeps running until Claude stops requesting tool calls
while True:
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=role["system_prompt"],
tools=self.tools,
messages=messages
)
# If Claude is done, return its final text response
if response.stop_reason == "end_turn":
final_text = ""
for block in response.content:
if hasattr(block, "text"):
final_text = block.text
print(f"\n ✅ Agent completed. Response: {final_text[:120]}...")
return final_text
# Claude wants to call a tool — process all tool_use blocks
if response.stop_reason == "tool_use":
# Add Claude's response to the message history
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f"\n 🔧 Tool call: {block.name}")
print(f" Input: {json.dumps(block.input, indent=2)}")
# Execute the tool and get the result
result = execute_tool(block.name, block.input)
print(f" Result: {json.dumps(result, indent=2)}")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
# Feed tool results back to Claude
messages.append({"role": "user", "content": tool_results})
def schedule_appointment_workflow(
self,
patient_id: str,
provider_id: str,
preferred_date: str,
reason: str
) -> dict:
"""
Full orchestrated workflow: check availability → book → confirm.
Returns a summary dict with the final appointment details.
"""
print(f"\n{'#'*55}")
print(f" ORCHESTRATOR: Starting scheduling workflow")
print(f" Patient: {patient_id} | Provider: {provider_id}")
print(f" Date: {preferred_date} | Reason: {reason}")
print(f"{'#'*55}")
# --- AGENT 1: Check Availability ---
availability_result = self.run_agent(
role_key="availability_checker",
user_message=(
f"Check available appointment slots for provider '{provider_id}' "
f"on or around {preferred_date}. Return all available times."
)
)
# --- AGENT 2: Schedule the Appointment ---
scheduling_result = self.run_agent(
role_key="scheduler",
user_message=(
f"Book an appointment for patient '{patient_id}' with provider '{provider_id}'. "
f"Availability check result: {availability_result}. "
f"Preferred date: {preferred_date}. Appointment reason: {reason}. "
f"Pick the earliest available slot on or after the preferred date."
)
)
# Extract appointment ID from APPOINTMENTS dict (most recently added)
appt_id = None
for aid, appt in APPOINTMENTS.items():
if appt["patient_id"] == patient_id and not appt["confirmation_sent"]:
appt_id = aid
if not appt_id:
return {
"success": False,
"error": "Scheduling agent did not create an appointment.",
"details": scheduling_result
}
# --- AGENT 3: Send Confirmation ---
specialty = APPOINTMENTS[appt_id]["specialty"]
prep_notes = self._get_prep_notes(specialty, reason)
confirmation_result = self.run_agent(
role_key="confirmation_agent",
user_message=(
f"Send a confirmation for appointment ID '{appt_id}'. "
f"Include these preparation notes: {prep_notes}. "
f"The patient's appointment reason is: {reason}."
)
)
# Build final summary
appt = APPOINTMENTS[appt_id]
return {
"success": True,
"appointment_id": appt_id,
"patient_name": appt["patient_name"],
"provider_name": appt["provider_name"],
"specialty": appt["specialty"],
"scheduled_slot": appt["slot"],
"confirmation_sent": appt["confirmation_sent"],
"scheduling_notes": scheduling_result,
"confirmation_notes": confirmation_result
}
def _get_prep_notes(self, specialty: str, reason: str) -> str:
"""Returns basic preparation instructions based on medical specialty."""
notes_map = {
"Family Medicine": "Please arrive 15 minutes early with your insurance card and a photo ID.",
"Cardiology": (
"Please avoid caffeine for 24 hours before your appointment. "
"Bring a list of all current medications."
)
}
return notes_map.get(specialty, "Please arrive 10 minutes early.")
Step 5: Implement Error Handling and Fallback Logic
Real healthcare systems fail in interesting ways — slots disappear between check and book, patient IDs come in with wrong formats, network calls time out. You need graceful fallbacks, not stack traces. Here's the runner function with full error handling wrapped around the orchestrator.
healthcare_scheduler.py (continued)def run_scheduling_demo():
"""
Entry point that demonstrates the full multi-agent workflow
with proper error handling and a clean summary output.
"""
orchestrator = HealthcareSchedulingOrchestrator()
# Test Case 1: Successful scheduling
print("\n" + "🏥 " * 20)
print("TEST CASE 1: Standard appointment scheduling")
print("🏥 " * 20)
try:
result = orchestrator.schedule_appointment_workflow(
patient_id="P001",
provider_id="dr_martinez",
preferred_date="2026-08-10",
reason="Annual physical exam and blood pressure follow-up"
)
if result["success"]:
print("\n" + "=" * 55)
print(" ✅ APPOINTMENT SCHEDULED SUCCESSFULLY")
print("=" * 55)
print(f" Appointment ID: {result['appointment_id']}")
print(f" Patient: {result['patient_name']}")
print(f" Provider: {result['provider_name']} ({result['specialty']})")
print(f" Scheduled Time: {result['scheduled_slot']}")