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))