From cef0d330bffa7cea29a432b9831827f67e5c9bab Mon Sep 17 00:00:00 2001 From: Deep Koluguri Date: Sat, 23 May 2026 00:06:25 -0400 Subject: [PATCH] feat: add appointment booking whatsapp agent --- .../__pycache__/activities.cpython-314.pyc | Bin 0 -> 9326 bytes .../__pycache__/lab_models.cpython-314.pyc | Bin 0 -> 1265 bytes .../__pycache__/models.cpython-314.pyc | Bin 0 -> 3076 bytes src/curaflow_agent/activities.py | 106 ++++++++++++++++++ src/curaflow_agent/db.py | 11 ++ src/curaflow_agent/test_twilio.py | 23 ++++ src/curaflow_agent/test_twilio_direct.py | 27 +++++ src/curaflow_agent/worker.py | 14 ++- src/curaflow_agent/workflows.py | 54 +++++++++ 9 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 src/curaflow_agent/__pycache__/activities.cpython-314.pyc create mode 100644 src/curaflow_agent/__pycache__/lab_models.cpython-314.pyc create mode 100644 src/curaflow_agent/__pycache__/models.cpython-314.pyc create mode 100644 src/curaflow_agent/test_twilio.py create mode 100644 src/curaflow_agent/test_twilio_direct.py diff --git a/src/curaflow_agent/__pycache__/activities.cpython-314.pyc b/src/curaflow_agent/__pycache__/activities.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89505d744ebcdeb4fda019a46332feceea2b18e4 GIT binary patch literal 9326 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 Uzrh>Nl9+5pKdt=*;XBIv7kooswEzGB literal 0 HcmV?d00001 diff --git a/src/curaflow_agent/__pycache__/models.cpython-314.pyc b/src/curaflow_agent/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..80634656c34aabe199a097a31795985a6ddde59c GIT binary patch literal 3076 zcmb_e&2tn*6z|!c{mv#Kf{E=DT`!55z0bHgvPKQS`06!LS2Z6$QT(Q`$#T4L~`nN zq)1;TqC1y@4R?86bGGGJreQC+ zg4U)l6{%KcR>@#hjZuTUj#hA)CeUpNi#lSGYj-VfiR@#=OXVpynI$t)J3CK>Vc9gV zIR<0U`7YJm!b?NV3S%&d#;c{Wa7$b>oV-?q#Ja4StxaHxanNnYz30G+P%F_qhNqdX zleaMSJX<^7>DQUg?<8A;3u+eEgmK$)R6H`><<4%RFl$2HH3v zh#Um$#xG#bD0Om~+z6fimXJJncau+*qH=_Mrd%WMg>uRS^l6tv?XrUPL%X1%{XxI* zHf`!QZIQtI(LgzdWk5^(X$T5lk6E;6K{wqJY|e6ox2t`Cx?9l8jB?18ZrQd2dGq@5 z5cT#U>g|qRDsW(4;>B5b$$zu$shB$8U6>2TP!FIS1u&Kj;iYxmw4pCRL+Coku*U1& z?A*I6pL5Fi3a>bMx>9ED{gQis#aM-i_+eIYgjJ#|oS7>C9HU^n_jK8|QbMZpY`N-n z>AK-Kt}q1Eb#Eldj%=_A&yAxv{rw&Jnyd{BH`QzFVWKF%Yx|q(gY^g$*P~=~>?i(n zrl}h1F?7U9-{2$jhmoe5Ur(STNqYMq&3#{OstfBWbaas&{Y}+bPh+Knc1w@07c9mFu$wxrt}_gnrO|Yw=|R(r zW(S%+G&{j~I|92bR2&m#9)J`>LM&4^gkaXqil7|a(DTJ1nAEm){jtIZVGfSq{(ueP zobis43A+Y0NLZb$jcSddR{r(~Z+#wI&`k)V;F&WoyD{sKaY}#v4b>)}DO* zW7+$o*62cgacyw4F;!{}I`#8wy#tN$rB?59ef~LS=o`Y22Xe{RtFwUSE$W>9f7My) zY*gxOA{P@$(AiWj4$Mh-saaqI2+?YiXc@4Tj)SB78 zFf?EZeZ7{Qhh6*!Eklj#Xcv~HRzdb6tP3Xe5*U~+s%7e_SY-Ho@Y1&`TwtQ)MluTP z2X7|C2H@wT(N_l_j>9-)IB*tQNR9O1r@;86?&)i)vp%Vl>85(ML+ZYPrh27A=>DOm z%AO&aee{jO@TE)_+YNuV2hCnIV`wA?C$Thz2IFTLG#Z+HXvWd(2NNXGL3E;#0iK3} zPojZcjmfjEfmxsha^Xs=_iBB9ZD@BR^F?du3h-uRZ)5sSYlPJ=JjVz-48f^h&ye?_ zWeY=gzKJ0c>+s})H$^n3iWp!$o{PX~MZLa2e))1mlq;fkqI3O%*lR|)Y{P-ww5fK} zr-cL^fAQAvUl@`NuSeM15a)IXgIiH~=~k4tC&U->(G)=laj4aUd_wkZsxjrF@^EI8;D57ERk-pnvq|v3*`>g1=Esr0AiAQTk$(U| Cdhd|{ literal 0 HcmV?d00001 diff --git a/src/curaflow_agent/activities.py b/src/curaflow_agent/activities.py index 63cce8f..733ded1 100644 --- a/src/curaflow_agent/activities.py +++ b/src/curaflow_agent/activities.py @@ -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() diff --git a/src/curaflow_agent/db.py b/src/curaflow_agent/db.py index 555c429..cfb1ecb 100644 --- a/src/curaflow_agent/db.py +++ b/src/curaflow_agent/db.py @@ -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) diff --git a/src/curaflow_agent/test_twilio.py b/src/curaflow_agent/test_twilio.py new file mode 100644 index 0000000..fdc2b69 --- /dev/null +++ b/src/curaflow_agent/test_twilio.py @@ -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()) diff --git a/src/curaflow_agent/test_twilio_direct.py b/src/curaflow_agent/test_twilio_direct.py new file mode 100644 index 0000000..1760724 --- /dev/null +++ b/src/curaflow_agent/test_twilio_direct.py @@ -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() diff --git a/src/curaflow_agent/worker.py b/src/curaflow_agent/worker.py index 3df020e..5d85283 100644 --- a/src/curaflow_agent/worker.py +++ b/src/curaflow_agent/worker.py @@ -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 ], ) diff --git a/src/curaflow_agent/workflows.py b/src/curaflow_agent/workflows.py index 5ef6dd7..d689c92 100644 --- a/src/curaflow_agent/workflows.py +++ b/src/curaflow_agent/workflows.py @@ -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())