feat: add appointment booking whatsapp agent
Build Agents Runtime / build (push) Successful in 2m6s Details

This commit is contained in:
Deep Koluguri 2026-05-23 00:06:25 -04:00
parent 65f0b96fa4
commit cef0d330bf
9 changed files with 231 additions and 4 deletions

Binary file not shown.

View File

@ -157,3 +157,109 @@ async def save_clinical_record_activity(payload: dict) -> str:
raise e
finally:
db.close()
# ==========================================
# Appointment Booking Activities
# ==========================================
class AppointmentIntentSchema(BaseModel):
is_booking_request: bool = Field(description="True if the user is asking to book, schedule, or modify an appointment.")
department: str = Field(description="The department they want to visit (e.g. Cardiology, General, Dental). Leave empty if not specified.")
appointment_date: str = Field(description="The date they want to visit (e.g. 'Monday', 'Tomorrow', '10/12/2026'). Leave empty if not specified.")
appointment_time: str = Field(description="The time they want to visit (e.g. 'Morning', '3 PM'). Leave empty if not specified.")
missing_info: list[str] = Field(description="List of fields that are still missing and need to be asked. E.g. ['department', 'appointment_date']")
reply_message: str = Field(description="A natural, friendly message to send back to the user. E.g. 'I can help you book that. Which department would you like to visit?' or 'Your appointment is booked!'")
@activity.defn
async def parse_booking_intent_activity(chat_history: list[dict]) -> dict:
"""
Parses the chat history using the LLM to extract booking intent and details.
chat_history is a list of {"role": "user"|"assistant", "content": "..."}
"""
from langchain_core.prompts import PromptTemplate
prompt = PromptTemplate.from_template(
"You are a helpful hospital receptionist bot talking to a patient on WhatsApp.\n"
"Here is the conversation history:\n"
"{history}\n\n"
"Analyze the conversation. Extract any appointment details (department, date, time).\n"
"If they want to book an appointment but haven't specified department, date, and time, "
"add those to missing_info and formulate a reply_message asking for them.\n"
"If all details are present, set missing_info to empty and formulate a confirmation reply_message.\n"
"Format output strictly as JSON matching the schema."
)
history_str = ""
for msg in chat_history:
history_str += f"{msg['role'].upper()}: {msg['content']}\n"
chain = prompt | get_llm().with_structured_output(AppointmentIntentSchema)
try:
print(f"Parsing intent for history:\n{history_str}")
result = chain.invoke({"history": history_str})
return result.dict()
except Exception as e:
print(f"Failed to parse booking intent: {e}")
# Fallback due to Pydantic strictness failures
return {
"is_booking_request": True,
"department": "",
"appointment_date": "",
"appointment_time": "",
"missing_info": ["department", "appointment_date", "appointment_time"],
"reply_message": "I'm sorry, I'm having trouble understanding. Could you please specify the department, date, and time you'd like to book?"
}
@activity.defn
async def send_whatsapp_reply_activity(payload: dict) -> str:
"""
Sends a WhatsApp message using Twilio API.
payload: {"phone_number": "whatsapp:+123...", "message": "..."}
"""
from twilio.rest import Client
import traceback
account_sid = os.environ.get("TWILIO_ACCOUNT_SID", "AC601e1962dc8417ddb3ae53cd8d865020")
auth_token = os.environ.get("TWILIO_AUTH_TOKEN", "08a68571a251d6d62f60dc83fc725b5a")
twilio_from = os.environ.get("TWILIO_FROM_NUMBER", "whatsapp:+14155238886")
phone_number = payload.get("phone_number")
message_body = payload.get("message")
try:
client = Client(account_sid, auth_token)
message = client.messages.create(
from_=twilio_from,
body=message_body,
to=phone_number
)
print(f"WhatsApp reply sent! SID: {message.sid}")
return f"Sent (SID: {message.sid})"
except Exception as e:
print(f"Failed to send WhatsApp reply: {str(e)}")
traceback.print_exc()
return f"Failed: {str(e)}"
@activity.defn
async def save_appointment_activity(payload: dict) -> str:
from .db import SessionLocal, Appointment, init_db
init_db()
db = SessionLocal()
try:
record = Appointment(
phone_number=payload.get("phone_number", ""),
department=payload.get("department", ""),
appointment_date=payload.get("appointment_date", ""),
appointment_time=payload.get("appointment_time", ""),
status="CONFIRMED"
)
db.add(record)
db.commit()
db.refresh(record)
return f"Saved Appointment ID: {record.id}"
except Exception as e:
db.rollback()
raise e
finally:
db.close()

View File

@ -28,6 +28,17 @@ class ClinicalRecord(Base):
# Billing
billing_codes_json = Column(JSON, nullable=True)
class Appointment(Base):
__tablename__ = "appointments"
id = Column(Integer, primary_key=True, index=True)
phone_number = Column(String(50), index=True, nullable=False)
department = Column(String(100), nullable=True)
appointment_date = Column(String(50), nullable=True)
appointment_time = Column(String(50), nullable=True)
status = Column(String(20), default="PENDING")
created_at = Column(DateTime, default=datetime.utcnow)
# Create tables if they don't exist
def init_db():
Base.metadata.create_all(bind=engine)

