feat: add appointment booking whatsapp agent
Build Agents Runtime / build (push) Successful in 2m6s
Details
Build Agents Runtime / build (push) Successful in 2m6s
Details
This commit is contained in:
parent
65f0b96fa4
commit
cef0d330bf
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -157,3 +157,109 @@ async def save_clinical_record_activity(payload: dict) -> str:
|
||||||
raise e
|
raise e
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
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()
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,17 @@ class ClinicalRecord(Base):
|
||||||
# Billing
|
# Billing
|
||||||
billing_codes_json = Column(JSON, nullable=True)
|
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
|
# Create tables if they don't exist
|
||||||
def init_db():
|
def init_db():
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -10,13 +10,16 @@ from temporalio.worker import Worker
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from curaflow_agent.workflows import ClinicalIntakeWorkflow, LabResultWatcherWorkflow
|
from curaflow_agent.workflows import ClinicalIntakeWorkflow, LabResultWatcherWorkflow, AppointmentBookingWorkflow
|
||||||
from curaflow_agent.activities import (
|
from curaflow_agent.activities import (
|
||||||
structure_note_activity,
|
structure_note_activity,
|
||||||
generate_billing_codes_activity,
|
generate_billing_codes_activity,
|
||||||
analyze_lab_anomaly_activity,
|
analyze_lab_anomaly_activity,
|
||||||
send_alert_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():
|
async def main():
|
||||||
|
|
@ -36,13 +39,16 @@ async def main():
|
||||||
worker = Worker(
|
worker = Worker(
|
||||||
client,
|
client,
|
||||||
task_queue=task_queue,
|
task_queue=task_queue,
|
||||||
workflows=[ClinicalIntakeWorkflow, LabResultWatcherWorkflow],
|
workflows=[ClinicalIntakeWorkflow, LabResultWatcherWorkflow, AppointmentBookingWorkflow],
|
||||||
activities=[
|
activities=[
|
||||||
structure_note_activity,
|
structure_note_activity,
|
||||||
generate_billing_codes_activity,
|
generate_billing_codes_activity,
|
||||||
analyze_lab_anomaly_activity,
|
analyze_lab_anomaly_activity,
|
||||||
send_alert_activity,
|
send_alert_activity,
|
||||||
save_clinical_record_activity
|
save_clinical_record_activity,
|
||||||
|
parse_booking_intent_activity,
|
||||||
|
send_whatsapp_reply_activity,
|
||||||
|
save_appointment_activity
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,3 +115,57 @@ class LabResultWatcherWorkflow:
|
||||||
"status": "discharged",
|
"status": "discharged",
|
||||||
"total_alerts_triggered": alerts_triggered
|
"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())
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue