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 0000000..89505d7 Binary files /dev/null and b/src/curaflow_agent/__pycache__/activities.cpython-314.pyc differ diff --git a/src/curaflow_agent/__pycache__/lab_models.cpython-314.pyc b/src/curaflow_agent/__pycache__/lab_models.cpython-314.pyc new file mode 100644 index 0000000..62811a0 Binary files /dev/null and b/src/curaflow_agent/__pycache__/lab_models.cpython-314.pyc differ 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 0000000..8063465 Binary files /dev/null and b/src/curaflow_agent/__pycache__/models.cpython-314.pyc differ 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())