View File

@ -0,0 +1,23 @@
import asyncio
from temporalio.client import Client
from curaflow_agent.activities import send_alert_activity
async def main():
print("Testing Twilio WhatsApp Integration directly...")
alert_data = {
"is_dangerous": True,
"danger_explanation": "Test Alert: Patient INR is 6.5, which is dangerously high while taking Warfarin. High risk of hemorrhage.",
"recommended_action": "Immediately administer Vitamin K and admit patient for observation."
}
# We can just call the activity directly as a standard Python function
# to test the Twilio logic without Temporal/LLM overhead for this proof!
try:
result = await send_alert_activity(alert_data)
print("Result:", result)
except Exception as e:
print("Error:", e)
if __name__ == "__main__":
asyncio.run(main())

View File

@ -0,0 +1,27 @@
from twilio.rest import Client
import traceback
def test():
account_sid = "AC601e1962dc8417ddb3ae53cd8d865020"
auth_token = "08a68571a251d6d62f60dc83fc725b5a"
twilio_from = "whatsapp:+14155238886"
physician_number = "whatsapp:+918500176938"
msg_body = "🚨 *CURAFLOW CRITICAL ALERT* 🚨\n\n*Anomaly Detected:*\nTest Alert: Patient INR is 6.5, which is dangerously high while taking Warfarin.\n\n*Recommended Action:*\nImmediately administer Vitamin K and admit patient for observation."
print(f"!!! Triggering Real Twilio WhatsApp Alert to {physician_number} !!!")
try:
client = Client(account_sid, auth_token)
message = client.messages.create(
from_=twilio_from,
body=msg_body,
to=physician_number
)
print(f"WhatsApp message sent successfully! SID: {message.sid}")
except Exception as e:
print(f"Failed to send WhatsApp message: {str(e)}")
traceback.print_exc()
if __name__ == "__main__":
test()

View File

@ -10,13 +10,16 @@ from temporalio.worker import Worker
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
from curaflow_agent.workflows import ClinicalIntakeWorkflow, LabResultWatcherWorkflow
from curaflow_agent.workflows import ClinicalIntakeWorkflow, LabResultWatcherWorkflow, AppointmentBookingWorkflow
from curaflow_agent.activities import (
structure_note_activity,
generate_billing_codes_activity,
analyze_lab_anomaly_activity,
send_alert_activity,
save_clinical_record_activity
save_clinical_record_activity,
parse_booking_intent_activity,
send_whatsapp_reply_activity,
save_appointment_activity
)
async def main():
@ -36,13 +39,16 @@ async def main():
worker = Worker(
client,
task_queue=task_queue,
workflows=[ClinicalIntakeWorkflow, LabResultWatcherWorkflow],
workflows=[ClinicalIntakeWorkflow, LabResultWatcherWorkflow, AppointmentBookingWorkflow],
activities=[
structure_note_activity,
generate_billing_codes_activity,
analyze_lab_anomaly_activity,
send_alert_activity,
save_clinical_record_activity
save_clinical_record_activity,
parse_booking_intent_activity,
send_whatsapp_reply_activity,
save_appointment_activity
],
)

View File

@ -115,3 +115,57 @@ class LabResultWatcherWorkflow:
"status": "discharged",
"total_alerts_triggered": alerts_triggered
}
@workflow.defn
class AppointmentBookingWorkflow:
def __init__(self):
import asyncio
self._chat_history = []
self._new_message_event = asyncio.Event()
@workflow.signal
def receive_message(self, message: str):
self._chat_history.append({"role": "user", "content": message})
self._new_message_event.set()
@workflow.run
async def run(self, initial_message: str, phone_number: str) -> dict:
self._chat_history.append({"role": "user", "content": initial_message})
while True:
# 1. Parse intent and get next action
intent_data = await workflow.execute_activity(
"parse_booking_intent_activity",
self._chat_history,
schedule_to_close_timeout=timedelta(minutes=5)
)
reply_msg = intent_data.get("reply_message", "I can help with that.")
self._chat_history.append({"role": "assistant", "content": reply_msg})
# 2. Send the reply back to the user via WhatsApp
await workflow.execute_activity(
"send_whatsapp_reply_activity",
{"phone_number": phone_number, "message": reply_msg},
schedule_to_close_timeout=timedelta(minutes=1)
)
# 3. Check if all details are gathered
missing = intent_data.get("missing_info", [])
if intent_data.get("is_booking_request") and not missing:
# All info gathered, save appointment!
await workflow.execute_activity(
"save_appointment_activity",
{
"phone_number": phone_number,
"department": intent_data.get("department"),
"appointment_date": intent_data.get("appointment_date"),
"appointment_time": intent_data.get("appointment_time")
},
schedule_to_close_timeout=timedelta(seconds=30)
)
return intent_data
# 4. Wait for the user's next message
self._new_message_event.clear()
await workflow.wait_condition(lambda: self._new_message_event.is_set())