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
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
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
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
Loading…
Reference in New Issue