Imagine a patient opens your mental health app and types:
"I need a Spanish-speaking therapist available Tuesday afternoon."
Instead of sending them to a calendar widget to click through filters and time slots, what if an AI agent justβ¦ handled it? Asked a clarifying question, found matching providers, presented options, and booked the appointment β all in a conversation?
That's exactly what we're building today.
We'll connect OpenAI's function calling to Orita's scheduling API to create an agent that takes natural language, finds available professionals, and books an appointment with a single confirmation from the user. Full Python and JavaScript implementations included.
What you'll build
User: "I need a Spanish-speaking therapist Tuesday afternoon"
β
OpenAI GPT-4o
(decides to call search_providers)
β
Orita resolveScheduling()
(returns matching slots)
β
Agent presents options to user
β
User: "Option 2 looks good"
β
OpenAI GPT-4o
(decides to call book_appointment)
β
Orita book()
(returns booking confirmation)
β
Agent: "You're all set! Dr. Ana RamΓrez on Tuesday at 3 PM."
The key ingredient is OpenAI function calling: GPT-4o decides when to call Orita and what arguments to pass, based purely on what the user said. Your code just defines the tools and executes them when called.
Prerequisites
- An Orita API key (
orita_...) from your dashboard - An OpenAI API key
- Python 3.9+ or Node.js 18+
# Python
pip install orita-sdk openai
# Node.js
npm install orita-sdk openai
Architecture overview
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Agent Loop β
β β
β messages[] βββΊ OpenAI chat completion β
β β β
β ββββββββββββββ΄βββββββββββββ β
β β text response β function_callβ
β β (show to user) β β
β β ββββββΌβββββββββββββ β
β β β execute_tool() β β
β β β β β
β β β search_providersβ β
β β β β resolveSchedulingβ
β β β β β
β β β book_appointmentβ β
β β β β book() β β
β β ββββββ¬βββββββββββββ β
β β β β
β βββββββββ append result βββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
The agent loop runs until OpenAI returns a plain text response with no function calls β that's when the job is done.
Step 1 β Define the OpenAI tools
OpenAI function calling requires you to describe your tools as JSON schemas. GPT-4o reads these descriptions and decides when and how to call them.
Python
tools = [
{
"type": "function",
"function": {
"name": "search_providers",
"description": (
"Search for available therapist time slots that match the user's "
"requirements. Call this when the user describes what they need "
"(specialty, language, date/time preferences). Returns a list of "
"available options the user can choose from."
),
"parameters": {
"type": "object",
"properties": {
"service": {
"type": "string",
"description": "Type of service (e.g., 'therapy', 'psychiatry', 'coaching')",
},
"language": {
"type": "string",
"description": "Preferred language of the professional (e.g., 'Spanish', 'English')",
},
"specialty": {
"type": "string",
"description": "Clinical specialty (e.g., 'anxiety', 'depression', 'trauma')",
},
"date_range_start": {
"type": "string",
"description": "Start of the preferred date range (ISO 8601, e.g., '2026-07-29')",
},
"date_range_end": {
"type": "string",
"description": "End of the preferred date range (ISO 8601, e.g., '2026-08-05')",
},
"preference": {
"type": "string",
"description": "Time of day preference: 'morning', 'afternoon', or 'evening'",
"enum": ["morning", "afternoon", "evening"],
},
},
"required": ["service"],
},
},
},
{
"type": "function",
"function": {
"name": "book_appointment",
"description": (
"Book a specific appointment slot that the user has selected. "
"Only call this AFTER the user has explicitly confirmed which option they want. "
"Never book without user confirmation."
),
"parameters": {
"type": "object",
"properties": {
"slot_id": {
"type": "string",
"description": "The slotId from search_providers results",
},
"patient_name": {
"type": "string",
"description": "Full name of the patient",
},
"patient_email": {
"type": "string",
"description": "Email address of the patient",
},
},
"required": ["slot_id", "patient_name", "patient_email"],
},
},
},
]
Step 2 β Connect the tools to Orita
import json
from orita_sdk import OritaClient
orita = OritaClient(api_key="orita_your_key_here")
def search_providers(
service: str,
language: str | None = None,
specialty: str | None = None,
date_range_start: str | None = None,
date_range_end: str | None = None,
preference: str | None = None,
) -> str:
"""Call Orita resolveScheduling and return results as a JSON string."""
constraints = {}
if language:
constraints["language"] = language
if specialty:
constraints["specialty"] = specialty
date_range = {}
if date_range_start:
date_range["start"] = date_range_start
if date_range_end:
date_range["end"] = date_range_end
options = orita.resolve_scheduling(
service=service,
constraints=constraints,
date_range=date_range if date_range else None,
preference=preference,
)
# Format nicely for the model
formatted = []
for i, opt in enumerate(options[:5], 1): # limit to 5 options
formatted.append({
"option": i,
"slot_id": opt["slotId"],
"professional": opt.get("professionalName", "Unknown"),
"datetime": opt.get("scheduledAt"),
"duration_minutes": opt.get("durationMinutes"),
"language": opt.get("language"),
})
return json.dumps({"options": formatted, "total_found": len(options)})
def book_appointment(slot_id: str, patient_name: str, patient_email: str) -> str:
"""Call Orita book() and return the confirmation."""
result = orita.book(
slot_id=slot_id,
customer={"name": patient_name, "email": patient_email},
)
return json.dumps({
"booking_id": result["id"],
"status": result["status"], # "pending"
"message": "Booking created successfully. You'll receive a confirmation email shortly.",
})
def execute_tool(name: str, arguments: dict) -> str:
"""Route a function call to the right implementation."""
if name == "search_providers":
return search_providers(**arguments)
elif name == "book_appointment":
return book_appointment(**arguments)
else:
return json.dumps({"error": f"Unknown tool: {name}"})
Step 3 β The agent loop
import json
from openai import OpenAI
openai_client = OpenAI(api_key="sk-your-openai-key")
SYSTEM_PROMPT = """You are a compassionate scheduling assistant for a mental health platform.
Your job is to help patients find and book therapy appointments.
Guidelines:
- Be warm, clear, and concise
- Always search for providers before booking
- Present options in a numbered list that's easy to scan
- ALWAYS ask for explicit confirmation before calling book_appointment
- Ask for the patient's name and email if you don't have them yet
- If no slots are found, apologize and suggest a wider date range
Never reveal internal slot IDs or technical details to the user."""
def run_agent(patient_name: str, patient_email: str):
"""Run an interactive booking agent session."""
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "system",
"content": f"Patient context: name={patient_name}, email={patient_email}",
},
]
print(f"\nπ€ Hello {patient_name}! How can I help you today?\n")
while True:
user_input = input("You: ").strip()
if not user_input or user_input.lower() in ("quit", "exit"):
break
messages.append({"role": "user", "content": user_input})
# Agent loop β keep going until we get a plain text response
while True:
response = openai_client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto",
)
message = response.choices[0].message
# If no tool calls, we have a final response
if not message.tool_calls:
print(f"\nπ€ {message.content}\n")
messages.append({"role": "assistant", "content": message.content})
break
# Execute each tool call
messages.append(message) # append the assistant message with tool_calls
for tool_call in message.tool_calls:
fn_name = tool_call.function.name
fn_args = json.loads(tool_call.function.arguments)
print(f" [Calling {fn_name}...]")
result = execute_tool(fn_name, fn_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
# Loop back to get OpenAI's next response
if __name__ == "__main__":
run_agent(
patient_name="Maria GarcΓa",
patient_email="maria@example.com",
)
Seeing it in action
Here's what a real conversation looks like:
π€ Hello Maria! How can I help you today?
You: I need a Spanish-speaking therapist for anxiety, Tuesday afternoon if possible
[Calling search_providers...]
π€ I found 3 available therapists for you:
**1. Dr. Ana RamΓrez**
π
Tuesday, July 29 at 2:00 PM (50 min)
π Spanish | Specialty: Anxiety & CBT
**2. Lic. Carlos Vega**
π
Tuesday, July 29 at 3:30 PM (50 min)
π Spanish | Specialty: Anxiety & Mindfulness
**3. Dr. Sofia Mendez**
π
Tuesday, July 29 at 4:00 PM (50 min)
π Spanish | Specialty: Anxiety & Depression
Which option would you like to book?
You: Option 1 please
π€ To confirm: you'd like to book with **Dr. Ana RamΓrez** on **Tuesday, July 29 at 2:00 PM**.
Shall I go ahead and book this for maria@example.com?
You: Yes, please go ahead
[Calling book_appointment...]
π€ β
You're all set, Maria!
Your appointment with Dr. Ana RamΓrez is confirmed for **Tuesday, July 29 at 2:00 PM**.
You'll receive a confirmation email at maria@example.com shortly.
Is there anything else I can help you with?
Step 4 β JavaScript version (Node.js)
The same flow in Node.js with the openai npm package and orita-sdk:
import OpenAI from "openai";
import { OritaClient } from "orita-sdk";
import * as readline from "readline";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const orita = new OritaClient({ apiKey: process.env.ORITA_API_KEY });
const tools = [
{
type: "function",
function: {
name: "search_providers",
description:
"Search for available therapist time slots matching user requirements.",
parameters: {
type: "object",
properties: {
service: { type: "string" },
language: { type: "string" },
specialty: { type: "string" },
date_range_start: { type: "string" },
date_range_end: { type: "string" },
preference: { type: "string", enum: ["morning", "afternoon", "evening"] },
},
required: ["service"],
},
},
},
{
type: "function",
function: {
name: "book_appointment",
description:
"Book a slot after the user has explicitly confirmed their choice.",
parameters: {
type: "object",
properties: {
slot_id: { type: "string" },
patient_name: { type: "string" },
patient_email: { type: "string" },
},
required: ["slot_id", "patient_name", "patient_email"],
},
},
},
];
async function searchProviders({ service, language, specialty, date_range_start, date_range_end, preference }) {
const constraints = {};
if (language) constraints.language = language;
if (specialty) constraints.specialty = specialty;
const dateRange = {};
if (date_range_start) dateRange.start = date_range_start;
if (date_range_end) dateRange.end = date_range_end;
const options = await orita.resolveScheduling({
service,
constraints,
dateRange: Object.keys(dateRange).length ? dateRange : undefined,
preference,
});
const formatted = options.slice(0, 5).map((opt, i) => ({
option: i + 1,
slotId: opt.slotId,
professional: opt.professionalName ?? "Unknown",
datetime: opt.scheduledAt,
durationMinutes: opt.durationMinutes,
language: opt.language,
}));
return JSON.stringify({ options: formatted, totalFound: options.length });
}
async function bookAppointment({ slot_id, patient_name, patient_email }) {
const result = await orita.book({
slotId: slot_id,
customer: { name: patient_name, email: patient_email },
});
return JSON.stringify({
bookingId: result.id,
status: result.status,
message: "Booking created. Confirmation email on its way!",
});
}
async function executeTool(name, args) {
if (name === "search_providers") return searchProviders(args);
if (name === "book_appointment") return bookAppointment(args);
return JSON.stringify({ error: `Unknown tool: ${name}` });
}
async function agentLoop(messages) {
while (true) {
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages,
tools,
tool_choice: "auto",
});
const message = response.choices[0].message;
if (!message.tool_calls || message.tool_calls.length === 0) {
return message.content;
}
messages.push(message);
for (const toolCall of message.tool_calls) {
const args = JSON.parse(toolCall.function.arguments);
process.stdout.write(` [Calling ${toolCall.function.name}...]\n`);
const result = await executeTool(toolCall.function.name, args);
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: result,
});
}
}
}
async function main() {
const patientName = "Maria GarcΓa";
const patientEmail = "maria@example.com";
const messages = [
{
role: "system",
content: `You are a compassionate scheduling assistant for a mental health platform.
Help patients find and book therapy appointments. Always confirm before booking.`,
},
{
role: "system",
content: `Patient context: name=${patientName}, email=${patientEmail}`,
},
];
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ask = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
console.log(`\nπ€ Hello ${patientName}! How can I help you today?\n`);
while (true) {
const userInput = await ask("You: ");
if (!userInput || ["quit", "exit"].includes(userInput.toLowerCase())) {
rl.close();
break;
}
messages.push({ role: "user", content: userInput });
const reply = await agentLoop(messages);
console.log(`\nπ€ ${reply}\n`);
messages.push({ role: "assistant", content: reply });
}
}
main().catch(console.error);
Key design decisions
Why two separate tools instead of one?
Splitting search_providers and book_appointment gives GPT-4o clear instructions about when each action is appropriate. The description on book_appointment explicitly says "only call after user confirmation" β the model respects this. A single combined tool would be harder to constrain.
Why return results as JSON strings?
OpenAI's tool result format requires a string in the content field. Returning structured JSON means the model can read and reason about the data (slot IDs, times, professional names) when formulating its response.
Why keep slotId out of the user-facing message?
Slot IDs are internal implementation details. The system prompt instructs the agent not to reveal them. When the user says "option 2", the model remembers the slotId from the search results it already has in context and passes it directly to book_appointment.
What to build next
Once this agent is working, here are three high-value extensions:
1. Rescheduling tool
{
"name": "reschedule_appointment",
"description": "Reschedule an existing booking to a new slot.",
"parameters": {
"type": "object",
"properties": {
"booking_id": {"type": "string"},
"new_slot_id": {"type": "string"},
},
"required": ["booking_id", "new_slot_id"],
},
}
Implement by calling orita.cancel(booking_id) followed by orita.book(new_slot_id, customer).
2. Cancellation tool
{
"name": "cancel_appointment",
"description": "Cancel an existing booking by ID.",
"parameters": {
"type": "object",
"properties": {
"booking_id": {"type": "string"},
"reason": {"type": "string"},
},
"required": ["booking_id"],
},
}
Calls orita.cancel(booking_id) and sends a cancellation webhook to your backend.
3. WhatsApp integration
Wrap the agent loop in a webhook handler for Twilio's WhatsApp API. Each incoming message becomes a user turn; the agent's final text reply goes back as a WhatsApp message. The same logic, zero UI needed β patients book appointments by texting a number.
Summary
You've built an end-to-end AI booking agent that:
- β Understands natural language scheduling requests
- β Uses OpenAI function calling to decide when to search and when to book
- β
Calls
resolveScheduling()to find matching slots from Orita - β
Waits for explicit user confirmation before calling
book() - β Handles the full conversation loop in both Python and JavaScript
The combination of OpenAI's reasoning and Orita's scheduling API means you get an agent that's both smart (understands context and nuance) and reliable (operates on real availability data). Your patients get a booking experience that feels like texting a knowledgeable friend.
Next steps:
- Add persistent conversation history per patient (store
messages[]in your DB) - Add voice input via OpenAI Whisper for a fully voice-driven experience
- Deploy the Python version as a FastAPI endpoint and connect it to any frontend
