Every day, millions of patients do the same thing: call a clinic, get put on hold, explain their symptoms to a receptionist who isn't a doctor, get transferred, wait more, and finally β maybe β book an appointment with someone who might be the right specialist.
It doesn't have to work that way.
This tutorial shows you how to build a Healthcare Copilot: an AI system that listens to symptoms, identifies the right specialist, checks real availability, and books a confirmed appointment. Using Claude (Anthropic's AI) for medical reasoning and Orita for scheduling infrastructure.
No call center. No hold music. No human in the loop.
The real problem with healthcare scheduling
Call centers are expensive to staff, slow, and prone to errors:
- A patient describes vague symptoms to a non-clinical receptionist
- The receptionist guesses at the right specialist
- Wrong booking β cancellation β rebooking β delay in care
- Average time from "I have symptoms" to "appointment booked": 14 minutes by phone
An AI copilot collapses this to under 60 seconds and gets the specialty right every time.
Architecture
Patient (symptoms)
β
βΌ
βββββββββββββββββββββββββββ
β Claude (AI Copilot) β β Medical reasoning engine
β - Interprets symptoms β Understands context,
β - Maps to specialty β asks follow-up questions,
β - Guides conversation β explains decisions
ββββββββββ¬βββββββββββββββββ
β tool calls
ββββββΌβββββ
βΌ βΌ βΌ
find_ check_ book_
spec- slots appt
ialist β β
β β β
βΌ βΌ βΌ
βββββββββββββββββββββββββββ
β Orita API β β Scheduling infrastructure
β /professionals β Real providers, real
β /slots β availability, real
β /bookings β confirmations
βββββββββββββββββββββββββββ
Why Claude for healthcare?
Claude's extended thinking capabilities make it well-suited for medical contexts:
- It can reason through ambiguous symptom descriptions
- It asks clarifying questions when symptoms could indicate multiple conditions
- It's trained to be appropriately cautious β recommending emergency services when needed
- It handles emotionally sensitive conversations with empathy
Why Orita for the scheduling backend?
Orita is purpose-built for agent workflows. It exposes:
- A
/professionalsendpoint with specialty filtering - Real-time
/slotswith provider-level availability - Atomic
/bookingscreation with conflict detection - Webhooks for downstream automation (EHR systems, SMS reminders)
Prerequisites
- Python 3.9+
- An Orita API key β Get one free
- An Anthropic API key β console.anthropic.com
- Professionals configured in Orita with availability
pip install anthropic orita-sdk
export ORITA_API_KEY=orita_your_key_here
export ANTHROPIC_API_KEY=sk-ant-your_key_here
Step 1: Initialize clients
import json
import os
from datetime import date, timedelta
import anthropic
from orita import OritaClient, OritaError
orita = OritaClient(api_key=os.environ.get("ORITA_API_KEY"))
claude = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
TODAY = date.today().isoformat()
Step 2: Define the tools
Anthropic's tool use (function calling) follows a similar pattern to OpenAI, but uses a slightly different schema format. The tool descriptions are the most important part β Claude reads them to decide which to invoke and when.
TOOLS = [
{
"name": "find_specialist",
"description": (
"Find medical specialists that match the patient's symptoms. "
"Analyzes symptoms and filters providers by the appropriate specialty. "
"Returns available professionals with IDs and event types."
),
"input_schema": {
"type": "object",
"properties": {
"symptoms": {
"type": "string",
"description": "Patient's symptoms in plain language",
},
"specialty": {
"type": "string",
"description": (
"Medical specialty inferred from symptoms "
"(e.g. 'psychology', 'cardiology', 'physiotherapy')"
),
},
},
"required": ["symptoms", "specialty"],
},
},
{
"name": "check_slots",
"description": (
"Check available appointment times for a provider on a given date. "
"Always call before book_appointment."
),
"input_schema": {
"type": "object",
"properties": {
"event_type_id": {"type": "string"},
"date": {"type": "string", "description": "YYYY-MM-DD"},
"provider_id": {"type": "string"},
},
"required": ["event_type_id", "date"],
},
},
{
"name": "book_appointment",
"description": (
"Book a confirmed appointment. "
"Only call after the patient approves the date, time, and provider."
),
"input_schema": {
"type": "object",
"properties": {
"event_type_id": {"type": "string"},
"date": {"type": "string"},
"time": {"type": "string"},
"client_name": {"type": "string"},
"client_lastname": {"type": "string"},
"client_email": {"type": "string"},
"notes": {
"type": "string",
"description": "Patient symptoms β sent as pre-visit notes to the doctor",
},
"provider_id": {"type": "string"},
},
"required": ["event_type_id", "date", "time",
"client_name", "client_lastname", "client_email"],
},
},
]
Notice the notes field on book_appointment. This is where the patient's symptoms get forwarded to the doctor as pre-visit context β no need for a separate intake form.
Step 3: Implement the tools
def execute_tool(name: str, args: dict) -> str:
try:
if name == "find_specialist":
specialty = args.get("specialty", "").lower()
providers = orita.professionals(specialty=specialty)
if not providers:
# Fallback to all event types
event_types = orita.event_types()
return json.dumps({
"message": f"No '{specialty}' specialists found. Showing all appointment types.",
"event_types": [{"id": et["id"], "title": et["title"]} for et in event_types],
})
result = []
for pro in providers:
pro_events = orita.event_types(provider_id=pro["id"])
result.append({
"provider_id": pro["id"],
"name": f"Dr. {pro.get('name', '')} {pro.get('lastname', '')}".strip(),
"specialty": pro.get("specialty", specialty),
"event_types": [
{"id": et["id"], "title": et["title"], "duration": et.get("duration", 60)}
for et in pro_events
],
})
return json.dumps({"specialists": result})
elif name == "check_slots":
slots = orita.slots(
event_type_id=args["event_type_id"],
date=args["date"],
provider_id=args.get("provider_id"),
)
if not slots:
next_day = (date.fromisoformat(args["date"]) + timedelta(days=1)).isoformat()
return json.dumps({
"available": False,
"suggestion": f"No slots on {args['date']}. Try {next_day}.",
})
return json.dumps({
"date": args["date"],
"available": True,
"slots": [{"time": s["value"], "display": s["label"]} for s in slots],
})
elif name == "book_appointment":
booking = orita.book(
event_type_id=args["event_type_id"],
date=args["date"],
time=args["time"],
client_name=args["client_name"],
client_lastname=args["client_lastname"],
client_email=args["client_email"],
notes=args.get("notes"),
provider_id=args.get("provider_id"),
)
return json.dumps({
"success": True,
"booking_id": booking["id"],
"status": booking["status"],
"confirmed_date": booking.get("date", args["date"]),
"confirmed_time": booking.get("time", args["time"]),
})
except OritaError as e:
return json.dumps({"error": str(e)})
The find_specialist tool calls orita.professionals(specialty=specialty) β this filters providers server-side. No need to download all professionals and filter locally.
Step 4: The system prompt
SYSTEM_PROMPT = f"""You are a compassionate AI healthcare copilot helping patients
get the right medical care quickly.
Today: {TODAY}
Your workflow:
1. Listen to the patient's symptoms with empathy
2. Identify the right specialty (don't ask them to choose β figure it out)
3. Call find_specialist to find available providers
4. Ask for their preferred date β suggest tomorrow or next week
5. Call check_slots and present times clearly
6. Collect full name and email
7. Call book_appointment β include their symptoms in the notes field
8. Give them the booking ID and what to expect
Clinical guidelines:
- You are a SCHEDULING assistant, not a diagnostic tool
- Never diagnose or prescribe
- If symptoms sound urgent or emergency-level (chest pain, difficulty breathing,
stroke symptoms), tell them to call emergency services (112/911) first
- Be empathetic β many patients are anxious about their health
- Explain WHY you're recommending a specific specialist
Communication:
- Warm and reassuring, never clinical or cold
- Translate times to human-readable format (not raw JSON)
- Explain your reasoning: "Based on what you're describing, a physiotherapist
would be the right specialist because..."
"""
Two things worth noting:
- The emergency escalation instruction is critical. Any healthcare AI scheduling system needs a clear path to "call 112/911" for urgent symptoms.
- Explain your reasoning β patients feel more confident when the AI tells them why it's recommending a particular specialist.
Step 5: The agentic loop
Claude's tool use works slightly differently from OpenAI: tool calls come back as content blocks with type: "tool_use", and tool results are sent as "user" role messages with "tool_result" blocks.
def run_healthcare_copilot():
messages = []
booking_confirmed = False
while not booking_confirmed:
user_input = input("You: ").strip()
messages.append({"role": "user", "content": user_input})
while True: # Inner loop: process all tool calls for this turn
response = claude.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system=SYSTEM_PROMPT,
messages=messages,
tools=TOOLS,
)
tool_uses = [b for b in response.content if b.type == "tool_use"]
text_blocks = [b for b in response.content if b.type == "text"]
# Print any text the model generated before calling tools
for tb in text_blocks:
if tb.text:
print(f"Copilot: {tb.text}")
if not tool_uses:
messages.append({"role": "assistant", "content": response.content})
break # Done with this turn
messages.append({"role": "assistant", "content": response.content})
# Execute tools and collect results
tool_results = []
for tu in tool_uses:
result_str = execute_tool(tu.name, tu.input)
result_data = json.loads(result_str)
if tu.name == "book_appointment" and result_data.get("success"):
booking_confirmed = True
tool_results.append({
"type": "tool_result",
"tool_use_id": tu.id,
"content": result_str,
})
# Return tool results as a user message
messages.append({"role": "user", "content": tool_results})
The inner while True loop keeps running until the model returns a pure text response (no tool calls). This handles cases where the model chains multiple tool calls β e.g., find_specialist β check_slots β book_appointment in sequence.
What a session looks like
You: Hi, I've been having a lot of back pain for the past two weeks.
It's getting worse and affecting my work.
β [Orita] find_specialist(specialty=physiotherapy)
Copilot: I'm sorry to hear that β back pain that's getting worse definitely
deserves attention. Based on what you're describing, a physiotherapist
would be the right specialist. They can assess the cause of the pain,
provide targeted treatment, and give you exercises to prevent it from
coming back.
We have Dr. Carlos Mendoza available for 60-minute physiotherapy
sessions. When would you like to come in? I can check tomorrow or
next week.
You: Next week Monday morning would work.
β [Orita] check_slots(event_type_id=evt_xxx, date=2026-08-03)
Copilot: Dr. Mendoza has these Monday morning slots available:
- 9:00 AM
- 10:00 AM
- 11:00 AM
Which one works for you?
You: 9 AM please. My name is Javier LΓ³pez, email javier@example.com.
Copilot: Let me confirm: Javier LΓ³pez, with Dr. Carlos Mendoza,
Monday August 3rd at 9:00 AM. Ready to book?
You: Yes!
β [Orita] book_appointment(date=2026-08-03, time=09:00, notes=back pain, ...)
Copilot: All booked! Your confirmation ID is BK-18423.
You'll receive a confirmation email at javier@example.com shortly.
For your appointment, wear comfortable clothing that allows Dr. Mendoza
to examine your back. See you Monday! π
The agentProfile in Orita
When you set up professionals in Orita, each profile can include structured metadata that helps AI agents make better routing decisions. The specialty field is the primary routing signal β Claude uses this to match symptoms to providers.
If your Orita account uses the /professionals API, each professional exposes:
{
"id": "pro_xxx",
"name": "Carlos",
"lastname": "Mendoza",
"specialty": "physiotherapy",
"profession": "Physiotherapist",
"location": "Madrid, Spain",
"languages": ["es", "en"]
}
Filtering by specialty with orita.professionals(specialty="physiotherapy") returns only relevant providers β no need for client-side filtering.
What happens on the backend
When book_appointment succeeds, Orita:
- Conflict detection: Atomically checks that the slot is still available (race conditions are handled server-side β no double-bookings)
- Confirmation email: Sends to the patient's email with date, time, and meeting link
- Provider notification: Emails the professional with the new booking
- Webhook: Fires
booking.createdto your configured endpoint for downstream systems (EHR, SMS reminders, calendar sync)
The booking is visible immediately in the professional's Orita dashboard.
Compliance and privacy considerations
Before deploying a healthcare copilot in production:
Data handling:
- Patient symptoms are sent to Claude (Anthropic) for processing β review Anthropic's data use policies
- The
notesfield (symptoms) is stored by Orita and visible to the provider β this is expected, but document it in your privacy policy - Consider anonymizing symptom descriptions for analytics
Regulatory context:
- This is a scheduling assistant, not a medical device β make that clear in your UI
- Depending on your jurisdiction, collecting health information may require consent flows (GDPR Article 9 in EU, HIPAA in US)
- Always include an emergency escalation path in the system prompt
Safe messaging:
- Never have the AI suggest a diagnosis
- Never recommend or contraindicate medications
- When in doubt: "Please consult with a healthcare professional"
Get the full code
The complete working example with demo mode:
git clone https://github.com/Alkilo-do/orita-python
cd orita-python
pip install -e .
export ORITA_API_KEY=orita_your_key
export ANTHROPIC_API_KEY=sk-ant-your_key
python examples/healthcare_copilot.py --demo
Start building
Orita's free tier includes 250 bookings/month β enough to build and validate your healthcare copilot.
Questions? hola@orita.online
