What You'll Build
You're going to build a production-ready AI healthcare chatbot that handles patient inquiries, retrieves patient data, and books medical appointments — all in under 100 lines of Python using the Claude API. By the end, you'll have a working agent with conversation memory, tool-calling logic, and a clean appointment booking flow that you can drop into any healthcare application. This is the same kind of Claude API integration we build for medical practices here in Southwest Florida at Naples AI.
Prerequisites
- Python 3.10 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python classes and functions
anthropicSDK installed:pip install anthropic- A
.envfile or environment variable set forANTHROPIC_API_KEY
Full Source Code
The full chatbot is assembled across Steps 1–5 below. I've broken it into logical chunks so you understand what each piece does before you put it all together. If you want to skip straight to running it, paste all five snippets into a single file called healthcare_chatbot.py in order.
Step 1: Set Up Your Claude API Environment
First, let's get the Anthropic SDK wired up with your API key and confirm your model is set correctly. I always start here because a misconfigured client causes silent failures that are annoying to debug later.
healthcare_chatbot.py — Part 1: Environment Setup
import os
import json
from datetime import datetime, timedelta
from anthropic import Anthropic
# Load your API key from the environment
# Set this with: export ANTHROPIC_API_KEY="sk-ant-..."
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
# Using claude-sonnet-4-6 for fast, accurate tool-calling
MODEL_NAME = "claude-sonnet-4-6"
# System prompt that defines the chatbot's healthcare persona
SYSTEM_PROMPT = """You are a helpful medical office assistant for Naples Family Health Clinic.
You help patients check their appointment status, look up basic clinic information,
and book new appointments. Always be professional, empathetic, and concise.
Never provide medical diagnoses. If a patient describes an emergency, instruct
them to call 911 or go to the nearest emergency room immediately."""
The system prompt is doing real work here — it constrains Claude's behavior to a healthcare context and includes a safety instruction for emergencies. Don't skip that last line. In a real deployment, you want those guardrails in place from day one.
Step 2: Define Patient Query Tools and Functions
Claude's tool-calling feature lets the model decide when to call your Python functions instead of just generating text. We're defining three tools: one to look up patient info, one to check available appointment slots, and one to book an appointment. This is where the real automation happens.
healthcare_chatbot.py — Part 2: Tool Definitions and Patient Query Functions
# --- Tool definitions passed to Claude ---
# Claude reads these schemas and decides when to call each function.
TOOLS = [
{
"name": "get_patient_info",
"description": "Retrieve basic patient information by patient ID, including name, date of birth, and upcoming appointments.",
"input_schema": {
"type": "object",
"properties": {
"patient_id": {
"type": "string",
"description": "The unique patient identifier, e.g. 'P1042'"
}
},
"required": ["patient_id"]
}
},
{
"name": "get_available_slots",
"description": "Get available appointment slots for a given date and doctor.",
"input_schema": {
"type": "object",
"properties": {
"date": {
"type": "string",
"description": "The appointment date in YYYY-MM-DD format"
},
"doctor_name": {
"type": "string",
"description": "The name of the doctor, e.g. 'Dr. Rivera'"
}
},
"required": ["date", "doctor_name"]
}
},
{
"name": "book_appointment",
"description": "Book a medical appointment for a patient.",
"input_schema": {
"type": "object",
"properties": {
"patient_id": {
"type": "string",
"description": "The unique patient identifier"
},
"doctor_name": {
"type": "string",
"description": "The name of the doctor"
},
"date": {
"type": "string",
"description": "Appointment date in YYYY-MM-DD format"
},
"time_slot": {
"type": "string",
"description": "Time slot in HH:MM format, e.g. '10:30'"
},
"reason": {
"type": "string",
"description": "Brief reason for the appointment"
}
},
"required": ["patient_id", "doctor_name", "date", "time_slot", "reason"]
}
}
]
# --- Python functions that back each tool ---
# In production, these would hit your real EHR or scheduling database.
def get_patient_info(patient_id: str) -> dict:
"""Simulate a patient record lookup."""
mock_patients = {
"P1042": {
"name": "Maria Gonzalez",
"dob": "1985-03-14",
"insurance": "BlueCross PPO",
"upcoming_appointments": [
{"date": "2026-06-10", "time": "09:00", "doctor": "Dr. Rivera", "reason": "Annual checkup"}
]
},
"P2078": {
"name": "James Thornton",
"dob": "1972-11-29",
"insurance": "Aetna HMO",
"upcoming_appointments": []
}
}
if patient_id in mock_patients:
return {"success": True, "patient": mock_patients[patient_id]}
return {"success": False, "error": f"No patient found with ID {patient_id}"}
def get_available_slots(date: str, doctor_name: str) -> dict:
"""Simulate fetching open appointment slots from a scheduling system."""
# In production, query your EHR's scheduling API here
available_times = ["09:00", "10:30", "13:00", "14:30", "16:00"]
return {
"success": True,
"doctor": doctor_name,
"date": date,
"available_slots": available_times
}
def book_appointment(patient_id: str, doctor_name: str, date: str,
time_slot: str, reason: str) -> dict:
"""Simulate writing a new appointment to the scheduling database."""
# In production, POST to your EHR or practice management system API
confirmation_number = f"APT-{patient_id}-{date.replace('-', '')}-{time_slot.replace(':', '')}"
return {
"success": True,
"confirmation_number": confirmation_number,
"patient_id": patient_id,
"doctor": doctor_name,
"date": date,
"time": time_slot,
"reason": reason,
"message": f"Appointment confirmed with {doctor_name} on {date} at {time_slot}."
}
def process_tool_call(tool_name: str, tool_input: dict) -> str:
"""Route a tool call from Claude to the correct Python function."""
if tool_name == "get_patient_info":
result = get_patient_info(**tool_input)
elif tool_name == "get_available_slots":
result = get_available_slots(**tool_input)
elif tool_name == "book_appointment":
result = book_appointment(**tool_input)
else:
result = {"error": f"Unknown tool: {tool_name}"}
# Return JSON string — Claude receives this as tool result content
return json.dumps(result)
Notice that process_tool_call acts as a simple router. Claude tells us which tool it wants to call and what inputs to use — we just execute the right function and hand the result back. This pattern scales cleanly when you add more tools later.
Step 3: Build the Main Chatbot Agent Class
Now we wrap everything into a clean HealthcareChatbot class. This is the core of the Claude API integration — it manages the conversation loop, handles tool calls, and knows when to stop and return a final response to the user.
class HealthcareChatbot:
"""
AI healthcare chatbot agent powered by Claude.
Handles patient queries, appointment lookups, and booking
using Claude's tool-calling (function-calling) capability.
"""
def __init__(self):
self.client = client
self.model = MODEL_NAME
self.tools = TOOLS
self.system_prompt = SYSTEM_PROMPT
self.conversation_history = [] # Persistent memory across turns
def _run_agent_loop(self, user_message: str) -> str:
"""
Core agentic loop: send message to Claude, handle any tool calls,
then return the final text response to the caller.
"""
# Append the new user message to conversation history
self.conversation_history.append({
"role": "user",
"content": user_message
})
# Keep looping until Claude returns a final text response
while True:
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=self.system_prompt,
tools=self.tools,
messages=self.conversation_history
)
# Claude wants to call a tool — execute it and loop back
if response.stop_reason == "tool_use":
# Add Claude's tool-call message to history
self.conversation_history.append({
"role": "assistant",
"content": response.content
})
# Process every tool call in this response
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool_result = process_tool_call(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": tool_result
})
# Feed tool results back to Claude as the next user turn
self.conversation_history.append({
"role": "user",
"content": tool_results
})
# Loop again so Claude can generate its final response
# Claude is done — extract and return the final text
elif response.stop_reason == "end_turn":
final_text = ""
for block in response.content:
if hasattr(block, "text"):
final_text += block.text
# Save Claude's final reply to conversation history
self.conversation_history.append({
"role": "assistant",
"content": final_text
})
return final_text
else:
# Handle unexpected stop reasons gracefully
return "I'm sorry, something went wrong. Please try again."
def chat(self, user_message: str) -> str:
"""Public method — call this to send a message and get a response."""
return self._run_agent_loop(user_message)
def reset(self):
"""Clear conversation history to start a fresh session."""
self.conversation_history = []
The while True loop is the key pattern here. Claude might need to call two or three tools in sequence before it has enough information to answer — this loop handles that automatically. We only break out when stop_reason is "end_turn", meaning Claude is confident it has a final answer.
Step 4: Implement Conversation Memory
You already saw self.conversation_history in the class above — that list is your conversation memory. Every user message and every assistant reply gets appended, so Claude always has full context of what's been said. Here's how to verify it's working and how to inspect it.
def print_conversation_history(chatbot: HealthcareChatbot) -> None:
"""
Debug utility: print the full conversation history in a readable format.
Useful during development to confirm memory is working correctly.
"""
print("\n" + "="*60)
print("CONVERSATION HISTORY")
print("="*60)
for i, turn in enumerate(chatbot.conversation_history):
role = turn["role"].upper()
content = turn["content"]
# Tool result turns contain lists, not plain strings
if isinstance(content, list):
print(f"\n[{i}] {role}: [Tool interaction - {len(content)} item(s)]")
for item in content:
if isinstance(item, dict):
print(f" → {item.get('type', 'unknown')}: {str(item)[:120]}...")
else:
# Truncate long responses for readability in debug output
display = str(content)[:200] + "..." if len(str(content)) > 200 else str(content)
print(f"\n[{i}] {role}: {display}")
print("="*60 + "\n")
conversation_history list grows with every turn. For long patient sessions, consider trimming old turns or summarizing them to stay within Claude's context window. A rolling window of the last 20 messages works well for most clinic chatbots.
Step 5: Add Appointment Booking Logic (Main Conversation Loop)
This is the final piece — the main conversation loop that ties everything together and shows the full booking flow with realistic sample output. Run this file directly to see the chatbot in action.
healthcare_chatbot.py — Part 5: Appointment Booking Flow and Main Loop
def run_demo_conversation():
"""
Demonstrates a realistic patient interaction:
- Patient identifies themselves
- Asks about available appointments
- Books a slot with a specific doctor
"""
print("\n" + "="*60)
print("Naples Family Health Clinic — AI Assistant")
print("="*60)
bot = HealthcareChatbot()
# --- Demo conversation with pre-scripted inputs ---
demo_exchanges = [
"Hi, my patient ID is P1042. Can you look up my account?",
"I'd like to see Dr. Rivera. What slots are available on 2026-06-15?",
"Please book me the 10:30 slot for a routine follow-up.",
"Thanks! Can you confirm all my upcoming appointments?"
]
for user_input in demo_exchanges:
print(f"\nPatient: {user_input}")
response = bot.chat(user_input)
print(f"Assistant: {response}")
# Show memory is intact after full conversation
print_conversation_history(bot)
def run_interactive_mode():
"""
Interactive REPL mode — type messages and get live responses.
Type 'quit' to exit, 'reset' to clear conversation history.
"""
print("\n" + "="*60)
print("Naples Family Health Clinic — AI Assistant (Interactive)")
print("Type 'quit' to exit | 'reset' to start over")
print("="*60)
bot = HealthcareChatbot()
while True:
user_input = input("\nYou: ").strip()
if not user_input:
continue
if user_input.lower() == "quit":
print("Goodbye!")
break
if user_input.lower() == "reset":
bot.reset()
print("Conversation cleared. Starting fresh.")
continue
response = bot.chat(user_input)
print(f"\nAssistant: {response}")
if __name__ == "__main__":
# Run the scripted demo by default
# Switch to run_interactive_mode() to type your own messages
run_demo_conversation()
Here's what the demo output actually looks like when you run it:
Sample Output
============================================================
Naples Family Health Clinic — AI Assistant
============================================================
Patient: Hi, my patient ID is P1042. Can you look up my account?
Assistant: Hello! I found your account. You're Maria Gonzalez, date of birth
March 14, 1985, insured under BlueCross PPO. You currently have one upcoming
appointment: a checkup with Dr. Rivera on June 10, 2026 at 9:00 AM. How can
I help you today?
Patient: I'd like to see Dr. Rivera. What slots are available on 2026-06-15?
Assistant: Dr. Rivera has the following openings on June 15, 2026:
• 9:00 AM
• 10:30 AM
• 1:00 PM
• 2:30 PM
• 4:00 PM
Which time works best for you?
Patient: Please book me the 10:30 slot for a routine follow-up.
Assistant: Done! Your appointment is confirmed. Here are the details:
• Doctor: Dr. Rivera
• Date: June 15, 2026
• Time: 10:30 AM
• Reason: Routine follow-up
• Confirmation #: APT-P1042-20260615-1030
Is there anything else I can help you with?
Patient: Thanks! Can you confirm all my upcoming appointments?
Assistant: Of course, Maria! Based on what we have on file plus the appointment
we just booked, here's a summary of your upcoming visits with Dr. Rivera:
1. June 10, 2026 at 9:00 AM — Annual checkup
2. June 15, 2026 at 10:30 AM — Routine follow-up (just booked, confirmation APT-P1042-20260615-1030)
Let me know if you need to make any changes!
How It Works
Here's the plain-English version of what's happening under the hood. When a patient sends a message, the chatbot adds it to conversation_history and sends the whole conversation to Claude along with the tool schemas. Claude reads the schemas and decides whether to answer directly or call a tool first.
If Claude calls a tool, we execute the matching Python function and send the result back to Claude as a follow-up message. Claude then uses that data to craft a natural-language response. This back-and-forth can happen multiple times in a single patient turn — for example, Claude might look up patient info, then check slots, then book the appointment, all before typing a single word to the patient.
The conversation history is the memory system. Because we append every turn — including tool calls and their results — Claude always has full context. It knows the patient's name from turn one and uses it naturally in turn four without you doing anything extra.
Common Errors and Fixes
Error 1: AuthenticationError — Invalid API Key
anthropic.AuthenticationError: 401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}
Fix: Your ANTHROPIC_API_KEY environment variable isn't set or has a typo. Run echo $ANTHROPIC_API_KEY in your terminal to check it. Make sure there are no extra spaces or quote characters in the value. You can also hardcode it temporarily as Anthropic(api_key="sk-ant-...") just to confirm the key itself works.
Error 2: Tool Result Not Returned as String
anthropic.BadRequestError: 400 tool_result content must be a string or array of content blocks
Fix: The content field inside your tool result must be a JSON string, not a Python dict. That's exactly why process_tool_call returns json.dumps(result) instead of the raw dict. If you're customizing the tool functions, make sure you're always calling json.dumps() before returning.
Error 3: Infinite Loop — Missing end_turn Handler
# The program hangs and never returns a response to the user
# No error message — it just runs forever
Fix: This happens when your while True loop doesn't have an elif response.stop_reason == "end_turn" branch, or when you accidentally return inside the tool-call branch before Claude finishes. Double-check that only the end_turn branch returns a value and breaks the loop. Also add the fallback else branch shown in Step