Spaces:
Running
Running
File size: 18,200 Bytes
59ba41d c63b8b6 59ba41d c63b8b6 59ba41d c63b8b6 59ba41d c63b8b6 59ba41d c63b8b6 59ba41d c63b8b6 59ba41d c63b8b6 2f49513 59ba41d c63b8b6 59ba41d c63b8b6 2f49513 c63b8b6 59ba41d c63b8b6 2f49513 c63b8b6 59ba41d c63b8b6 2f49513 c63b8b6 59ba41d c63b8b6 2f49513 c63b8b6 59ba41d c63b8b6 2f49513 c63b8b6 59ba41d c63b8b6 2f49513 c63b8b6 59ba41d c63b8b6 2f49513 9312c3a 2f49513 9312c3a 2f49513 9312c3a 2f49513 9312c3a 2f49513 9312c3a 2f49513 9312c3a 2f49513 c63b8b6 59ba41d c63b8b6 2f49513 c63b8b6 59ba41d c63b8b6 59ba41d c63b8b6 59ba41d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 |
import os
import sys
import json
from typing import Any, Dict, Optional
from langchain_core.tools import tool
# Robust logic import isolated to this agent
try:
from . import logic as hc_logic # type: ignore
except Exception:
import importlib.util as _ilu
_dir = os.path.dirname(__file__)
_logic_path = os.path.join(_dir, "logic.py")
_spec = _ilu.spec_from_file_location("healthcare_agent_logic", _logic_path)
hc_logic = _ilu.module_from_spec(_spec) # type: ignore
assert _spec and _spec.loader
_spec.loader.exec_module(hc_logic) # type: ignore
find_patient_by_name = hc_logic.find_patient_by_name
find_patient_by_full_name = hc_logic.find_patient_by_full_name
get_patient_profile = hc_logic.get_patient_profile
authenticate_patient = hc_logic.authenticate_patient
get_preferred_pharmacy = hc_logic.get_preferred_pharmacy
list_providers = hc_logic.list_providers
get_provider_slots = hc_logic.get_provider_slots
schedule_appointment = hc_logic.schedule_appointment
triage_symptoms = hc_logic.triage_symptoms
log_call = hc_logic.log_call
@tool
def find_patient(first_name: str | None = None, last_name: str | None = None, full_name: str | None = None) -> str:
"""Find a patient_id by name to use in subsequent tool calls.
WHEN TO CALL: After the caller provides their name, call this tool FIRST before any other tools.
PARAMETERS:
- full_name: Full name like "John Marshall" (PREFERRED - use this if you have it)
- first_name, last_name: Split names if full_name not available
RETURNS: JSON object with either:
- {"patient_id": "pt_abc123", "profile": {...}} if found
- {} if not found (ask caller to verify spelling or provide more info)
NEXT STEP: If patient_id found, call verify_identity() next to authenticate them.
If not found, politely ask the caller to verify their name spelling.
EXAMPLE:
Caller says: "My name is John Marshall"
β Call find_patient(full_name="John Marshall")
β Get {"patient_id": "pt_jmarshall", ...}
β Next call verify_identity(patient_id="pt_jmarshall", ...)
"""
if isinstance(full_name, str) and full_name.strip():
return json.dumps(find_patient_by_full_name(full_name))
return json.dumps(find_patient_by_name(first_name or "", last_name or ""))
@tool
def get_patient_profile_tool(patient_id: str) -> str:
"""Fetch comprehensive patient medical profile including allergies, medications, conditions, and recent visits.
WHEN TO CALL: ONLY after verify_identity() returns verified=true. Call this before giving medical advice.
PARAMETERS:
- patient_id: From find_patient() result (auto-injected if available)
RETURNS: JSON with:
- "profile": {first_name, last_name, dob, phone, email, etc.}
- "allergies": ["Penicillin", ...] - CRITICAL for prescriptions/recommendations
- "medications": [{name, sig, otc}, ...] - Current meds (check before recommending OTC drugs)
- "conditions": ["Hypertension", ...] - Existing conditions
- "recent_visits": [{date, type, reason, outcome}, ...]
- "vitals": {last: {date, bp, hr, temp_f, bmi}}
WHAT TO DO WITH THIS DATA:
- Check allergies before ANY medication recommendations (including OTC)
- Review current medications to avoid duplicates or interactions
- Consider existing conditions when giving advice
- Reference recent visits if relevant to current symptoms
EXAMPLE:
β Call get_patient_profile_tool(patient_id="pt_jmarshall")
β Returns: {"allergies": ["Penicillin"], "medications": [{"name": "Acetaminophen", "sig": "500mg as needed", "otc": true}], ...}
β When giving advice: "Since you're already taking acetaminophen as needed and have a penicillin allergy, I recommend..."
"""
return json.dumps(get_patient_profile(patient_id))
@tool
def verify_identity(session_id: str, patient_id: str | None = None, full_name: str | None = None, dob_yyyy_mm_dd: str | None = None, mrn_last4: str | None = None, secret_answer: str | None = None) -> str:
"""Verify caller identity before accessing medical records. CRITICAL: Identity verification is MANDATORY before any medical information access.
WHEN TO CALL: After find_patient() returns a patient_id. Call this repeatedly until verified=true.
PARAMETERS (collect these from caller):
- session_id: Current session/thread ID (REQUIRED - auto-injected)
- patient_id: From find_patient() result (REQUIRED - auto-injected if available)
- full_name: Caller's full name
- dob_yyyy_mm_dd: Date of birth in ANY format (we normalize it). Examples: "January 1st 1960", "01/01/1960", "1960-01-01"
- mrn_last4: Last 4 digits of Medical Record Number (MRN)
- secret_answer: Answer to the secret question (if question was provided in previous call)
AUTHENTICATION LOGIC:
Caller is verified ONLY if: DOB matches AND (MRN last-4 matches OR secret_answer matches)
RETURNS: JSON with:
- "verified": true/false - Whether identity is confirmed
- "needs": ["dob", "mrn_last4_or_secret"] - List of missing required fields
- "question": "What is your favorite color?" - Secret question to ask caller (if available and mrn_last4 not provided)
CRITICAL SECRET QUESTION FLOW:
1. First call: verify_identity(patient_id="pt_abc", dob_yyyy_mm_dd="1960-01-01")
2. Response: {"verified": false, "needs": ["mrn_last4_or_secret"], "question": "What is your favorite color?"}
3. YOU MUST: Read the question verbatim to the caller: "What is your favorite color?"
4. Collect their answer (e.g., "blue")
5. Second call: verify_identity(patient_id="pt_abc", dob_yyyy_mm_dd="1960-01-01", secret_answer="blue")
6. Response: {"verified": true, "needs": []}
WHAT TO DO:
- If "verified": true β Proceed to get_patient_profile_tool()
- If "verified": false AND "question" present β ASK the question to the caller, collect answer, call verify_identity again with secret_answer
- If "verified": false AND "needs" has items β Ask caller for missing info ONLY, then call verify_identity again
- If verification fails after all info provided β Politely explain you cannot verify identity and cannot proceed
EXAMPLE CONVERSATION:
Agent: "Please confirm your date of birth."
Caller: "January 1st, 1960"
β Call verify_identity(dob_yyyy_mm_dd="January 1st, 1960")
β Returns: {"verified": false, "needs": ["mrn_last4_or_secret"], "question": "What is your favorite color?"}
Agent: "Thank you. For security, what is your favorite color?"
Caller: "Blue"
β Call verify_identity(dob_yyyy_mm_dd="January 1st, 1960", secret_answer="Blue")
β Returns: {"verified": true, "needs": []}
Agent: "Thank you, you're verified. What brings you in today?"
"""
res = authenticate_patient(session_id, patient_id, full_name, dob_yyyy_mm_dd, mrn_last4, secret_answer)
return json.dumps(res)
@tool
def get_preferred_pharmacy_tool(patient_id: str) -> str:
"""Get the patient's preferred pharmacy on file for prescription fulfillment.
WHEN TO CALL: When booking an appointment that may result in a prescription, or if caller asks about pharmacy.
PARAMETERS:
- patient_id: From find_patient() result (auto-injected if available)
RETURNS: JSON with:
- "pharmacy_id": "ph_sc_1010"
- "name": "CVS Pharmacy"
- "address": "1010 El Camino Real, Santa Clara, CA 95050"
- "phone": "+1-408-555-9999"
Or {} if no preferred pharmacy on file.
WHAT TO DO:
- Confirm with patient: "Should we keep your pharmacy at [address] for any prescriptions?"
- If they want to change it, note that for the provider
EXAMPLE:
β Call get_preferred_pharmacy_tool(patient_id="pt_jmarshall")
β Returns: {"name": "CVS Pharmacy", "address": "1010 El Camino Real, Santa Clara, CA"}
β Say: "Should we keep the pharmacy at 1010 El Camino Real in Santa Clara for any prescriptions?"
"""
return json.dumps(get_preferred_pharmacy(patient_id))
@tool
def list_providers_tool(specialty: str | None = None) -> str:
"""List available healthcare providers for appointment booking.
WHEN TO CALL: When ready to book an appointment after triage and patient wants to schedule.
PARAMETERS:
- specialty: Filter by specialty (e.g., "Primary Care", "Urgent Care", "Cardiology"). Leave None for all providers.
RETURNS: JSON array of providers with:
- "provider_id": "prov_smith_md"
- "name": "Dr. Emily Smith"
- "specialty": "Primary Care"
- "credentials": "MD"
WHAT TO DO:
- Present 1-2 options to patient: "I can book you with Dr. Emily Smith, our primary care physician, or Alex Chang, nurse practitioner."
- Don't overwhelm with too many choices
- After patient chooses, call get_provider_slots_tool() to show available times
EXAMPLE:
β Call list_providers_tool(specialty="Primary Care")
β Returns: [{"provider_id": "prov_smith_md", "name": "Dr. Emily Smith", "specialty": "Primary Care"}, ...]
β Say: "I can book you with Dr. Emily Smith. Let me check her availability."
β Next: Call get_provider_slots_tool(provider_id="prov_smith_md")
"""
return json.dumps(list_providers(specialty))
@tool
def get_provider_slots_tool(provider_id: str, count: int = 3) -> str:
"""Get available appointment time slots for a specific provider.
WHEN TO CALL: After patient chooses a provider from list_providers_tool().
PARAMETERS:
- provider_id: From list_providers_tool() result (e.g., "prov_smith_md")
- count: Number of slots to return (default 3, keep it 2-4 for voice conversation)
RETURNS: JSON array of ISO datetime strings like:
- ["2025-10-08T20:00:00", "2025-10-09T08:30:00", "2025-10-09T16:00:00"]
WHAT TO DO:
- Convert times to friendly format: "today at 8pm", "tomorrow at 8:30am", "tomorrow at 4pm"
- Present 2-3 options: "Next openings are today at 8pm, or tomorrow at 8:30am or 4pm. Which works for you?"
- Wait for patient to choose ONE specific time
- After patient chooses, call schedule_appointment_tool() with their chosen slot
EXAMPLE:
β Call get_provider_slots_tool(provider_id="prov_smith_md", count=3)
β Returns: ["2025-10-08T20:00:00", "2025-10-09T08:30:00", "2025-10-09T16:00:00"]
β Say: "Next openings are today at 8pm, or tomorrow at 8:30am or 4pm. Which works for you?"
Caller: "Tomorrow at 8:30am"
β Call schedule_appointment_tool(provider_id="prov_smith_md", slot_iso="2025-10-09T08:30:00")
"""
return json.dumps(get_provider_slots(provider_id, count))
@tool
def schedule_appointment_tool(provider_id: str, slot_iso: str, patient_id: str | None = None) -> str:
"""Book/confirm an appointment slot with a provider for the patient.
WHEN TO CALL: After patient verbally confirms which time slot they want from get_provider_slots_tool().
PARAMETERS:
- provider_id: From list_providers_tool() (e.g., "prov_smith_md")
- slot_iso: EXACT ISO datetime string from get_provider_slots_tool() that patient chose (e.g., "2025-10-09T08:30:00")
- patient_id: From find_patient() result (auto-injected if available)
RETURNS: JSON with:
- "appointment_id": "A-abc12345"
- "provider_id": "prov_smith_md"
- "slot": "2025-10-09T08:30:00"
- "status": "booked"
WHAT TO DO AFTER:
- Confirm to patient: "Booked. I'll send details to your phone ending in [last 4 digits]."
- Ask about pharmacy if appointment may involve prescriptions: call get_preferred_pharmacy_tool()
- At end of call, call log_call_tool() to document the visit
EXAMPLE:
Caller chose: "Tomorrow at 8:30am"
β Call schedule_appointment_tool(provider_id="prov_smith_md", slot_iso="2025-10-09T08:30:00")
β Returns: {"appointment_id": "A-abc12345", "status": "booked"}
β Say: "Booked. I'll send details to your phone. Should we keep your pharmacy at [address]?"
"""
return json.dumps(schedule_appointment(provider_id, slot_iso, patient_id))
@tool
def triage_symptoms_tool(patient_id: str | None, symptoms_text: str) -> str:
"""Analyze patient symptoms using clinical triage rules to determine urgency and guidance.
WHEN TO CALL: ONLY after thorough symptom assessment. Ask clarifying questions about red flags BEFORE calling this tool.
CRITICAL: This tool uses simple keyword matching, so be VERY CAREFUL with your symptoms_text.
- Only include symptoms that ARE PRESENT
- Do NOT mention symptoms that are absent (saying "no numbness" will trigger the "numbness" keyword!)
- Instead, after screening for red flags, ONLY list positive findings in symptoms_text
- Use descriptive language: "mild headache for 2 days, gradual onset, no concerning features"
- If patient denies all red flags, do NOT list them - just describe the actual complaint
PARAMETERS:
- patient_id: From find_patient() result (auto-injected if available, used for age-based rules)
- symptoms_text: Description of PRESENT symptoms only (DO NOT list absent symptoms to avoid false triggers)
Good: "mild headache for 2 days, gradual onset, relieved by rest"
Good: "moderate headache with fever 101F, started yesterday"
Bad: "headache, no numbness, no confusion" (will trigger "numbness" and "confusion" keywords!)
Bad: "headache" (too vague, lacks detail for proper triage)
RETURNS: JSON with:
- "risk": "urgent" | "soon" | "self_care" - Urgency level
- "advice": "Try rest, hydration..." - Clinical guidance to share with patient
- "red_flags": ["stiff neck", "high fever"] - Keywords detected (may include false positives!)
- "rule": "Headache - typical" - Internal rule name that matched
RISK LEVELS:
- "urgent": Potential emergency (but verify with clinical judgment)
- "soon": Schedule appointment within 1-2 days
- "self_care": Home care with OTC medications, monitor symptoms
WHAT TO DO WITH RESULTS (USE CLINICAL JUDGMENT):
- If risk="urgent" AND red_flags has items AND patient confirmed those symptoms: Direct to ER/911
- If risk="urgent" BUT patient explicitly denied red flag symptoms: FALSE POSITIVE - schedule appointment instead
- If risk="soon": Give advice and offer appointment within 1-2 days
- If risk="self_care": Give advice, check allergies/meds for safety, offer optional follow-up
- ALWAYS tailor advice based on patient's allergies and current medications from get_patient_profile_tool()
- Remember: Most common symptoms (headache, fever, fatigue) are NOT emergencies
EXAMPLE 1 (TRUE URGENT):
Conversation: Patient says "severe crushing chest pain, sweating, short of breath"
β Call triage_symptoms_tool(symptoms_text="severe chest pain with sweating and shortness of breath")
β Returns: {"risk": "urgent", "red_flags": ["chest pain"]}
β Clinical judgment: Patient confirmed severe chest pain = TRUE URGENT
β Say: "This sounds serious. Please call 911 now or go to the nearest emergency room."
EXAMPLE 2 (AVOIDING FALSE POSITIVES):
Conversation: You ask "Any severe symptoms like confusion, weakness, or numbness?" Patient says "No, none of those"
β Call triage_symptoms_tool(symptoms_text="mild headache for 2 days, gradual onset, relieved with rest")
β Returns: {"risk": "self_care", "red_flags": []}
β Say: "Try rest, hydration, and acetaminophen. Would you like a follow-up appointment?"
(Note: Did NOT mention "no confusion, no numbness" to avoid triggering those keywords)
EXAMPLE 3 (SELF-CARE):
β Call triage_symptoms_tool(symptoms_text="low-grade fever 100.5F for 1 day with mild fatigue")
β Returns: {"risk": "self_care", "advice": "Hydration, rest, and acetaminophen can help..."}
β Say: "For a low-grade fever, rest and hydration are key. You're already taking acetaminophen as needed, which is safe with your medications."
"""
return json.dumps(triage_symptoms(patient_id, symptoms_text))
@tool
def log_call_tool(session_id: str, patient_id: str | None = None, notes: str | None = None, triage_json: str | None = None) -> str:
"""Log the call encounter details, symptoms, triage outcome, and advice provided for medical records.
WHEN TO CALL: At the END of the call, after all guidance provided and appointments scheduled.
PARAMETERS:
- session_id: Current session/thread ID (REQUIRED - auto-injected)
- patient_id: From find_patient() result (auto-injected if available)
- notes: Brief summary of the call in plain text (e.g., "Patient called with headache and fatigue. No red flags. Advised rest and hydration. Appointment scheduled for tomorrow 8:30am with Dr. Smith.")
- triage_json: JSON string of triage_symptoms_tool() output (pass the entire JSON result as a string)
RETURNS: JSON with:
- "logged": true
- "log_id": "L-abc12345"
WHAT TO DO:
- Call this as the final step before ending the call
- Include key details: symptoms reported, advice given, appointments booked, pharmacy confirmed
- No need to tell the patient you're logging it, just do it silently
EXAMPLE:
After full call with symptom discussion and appointment booking:
β Call log_call_tool(
notes="Patient reported mild headache and fatigue. No red flags. Has penicillin allergy and takes acetaminophen PRN. Advised rest, hydration, acetaminophen as needed. Booked appointment tomorrow 8:30am with Dr. Smith. Pharmacy confirmed at CVS Santa Clara.",
triage_json='{"risk": "self_care", "advice": "Try rest and hydration...", "red_flags": []}'
)
β Returns: {"logged": true, "log_id": "L-abc12345"}
"""
triage: Dict[str, Any] | None
try:
triage = json.loads(triage_json or "null") if triage_json else None
except Exception:
triage = None
return json.dumps(log_call(session_id, patient_id, notes, triage))
|