Files
aionboarding/ai-patient-onboarding/app/ai/orchestrator.py
T

1114 lines
60 KiB
Python

import json
import re
import os
from typing import Dict, Any, List, Optional
from pydantic import ValidationError
from app.config import OPENAI_API_KEY, GEMINI_API_KEY, GEMINI_MODEL
from app.ai.openai_client import OpenAIClient
from app.ai.gemini_client import GeminiClient
from app.schemas.onboarding import AIOnboardingResponse
from app.services.contextual_normalizer import (
normalize_duration_from_message,
detect_previous_question_intent,
is_duration_expression,
is_severity_expression,
is_location_expression,
is_negative_expression,
is_casual_filler,
extract_severity_from_message,
extract_location_from_message,
)
from app.ai.prompt_rules.safety_rules import (
EMERGENCY_KEYWORDS_EN,
EMERGENCY_KEYWORDS_INDIC,
CRISIS_KEYWORDS_EN,
CRISIS_KEYWORDS_INDIC,
)
SPECIALTY_RULES = [
{
"code": "psychiatry",
"name": "Psychiatry & Mental Health Care",
"keywords": [
"depression", "depressed", "mental", "anxiety", "panic", "stress", "sadness", "suicidal", "suicide", "sleep", "insomnia", "emotional", "mood", "lonely", "hopeless",
"மன அழுத்தம்", "மனச்சோர்வு", "தூக்கமின்மை", "கவலை", "உணர்ச்சி", "தற்கொலை", "டிப்ரெஷன்", "டிப்ரஷன்",
"डिप्रेशन", "तनाव", "चिंता", "उदासी", "नींद न आना", "आत्महत्या",
"మానసిక ఒత్తిడి", "డిప్రెషన్", "నిద్రలేమి",
"ಮನೋಒತ್ತಡ", "ಖಿನ್ನತೆ", "ನಿದ್ರಾಹೀನತೆ"
]
},
{
"code": "cardiology",
"name": "Cardiology (Heart Care)",
"keywords": [
"chest pain", "heart", "palpitation", "breathless", "cardiac", "chest pressure",
"நெஞ்சு வலி", "இருதய வலி", "மூச்சு திணறல்", "படபடப்பு", "படபடன்னு", "நெஞ்சு படபடன்னு", "நெஞ்சு படபடப்பு",
"छाती में दर्द", "दिल की धड़कन", "గుండె నొప్పి"
]
},
{
"code": "dermatology",
"name": "Dermatology (Skin & Hair Care)",
"keywords": [
"skin", "rash", "itching", "eczema", "acne", "pimples", "fungal", "allergy",
"தோல் அரிப்பு", "தோலில் அரிப்பு", "தழும்பு", "படை", "கொப்புளம்", "அரிப்பு", "त्वचा में खुजली", "दाद", "पिंपल्स"
]
},
{
"code": "neurology",
"name": "Neurology (Brain & Nervous System)",
"keywords": [
"headache", "dizziness", "seizure", "stroke", "numbness", "paralysis", "migraine",
"தலைவலி", "மயக்கம்", "பக்கவாதம்", "வலிப்பு", "கை கால் மரத்துப்போதல்", "தலை சுத்துது", "தலைசுற்றல்", "தலை பாரம்", "सिरदर्द", "चक्कर", "दौरा"
]
},
{
"code": "orthopedics",
"name": "Orthopedics (Bone & Joint Care)",
"keywords": [
"knee", "joint pain", "bone", "fracture", "back pain", "shoulder", "arthritis", "sprain", "swelling",
"மூட்டு வலி", "இடுப்பு வலி", "முதுகு வலி", "எலும்பு முறிவு", "घुटने का दर्द", "जोड़ों का दर्द", "हड्डी"
]
},
{
"code": "gastroenterology",
"name": "Gastroenterology (Stomach & Digestive Care)",
"keywords": [
"stomach pain", "stomach ache", "acidity", "vomiting", "diarrhea", "indigestion", "gastric", "loose motion", "constipation",
"வயிற்று வலி", "வாந்தி", "அஜீரணம்", "வயிற்றுப்போக்கு", "गैस", "पेट दर्द", "उल्टी", "दस्त"
]
},
{
"code": "pulmonology",
"name": "Pulmonology (Lungs & Respiratory)",
"keywords": [
"cough", "asthma", "wheezing", "phlegm", "shortness of breath", "cold", "flu",
"இருமல்", "சளி", "ஆஸ்துமா", "இளைப்பு", "खांसी", "दमा", "कफ", "सांस फूलना"
]
},
{
"code": "general_medicine",
"name": "General Medicine",
"keywords": [
"fever", "weakness", "body pain", "fatigue", "tired", "chills", "weight loss", "diabetes", "bp", "sugar",
"காய்ச்சல்", "உடல் வலி", "உடம்பு வலி", "சோர்வு", "குளிர்", "बुखार", "कमजोरी", "बदन दर्द"
]
}
]
def classify_specialty_from_symptoms(text: str) -> Dict[str, str]:
if not text:
return {"code": "general_medicine", "name": "General Medicine"}
t_lower = text.lower()
for rule in SPECIALTY_RULES:
for kw in rule["keywords"]:
if kw.lower() in t_lower:
return {"code": rule["code"], "name": rule["name"]}
return {"code": "general_medicine", "name": "General Medicine"}
def validate_ai_response(data: dict) -> AIOnboardingResponse:
try:
return AIOnboardingResponse.model_validate(data)
except ValidationError:
updates = data.get("state_updates", {})
if not isinstance(updates, dict):
updates = {}
cleaned = {
"reply": str(data.get("reply", "சரிங்க, மேலும் விவரங்களை சொல்லுங்க.")),
"next_stage": str(data.get("next_stage", "symptom_details")),
"state_updates": updates,
"completed": bool(data.get("completed", False)),
"suggested_specialty": data.get("suggested_specialty"),
}
return AIOnboardingResponse.model_validate(cleaned)
CANONICAL_SYMPTOM_MAP = {
"தலைவலி": "headache",
"தலை வலி": "headache",
"தலை பாரம்": "headache",
"सिरदर्द": "headache",
"காய்ச்சல்": "fever",
"சுரம்": "fever",
"बुखार": "fever",
"வயிற்று வலி": "stomach pain",
"வாந்தி": "vomiting",
"उल्टी": "vomiting",
"இருமல்": "cough",
"खांसी": "cough",
"சளி": "cold",
"நெஞ்சு வலி": "chest pain",
"छाती में दर्द": "chest pain",
"மூச்சு திணறல்": "breathlessness",
"மூச்சு வாங்குது": "breathlessness",
"கை கால் வலி": "body pain",
"உடல் வலி": "body pain",
"உடம்பு வலி": "body pain",
"बदन दर्द": "body pain",
"தலைச்சுற்றல்": "dizziness",
"தலை சுத்துது": "dizziness",
"चक्कर": "dizziness",
"நெஞ்சு படபடப்பு": "palpitations",
"படபடப்பு": "palpitations",
"படபடன்னு": "palpitations",
"தோல் அரிப்பு": "skin rash",
"அரிப்பு": "skin rash",
"தூக்கமின்மை": "insomnia",
"மன அழுத்தம்": "stress",
}
def _normalize_key(name: str) -> str:
if not name:
return ""
cleaned = name.strip().lower()
for k, canonical in CANONICAL_SYMPTOM_MAP.items():
if k in cleaned or cleaned in k:
return canonical
return cleaned
def merge_state_updates(existing_state: Dict[str, Any], new_updates: Dict[str, Any]) -> Dict[str, Any]:
merged = dict(existing_state)
if not isinstance(new_updates, dict):
return merged
# 1. reason_for_visit
rfv = new_updates.get("reason_for_visit")
if rfv is not None and rfv != "":
merged["reason_for_visit"] = rfv
# 2. symptoms
new_symptoms = new_updates.get("symptoms")
if new_symptoms:
if isinstance(new_symptoms, dict):
new_symptoms = new_symptoms.get("items", [])
if isinstance(new_symptoms, list):
existing_symptoms = merged.get("symptoms", [])
if isinstance(existing_symptoms, dict):
existing_symptoms = existing_symptoms.get("items", [])
if not isinstance(existing_symptoms, list):
existing_symptoms = []
sym_map = {}
for item in existing_symptoms:
if isinstance(item, dict):
key = _normalize_key(item.get("name", ""))
if key:
sym_map[key] = dict(item)
else:
sym_map[f"_unnamed_{len(sym_map)}"] = dict(item)
for new_item in new_symptoms:
if isinstance(new_item, dict):
new_name = new_item.get("name")
if new_name:
key = _normalize_key(new_name)
if key in sym_map:
for attr, val in new_item.items():
if val is not None and val != "":
sym_map[key][attr] = val
else:
sym_map[key] = dict(new_item)
else:
# Contextual attribute for the primary symptom
primary_keys = list(sym_map.keys())
if primary_keys:
target_key = primary_keys[0]
for attr, val in new_item.items():
if val is not None and val != "":
sym_map[target_key][attr] = val
else:
sym_map[f"_unnamed_{len(sym_map)}"] = dict(new_item)
merged["symptoms"] = list(sym_map.values())
# 3. medical_history
new_hist = new_updates.get("medical_history")
if new_hist:
if isinstance(new_hist, dict):
if new_hist.get("status") == "confirmed_none":
existing_curr = merged.get("medical_history")
existing_items = existing_curr.get("items", []) if isinstance(existing_curr, dict) else (existing_curr or [])
if not existing_items:
merged["medical_history"] = {"status": "confirmed_none", "items": []}
else:
new_hist = new_hist.get("items", [])
if isinstance(new_hist, list):
existing_hist = merged.get("medical_history", [])
if isinstance(existing_hist, dict):
existing_hist = existing_hist.get("items", [])
if not isinstance(existing_hist, list):
existing_hist = []
h_map = {}
for item in existing_hist:
if isinstance(item, dict):
c = item.get("condition", "").lower().strip()
if c:
h_map[c] = dict(item)
elif isinstance(item, str) and item.strip():
h_map[item.lower().strip()] = {"condition": item.strip()}
for item in new_hist:
if isinstance(item, dict):
c = item.get("condition", "").lower().strip()
if c in h_map:
for attr, val in item.items():
if val is not None and val != "":
h_map[c][attr] = val
elif c:
h_map[c] = dict(item)
elif isinstance(item, str) and item.strip():
c = item.lower().strip()
if c not in h_map:
h_map[c] = {"condition": item.strip()}
merged["medical_history"] = list(h_map.values())
# 4. medications
new_meds = new_updates.get("medications")
if new_meds:
if isinstance(new_meds, dict):
if new_meds.get("status") == "confirmed_none":
existing_curr = merged.get("medications")
existing_items = existing_curr.get("items", []) if isinstance(existing_curr, dict) else (existing_curr or [])
if not existing_items:
merged["medications"] = {"status": "confirmed_none", "items": []}
else:
new_meds = new_meds.get("items", [])
if isinstance(new_meds, list):
existing_meds = merged.get("medications", [])
if isinstance(existing_meds, dict):
existing_meds = existing_meds.get("items", [])
if not isinstance(existing_meds, list):
existing_meds = []
m_map = {}
for item in existing_meds:
if isinstance(item, dict):
n = item.get("name", "").lower().strip()
if n:
m_map[n] = dict(item)
elif isinstance(item, str) and item.strip():
m_map[item.lower().strip()] = {"name": item.strip()}
for item in new_meds:
if isinstance(item, dict):
n = item.get("name", "").lower().strip()
if n in m_map:
for attr, val in item.items():
if val is not None and val != "":
m_map[n][attr] = val
elif n:
m_map[n] = dict(item)
elif isinstance(item, str) and item.strip():
n = item.lower().strip()
if n not in m_map:
m_map[n] = {"name": item.strip()}
merged["medications"] = list(m_map.values())
# 5. allergies
new_algs = new_updates.get("allergies")
if new_algs:
if isinstance(new_algs, dict):
if new_algs.get("status") == "confirmed_none":
existing_curr = merged.get("allergies")
existing_items = existing_curr.get("items", []) if isinstance(existing_curr, dict) else (existing_curr or [])
if not existing_items:
merged["allergies"] = {"status": "confirmed_none", "items": []}
else:
new_algs = new_algs.get("items", [])
if isinstance(new_algs, list):
existing_algs = merged.get("allergies", [])
if isinstance(existing_algs, dict):
existing_algs = existing_algs.get("items", [])
if not isinstance(existing_algs, list):
existing_algs = []
a_map = {}
for item in existing_algs:
if isinstance(item, dict):
n = item.get("name", "").lower().strip()
if n:
a_map[n] = dict(item)
elif isinstance(item, str) and item.strip():
a_map[item.lower().strip()] = {"name": item.strip()}
for item in new_algs:
if isinstance(item, dict):
n = item.get("name", "").lower().strip()
if n in a_map:
for attr, val in item.items():
if val is not None and val != "":
a_map[n][attr] = val
elif n:
a_map[n] = dict(item)
elif isinstance(item, str) and item.strip():
n = item.lower().strip()
if n not in a_map:
a_map[n] = {"name": item.strip()}
merged["allergies"] = list(a_map.values())
# 6. surgical_history, previous_episodes, family_history, additional_information
for key in ["surgical_history", "previous_episodes", "family_history", "additional_information"]:
val = new_updates.get(key)
if val:
existing_list = merged.get(key, [])
if not isinstance(existing_list, list):
existing_list = [existing_list] if existing_list else []
if isinstance(val, list):
merged[key] = existing_list + [v for v in val if v not in existing_list]
else:
if val not in existing_list:
merged[key] = existing_list + [val]
return merged
def _field_status(field_val) -> str:
if field_val is None:
return "not_collected"
if isinstance(field_val, dict):
status = field_val.get("status")
if status in ["reported", "confirmed_none"]:
return status
items = field_val.get("items", [])
return "reported" if items else "not_collected"
if isinstance(field_val, list):
return "reported" if len(field_val) > 0 else "not_collected"
if isinstance(field_val, str) and field_val.strip():
return "reported"
return "not_collected"
def _field_has_items(field_val) -> bool:
"""Backward-compatible helper: returns True if the field has at least one item."""
return _field_status(field_val) == "reported"
def _any_symptom_has_duration(symptoms) -> bool:
"""Backward-compatible helper: returns True if any symptom has a duration."""
if isinstance(symptoms, dict):
symptoms = symptoms.get("items", [])
if not isinstance(symptoms, list):
return False
return any(
bool(s.get("duration") or s.get("onset") or s.get("onset_and_severity"))
for s in symptoms if isinstance(s, dict)
)
def check_required_missing(state: Dict[str, Any]) -> List[str]:
missing = []
# 1. Reason for visit / Chief complaint
rfv = state.get("reason_for_visit")
if _field_status(rfv) == "not_collected":
missing.append("reason_for_visit")
# 2. Symptoms
syms = state.get("symptoms")
syms_list = syms.get("items", []) if isinstance(syms, dict) else (syms if isinstance(syms, list) else [])
if not syms_list:
missing.append("symptoms")
else:
# Check duration on at least the primary symptom
has_duration = any(
bool(s.get("duration") or s.get("onset") or s.get("onset_and_severity"))
for s in syms_list if isinstance(s, dict)
)
if not has_duration:
missing.append("duration")
# 3. Medical history
mh = state.get("medical_history")
if _field_status(mh) == "not_collected":
missing.append("medical_history")
# 4. Medications
meds = state.get("medications")
if _field_status(meds) == "not_collected":
missing.append("medications")
# 5. Allergies
algs = state.get("allergies")
if _field_status(algs) == "not_collected":
missing.append("allergies")
return missing
def _stage_for_missing(missing_field: str) -> str:
mapping = {
"reason_for_visit": "reason_for_visit",
"symptoms": "symptom_collection",
"duration": "symptom_details",
"medical_history": "medical_history",
"medications": "medications",
"allergies": "allergies",
}
return mapping.get(missing_field, "symptom_details")
def _pick_best_question(missing_list: List[str], lang: str, is_psych: bool = False) -> str:
if not missing_list:
if lang.startswith("ta"):
return "சரிங்க, எல்லா விவரத்தையும் டாக்டர்கிட்ட குடுத்துடுறேன். ரொம்ப நன்றிங்க!"
elif lang.startswith("hi"):
return "ठीक है जी, मैं यह सारी जानकारी डॉक्टर साहब को दे देती हूँ। शुक्रिया!"
elif lang.startswith("te"):
return "చాలా ధన్యవాదాలు! మీ వివరాలన్నీ డాక్టర్ గారికి అందిస్తాను."
elif lang.startswith("kn"):
return "ಸರಿ, ಈ ಎಲ್ಲಾ ವಿವರಗಳನ್ನು ಡಾಕ್ಟರ್‌ಗೆ ತಿಳಿಸ್ತೀನಿ. ಧನ್ಯವಾದಗಳು!"
else:
return "Got it. I have compiled all these details for the doctor. Thank you!"
field = missing_list[0]
# Tamil
if lang.startswith("ta"):
if field == "reason_for_visit" or field == "symptoms":
return "வணக்கம், உங்களுக்கு என்ன உடம்பு பிரச்சனை இருக்குன்னு சொல்ல முடியுமா?"
elif field == "duration":
if is_psych:
return "புரியுதுங்க. இந்த மன அழுத்தம் அல்லது கவலை எப்போலிருந்து இருக்குன்னு சொல்ல முடியுமா?"
return "சரிங்க, இந்த பிரச்சனை எத்தனை நாளா இருக்குன்னு சொல்ல முடியுமா?"
elif field == "medical_history":
if is_psych:
return "புரியுதுங்க. இதுக்கு முன்னாடி மன அழுத்தம் அல்லது தூக்கமின்மைக்காக சிகிச்சை எடுத்திருக்கீங்களா?"
return "சரிங்க. உங்களுக்கு பிபி, சுகர், ஆஸ்துமா போன்ற வேறு ஏதேனும் உடம்பு பிரச்சனை இருக்கா?"
elif field == "medications":
return "சரிங்க, இதுக்காக இல்ல வேறு ஏதேனும் பிரச்சனைக்காக வழக்கமா மாத்திரை சாப்பிடுறீங்களா?"
elif field == "allergies":
return "புரியுதுங்க. உங்களுக்கு ஏதேனும் மருந்து அல்லது உணவு அலர்ஜி இருக்கா?"
# Hindi
elif lang.startswith("hi"):
if field == "reason_for_visit" or field == "symptoms":
return "नमस्ते, आपको क्या स्वास्थ्य समस्या हो रही है?"
elif field == "duration":
return "समझ गया जी, यह तकलीफ कितने दिनों से हो रही है?"
elif field == "medical_history":
return "जी ठीक है, क्या आपको बीपी, शुगर या अस्थामा जैसी कोई पुरानी बीमारी है?"
elif field == "medications":
return "समझ गया, क्या आप नियमित रूप से कोई दवाई लेते हैं?"
elif field == "allergies":
return "अच्छा जी, आपको किसी दवाई या खाने से एलर्जी तो नहीं है?"
# Telugu
elif lang.startswith("te"):
if field == "reason_for_visit" or field == "symptoms":
return "నమస్కారం, మీ ఆరోగ్య సమస్య ఏమిటో చెప్పగలరా?"
elif field == "duration":
return "అర్థమైందండి, ఈ ఇబ్బంది ఎన్ని రోజుల నుండి ఉంది?"
elif field == "medical_history":
return "అర్థమైంది, మీకు బీపీ, షుగర్ లేదా ఆస్తమా వంటి పాత ఆరోగ్య సమస్యలు ఉన్నాయా?"
elif field == "medications":
return "సరేనండి, మీరు క్రమం తప్పకుండా ఏమైనా మందులు వాడుతున్నారా?"
elif field == "allergies":
return "మీకు ఏదైనా మందుల అలర్జీ ఉందా?"
# Kannada
elif lang.startswith("kn"):
if field == "reason_for_visit" or field == "symptoms":
return "ನಮಸ್ಕಾರ, ನಿಮ್ಮ ಆರೋಗ್ಯ ಸಮಸ್ಯೆ ಏನು ಎಂದು ತಿಳಿಸುವಿರಾ?"
elif field == "duration":
return "ಅರ್ಥವಾಯಿತು, ಈ ತೊಂದರೆ ಎಷ್ಟು ದಿನದಿಂದ ಇದೆ?"
elif field == "medical_history":
return "ತಿಳಿಯಿತು, ನಿಮಗೆ ಬಿಪಿ, ಶುಗರ್ ಅಥವಾ ಅಸ್ತಮಾದಂತಹ ಹಳೆಯ ತೊಂದರೆ ಇದೆಯಾ?"
elif field == "medications":
return "ಸರಿ, ನೀವು ನಿಯಮಿತವಾಗಿ ಯಾವುದೇ ಮಾತ್ರೆಗಳನ್ನು ತೆಗೆದುಕೊಳ್ಳುತ್ತಿದ್ದೀರಾ?"
elif field == "allergies":
return "ನಿಮಗೆ ಯಾವುದೇ ಮಾತ್ರೆ ಅಥವಾ ಔಷಧಿಯ ಅಲರ್ಜಿ ಇದೆಯಾ?"
# English Default
if field == "reason_for_visit" or field == "symptoms":
return "Hello, could you please describe what symptoms or health concerns you are experiencing?"
elif field == "duration":
return "Got it. How long have you had these symptoms?"
elif field == "medical_history":
if is_psych:
return "Understood. Have you previously received counseling or treatment for similar concerns?"
return "Got it. Do you have a history of diabetes, high blood pressure, asthma, or other chronic conditions?"
elif field == "medications":
return "Understood. Are you currently taking any prescription medications or daily tablets?"
elif field == "allergies":
return "Understood. Do you have any known medication or food allergies?"
return "Could you provide any additional details about your symptoms?"
class AIOrchestrator:
def __init__(self):
self.openai = OpenAIClient()
self.gemini = GeminiClient()
async def process(
self,
session,
state,
conversation_history,
user_message: str,
) -> Dict[str, Any]:
specialty = {
"code": getattr(session, "category_code", "general_medicine") or "general_medicine",
"name": getattr(session, "category_name", "General Medicine") or "General Medicine",
}
# ── EMERGENCY / CRISIS OVERRIDE (Executed first before anything else) ──
msg_lower = (user_message or "").lower()
msg_raw = user_message or ""
lang = (session.language or "ta-IN").lower()
is_crisis = (
any(k in msg_lower for k in CRISIS_KEYWORDS_EN)
or any(k in msg_raw for k in CRISIS_KEYWORDS_INDIC)
)
is_emergency = (
any(k in msg_lower for k in EMERGENCY_KEYWORDS_EN)
or any(k in msg_raw for k in EMERGENCY_KEYWORDS_INDIC)
)
if is_crisis:
if lang.startswith("ta"):
reply = "உங்கள் மன நிலை எங்களுக்கு புரிகிறது. உடனடியாக உதவி பெற Tele-MANAS (14416) அல்லது 108 என்ற இலவச எண்ணை அழைக்கவும்."
elif lang.startswith("hi"):
reply = "आपकी परेशानी हम समझ सकते हैं। कृपया तुरंत मदद के लिए Tele-MANAS (14416) या 108 पर कॉल करें।"
elif lang.startswith("te"):
reply = "దయచేసి ఆందోళన చెందకండి. తక్షణ సహాయం కోసం Tele-MANAS (14416) లేదా 108 కి కాల్ చేయండి."
elif lang.startswith("kn"):
reply = "ದಯವಿಟ್ಟು ಧೈರ್ಯವಾಗಿರಿ. ತುರ್ತು ನೆರವಿಗಾಗಿ Tele-MANAS (14416) ಅಥವಾ 108 ಗೆ ಕರೆ ಮಾಡಿ."
else:
reply = "We care about your safety. Please immediately reach out to the 24/7 Tele-MANAS mental health helpline at 14416 or call 108."
return {
"reply": reply,
"next_stage": "confirmation",
"state_updates": {"additional_information": [{"category": "crisis_alert", "value": user_message}]},
"completed": True,
"suggested_specialty": {"code": "psychiatry", "name": "Psychiatry & Mental Health Care"},
}
if is_emergency:
if lang.startswith("ta"):
reply = "இதற்கு உடனடியாக தீவிர மருத்துவ சிகிச்சை தேவைப்படலாம். தயவுசெய்து உடனே 108 அவசர ஆம்புலன்ஸை அழைக்கவும் அல்லது அவசர சிகிச்சை பிரிவுக்கு செல்லவும்."
elif lang.startswith("hi"):
reply = "इसके लिए तुरंत आपातकालीन चिकित्सा की आवश्यकता हो सकती है। कृपया तुरंत 108 एम्बुलेंस बुलाएं या नजदीकी अस्पताल जाएं।"
elif lang.startswith("te"):
reply = "దీనికి తక్షణ వైద్య సహాయం అవసరం కావచ్చు. దయచేసి వెంటనే 108 కి కాల్ చేయండి."
elif lang.startswith("kn"):
reply = "ಇದಕ್ಕೆ ತಕ್ಷಣದ ಚಿಕಿತ್ಸೆ ಅಗತ್ಯವಿದೆ. ದಯವಿಟ್ಟು ಕೂಡಲೇ 108 ಆಂಬುಲೆನ್ಸ್‌ಗೆ ಕರೆ ಮಾಡಿ."
else:
reply = "This may require immediate medical attention. Please call 108 for an ambulance or visit the nearest emergency room immediately."
return {
"reply": reply,
"next_stage": "confirmation",
"state_updates": {"additional_information": [{"category": "emergency_alert", "value": user_message}]},
"completed": True,
"suggested_specialty": {"code": "cardiology", "name": "Cardiology (Heart Care)"},
}
current_state = json.loads(state.state_json) if hasattr(state, "state_json") and state.state_json else {}
# Auto-detect / classify specialty from user message
suggested_specialty = classify_specialty_from_symptoms(user_message)
# Only upgrade specialty, never downgrade from a specific specialty to general_medicine
if suggested_specialty["code"] == "general_medicine" and specialty["code"] != "general_medicine":
suggested_specialty = specialty
elif suggested_specialty["code"] != "general_medicine" and specialty["code"] == "general_medicine":
specialty = suggested_specialty
last_ai_message = None
for m in reversed(conversation_history or []):
if m.get("role") in ["assistant", "ai"]:
last_ai_message = m.get("message")
break
# ── STRATEGY: Try Gemini LLM for natural contextual response, fall back to rule-based ──
gemini_result = None
try:
gemini_result = await self._try_gemini_response(
session, specialty, user_message, current_state, conversation_history, suggested_specialty
)
except Exception as e:
print(f"[AIOrchestrator] Gemini LLM call failed, using rule-based fallback: {e}")
if gemini_result:
# ── HYBRID: Use Gemini's reply text, but validate state with deterministic extraction ──
deterministic = self._simulate_specialty_response(
session, specialty, user_message, current_state,
len(conversation_history or []), suggested_specialty,
last_ai_message=last_ai_message,
)
# Use Gemini's natural reply, but trust deterministic state_updates and completed flag
gemini_reply = gemini_result.get("reply", "")
gemini_state = gemini_result.get("state_updates", {})
det_state = deterministic.get("state_updates", {})
# Merge: deterministic extraction is the safety net, Gemini can add extras
final_state = dict(det_state)
for key in ["additional_information", "surgical_history", "previous_episodes", "family_history"]:
gemini_val = gemini_state.get(key)
if gemini_val and not final_state.get(key):
final_state[key] = gemini_val
# For core fields, prefer Gemini if it extracted something deterministic missed
for key in ["symptoms", "medical_history", "medications", "allergies"]:
g_val = gemini_state.get(key)
d_val = final_state.get(key)
if g_val and not d_val:
final_state[key] = g_val
parsed = {
"reply": gemini_reply,
"next_stage": deterministic.get("next_stage", "symptom_details"),
"state_updates": final_state,
"completed": deterministic["completed"], # Trust deterministic completion
}
else:
# ── FALLBACK: Pure rule-based response ──
parsed = self._simulate_specialty_response(
session, specialty, user_message, current_state,
len(conversation_history or []), suggested_specialty,
last_ai_message=last_ai_message,
)
parsed["suggested_specialty"] = suggested_specialty
validated = validate_ai_response(parsed)
res_dict = validated.model_dump()
res_dict["suggested_specialty"] = suggested_specialty
return res_dict
async def _try_gemini_response(
self,
session,
specialty: dict,
user_message: str,
current_state: dict,
conversation_history: list,
suggested_specialty: dict,
) -> Optional[Dict[str, Any]]:
"""
Try to get a natural, contextual response from Gemini LLM.
Returns parsed JSON dict on success, None on failure.
"""
from app.ai.prompts import build_system_prompt
api_key = self.gemini.api_key
if not api_key or api_key in ["your_gemini_key", "your_gemini_api_key", ""]:
return None
patient = {
"name": getattr(session, "first_name", "Patient"),
"age": getattr(session, "age", ""),
"gender": getattr(session, "gender", ""),
}
context = {
"specialty_config": specialty,
"patient": patient,
"language": session.language or "ta-IN",
"current_stage": getattr(session, "stage", "greeting"),
"current_state": current_state,
"conversation_history": conversation_history or [],
"new_patient_message": user_message,
}
system_prompt = build_system_prompt(context)
raw_text = await self.gemini.generate_response(system_prompt, user_message)
# Parse JSON from Gemini response
cleaned = raw_text.strip()
if cleaned.startswith("```"):
cleaned = cleaned.split("\n", 1)[-1]
if cleaned.endswith("```"):
cleaned = cleaned[:-3]
cleaned = cleaned.strip()
parsed = json.loads(cleaned)
# Validate it has required keys
if "reply" not in parsed or not isinstance(parsed.get("reply"), str):
return None
if "state_updates" not in parsed or not isinstance(parsed.get("state_updates"), dict):
parsed["state_updates"] = {}
if "completed" not in parsed:
parsed["completed"] = False
return parsed
def _simulate_specialty_response(
self,
session,
specialty: dict,
msg: str,
current_state: dict,
turn_count: int,
suggested_specialty: dict = None,
last_ai_message: str = None,
) -> dict:
lang = (session.language or "ta-IN").lower()
msg_lower = msg.lower()
# ── STEP 1: Emergency / Crisis detection ──
is_crisis = any(k in msg_lower for k in CRISIS_KEYWORDS_EN) or any(k in msg for k in CRISIS_KEYWORDS_EN) or any(k in msg for k in CRISIS_KEYWORDS_INDIC)
is_emergency = any(k in msg_lower for k in EMERGENCY_KEYWORDS_EN) or any(k in msg for k in EMERGENCY_KEYWORDS_INDIC)
if is_crisis:
if lang.startswith("ta"):
reply = "உங்கள் மன நிலை எங்களுக்கு புரிகிறது. உடனடியாக உதவி பெற Tele-MANAS (14416) அல்லது 108 என்ற இலவச எண்ணை அழைக்கவும்."
elif lang.startswith("hi"):
reply = "आपकी परेशानी हम समझ सकते हैं। कृपया तुरंत मदद के लिए Tele-MANAS (14416) या 108 पर कॉल करें।"
elif lang.startswith("te"):
reply = "దయచేసి ఆందోళన చెందకండి. తక్షణ సహాయం కోసం Tele-MANAS (14416) లేదా 108 కి కాల్ చేయండి."
elif lang.startswith("kn"):
reply = "ದಯವಿಟ್ಟು ಧೈರ್ಯವಾಗಿರಿ. ತುರ್ತು ನೆರವಿಗಾಗಿ Tele-MANAS (14416) ಅಥವಾ 108 ಗೆ ಕರೆ ಮಾಡಿ."
else:
reply = "We care about your safety. Please immediately reach out to the 24/7 Tele-MANAS mental health helpline at 14416 or call 108."
return {
"reply": reply,
"next_stage": "confirmation",
"state_updates": {"additional_information": [{"category": "crisis_alert", "value": msg}]},
"completed": True,
}
if is_emergency:
if lang.startswith("ta"):
reply = "இதற்கு உடனடியாக தீவிர மருத்துவ சிகிச்சை தேவைப்படலாம். தயவுசெய்து உடனே 108 அவசர ஆம்புலன்ஸை அழைக்கவும் அல்லது அவசர சிகிச்சை பிரிவுக்கு செல்லவும்."
elif lang.startswith("hi"):
reply = "इसके लिए तुरंत आपातकालीन चिकित्सा की आवश्यकता हो सकती है। कृपया तुरंत 108 एम्बुलेंस बुलाएं या नजदीकी अस्पताल जाएं।"
elif lang.startswith("te"):
reply = "దీనికి తక్షణ వైద్య సహాయం అవసరం కావచ్చు. దయచేసి వెంటనే 108 కి కాల్ చేయండి."
elif lang.startswith("kn"):
reply = "ಇದಕ್ಕೆ ತಕ್ಷಣದ ಚಿಕಿತ್ಸೆ ಅಗತ್ಯವಿದೆ. ದಯವಿಟ್ಟು ಕೂಡಲೇ 108 ಆಂಬುಲೆನ್ಸ್‌ಗೆ ಕರೆ ಮಾಡಿ."
else:
reply = "This may require immediate medical attention. Please call 108 for an ambulance or visit the nearest emergency room immediately."
return {
"reply": reply,
"next_stage": "confirmation",
"state_updates": {"additional_information": [{"category": "emergency_alert", "value": msg}]},
"completed": True,
}
# ── STEP 2: Build closing text ──
if lang.startswith("ta"):
conclusion_text = "சரிங்க, எல்லா விவரத்தையும் டாக்டர்கிட்ட குடுத்துடுறேன். ரொம்ப நன்றிங்க!"
elif lang.startswith("hi"):
conclusion_text = "ठीक है जी, मैं यह सारी जानकारी डॉक्टर साहब को दे देती हूँ। शुक्रिया!"
elif lang.startswith("te"):
conclusion_text = "చాలా ధన్యవాదాలు! మీ వివరాలన్నీ డాక్టర్ గారికి అందిస్తాను."
elif lang.startswith("kn"):
conclusion_text = "ಸರಿ, ಈ ಎಲ್ಲಾ ವಿವರಗಳನ್ನು ಡಾಕ್ಟರ್‌ಗೆ ತಿಳಿಸ್ತೀನಿ. ಧನ್ಯವಾದಗಳು!"
else:
conclusion_text = "Got it. I have compiled all these details for the doctor. Thank you!"
# ── STEP 3: Extract facts from patient message ──
extracted_updates = self._extract_facts_from_message(
msg,
msg_lower,
last_ai_message=last_ai_message,
current_state=current_state,
)
# ── STEP 4: Handle "nothing else" / negation with contextual confirmed_none ──
is_negative = is_negative_expression(msg)
if is_negative:
expected_intent = detect_previous_question_intent(last_ai_message)
if expected_intent in ["medical_history", "history"]:
if "medical_history" not in extracted_updates or not extracted_updates.get("medical_history"):
extracted_updates["medical_history"] = {"status": "confirmed_none", "items": []}
elif expected_intent in ["medications", "medicine"]:
if "medications" not in extracted_updates or not extracted_updates.get("medications"):
extracted_updates["medications"] = {"status": "confirmed_none", "items": []}
elif expected_intent in ["allergies", "allergy"]:
if "allergies" not in extracted_updates or not extracted_updates.get("allergies"):
extracted_updates["allergies"] = {"status": "confirmed_none", "items": []}
elif expected_intent in ["medications_and_allergies"]:
if "medications" not in extracted_updates or not extracted_updates.get("medications"):
extracted_updates["medications"] = {"status": "confirmed_none", "items": []}
if "allergies" not in extracted_updates or not extracted_updates.get("allergies"):
extracted_updates["allergies"] = {"status": "confirmed_none", "items": []}
# ── STEP 5: Merge and check what is still missing ──
merged_preview = merge_state_updates(current_state, extracted_updates)
required_missing = check_required_missing(merged_preview)
is_psych = (specialty.get("code") == "psychiatry") or (suggested_specialty and suggested_specialty.get("code") == "psychiatry")
# ── STEP 6: If negative and all required is collected → complete ──
if is_negative and not required_missing:
return {
"reply": conclusion_text,
"next_stage": "confirmation",
"state_updates": extracted_updates,
"completed": True,
}
# ── STEP 7: If all required is collected (even without negation) → complete ──
if not required_missing:
return {
"reply": conclusion_text,
"next_stage": "confirmation",
"state_updates": extracted_updates,
"completed": True,
}
# ── STEP 8: Ask ONE best question for the highest priority missing field ──
q = _pick_best_question(required_missing, lang, is_psych)
next_stg = _stage_for_missing(required_missing[0])
return {
"reply": q,
"next_stage": next_stg,
"state_updates": extracted_updates,
"completed": False,
}
def _extract_facts_from_message(
self,
msg: str,
msg_lower: str,
last_ai_message: str = None,
current_state: dict = None,
) -> dict:
updates = {}
# Detect confirmed_none expressions for specific categories
confirmed_none_meds_patterns = [
"எந்த மாத்திரையும்", "மாத்திரை சாப்பிடல", "மாத்திரையும் சாப்பிடல",
"no medicine", "no medication", "not taking any", "no tablets", "no pills",
"कोई दवाई नहीं", "दवाई नहीं",
]
confirmed_none_allergy_patterns = [
"எந்த அலர்ஜியும் இல்லை", "அலர்ஜி இல்லை", "அலர்ஜி இல்ல",
"no allergy", "no allergies", "not allergic", "nkda",
"कोई एलर्जी नहीं", "एलर्जी नहीं",
]
confirmed_none_history_patterns = [
"எந்த நோயும் இல்லை", "நோய் இல்லை", "நோயும் இல்ல",
"உடம்பு பிரச்சனை இல்ல", "பிரச்சனை இல்ல",
"no medical history", "no chronic", "no diseases", "no conditions",
"कोई बीमारी नहीं", "बीमारी नहीं",
]
curr_meds_status = _field_status(current_state.get("medications")) if isinstance(current_state, dict) else "not_collected"
curr_alg_status = _field_status(current_state.get("allergies")) if isinstance(current_state, dict) else "not_collected"
curr_mh_status = _field_status(current_state.get("medical_history")) if isinstance(current_state, dict) else "not_collected"
if curr_meds_status != "reported":
for pat in confirmed_none_meds_patterns:
if pat in msg_lower or pat in msg:
updates["medications"] = {"status": "confirmed_none", "items": []}
break
if curr_alg_status != "reported":
for pat in confirmed_none_allergy_patterns:
if pat in msg_lower or pat in msg:
updates["allergies"] = {"status": "confirmed_none", "items": []}
break
if curr_mh_status != "reported":
for pat in confirmed_none_history_patterns:
if pat in msg_lower or pat in msg:
updates["medical_history"] = {"status": "confirmed_none", "items": []}
break
# If negative expression or casual filler without clinical content, return confirmed_none updates only
if is_negative_expression(msg) or (is_casual_filler(msg) and not any(k in msg_lower for k in ["வலி", "pain", "fever", "காய்ச்சல்", "cough", "இருமல்"])):
return updates
expected_intent = detect_previous_question_intent(last_ai_message)
# 1. Symptoms & Attributes Extraction
symptoms = []
symptom_keywords = [
("stomach pain", "stomach pain"), ("stomach ache", "stomach pain"), ("gastric", "gastric pain"),
("vomiting", "vomiting"), ("vomited", "vomiting"), ("nausea", "nausea"),
("headache", "headache"), ("head ache", "headache"),
("fever", "fever"), ("temperature", "fever"), ("chills", "chills"),
("cough", "cough"), ("cold", "cold"), ("sore throat", "sore throat"),
("chest pain", "chest pain"), ("chest tightness", "chest tightness"),
("breathless", "breathlessness"), ("breathing difficulty", "breathlessness"),
("dizziness", "dizziness"), ("giddiness", "dizziness"),
("knee pain", "knee pain"), ("joint pain", "joint pain"), ("back pain", "back pain"),
("swelling", "swelling"), ("rash", "skin rash"), ("itching", "itching"),
("depression", "depression"), ("anxiety", "anxiety"), ("stress", "stress"),
("insomnia", "insomnia"), ("cannot sleep", "sleep difficulty"), ("couldn't sleep", "sleep difficulty"),
# Tamil
("வயிற்று வலி", "வயிற்று வலி"), ("வாந்தி", "வாந்தி"), ("தலைவலி", "தலைவலி"), ("காய்ச்சல்", "காய்ச்சல்"),
("இருமல்", "இருமல்"), ("சளி", "சளி"), ("நெஞ்சு வலி", "நெஞ்சு வலி"), ("மூச்சு திணறல்", "மூச்சு திணறல்"), ("மூச்சு வாங்குது", "மூச்சு திணறல்"), ("மூச்சு வாங்குதல்", "மூச்சு திணறல்"),
("மூட்டு வலி", "மூட்டு வலி"), ("தோல் அரிப்பு", "தோல் அரிப்பு"), ("மன அழுத்தம்", "மன அழுத்தம்"),
("கை கால் வலி", "உடல் வலி"), ("உடல் வலி", "உடல் வலி"), ("உடம்பு வலி", "உடல் வலி"),
("தலை சுத்துது", "தலைச்சுற்றல்"), ("தலைசுற்றல்", "தலைச்சுற்றல்"), ("படபடப்பு", "நெஞ்சு படபடப்பு"), ("படபடன்னு", "நெஞ்சு படபடப்பு"),
("தூக்கம் சரியா வரல", "தூக்கமின்மை"), ("தூக்கமின்மை", "தூக்கமின்மை"),
# Hindi
("पेट दर्द", "पेट दर्द"), ("उल्टी", "उल्टी"), ("सिरदर्द", "सिरदर्द"), ("बुखार", "बुखार"),
("खांसी", "खांसी"), ("छाती में दर्द", "छाती में दर्द"), ("सांस लेने में तकलीफ", "सांस लेने में तकलीफ"),
]
found_sym_names = set()
for kw, sym_name in symptom_keywords:
if kw in msg_lower or kw in msg:
if sym_name not in found_sym_names:
found_sym_names.add(sym_name)
# Detect normalized duration
duration = normalize_duration_from_message(msg)
# Detect severity & location
severity = extract_severity_from_message(msg)
location = extract_location_from_message(msg)
# Detect frequency (e.g. "twice", "3 times", "irandu murai")
freq = None
freq_m = re.search(r"(twice|once|\d+\s*times|ரெண்டு வாட்டி|இரண்டு முறை|दो बार|तीन बार)", msg_lower)
if freq_m:
freq = freq_m.group(1).strip()
# Build structured symptoms
if found_sym_names:
for sname in found_sym_names:
s_obj = {"name": sname}
if duration:
s_obj["duration"] = duration
if severity:
s_obj["severity"] = severity
if location:
s_obj["location"] = location
if freq and ("vomit" in sname or "வாந்தி" in sname or "உल्टी" in sname):
s_obj["frequency"] = freq
symptoms.append(s_obj)
updates["symptoms"] = symptoms
updates["reason_for_visit"] = list(found_sym_names)[0]
elif duration or severity or location:
attr_obj = {}
if duration:
attr_obj["duration"] = duration
if severity:
attr_obj["severity"] = severity
if location:
attr_obj["location"] = location
updates["symptoms"] = [attr_obj]
elif (
msg.strip()
and len(msg.split()) < 12
and not is_duration_expression(msg)
and not is_severity_expression(msg)
and not is_location_expression(msg)
and not is_casual_filler(msg)
and not is_negative_expression(msg)
and not any(k in msg_lower or k in msg for k in [
"metformin", "dolo", "paracetamol", "pantocid", "telmisartan", "amlodipine", "tablets", "medicine", "pill", "pills", "goli", "mathirai", "மாத்திரை", "மருந்து", "சாப்பிடுறேன்", "சாப்பிட்டேன்", "வாங்குறேன்", "दवाई", "दवा",
"diabetes", "bp", "hypertension", "asthma", "thyroid", "சர்க்கரை", "பிபி", "ஆஸ்துமா", "நோயும்", "வியாதி", "बीमारी", "शुगर",
"allergy", "allergies", "allergic", "அலர்ஜி", "ஒவ்வாமை", "एलर्जी"
])
):
updates["reason_for_visit"] = msg.strip()
updates["symptoms"] = [{"name": msg.strip()}]
# 2. Medical History Extraction (e.g. "diabetes for 8 years", "BP", "hypertension", "asthma")
med_history = []
history_keywords = [
("diabetes", "Diabetes"), ("sugar", "Diabetes"), ("diabetic", "Diabetes"),
("hypertension", "Hypertension (High BP)"), ("high bp", "Hypertension (High BP)"), ("bp", "Hypertension (High BP)"),
("asthma", "Asthma"), ("thyroid", "Thyroid disorder"),
("சர்க்கரை நோய்", "Diabetes"), ("சுகர்", "Diabetes"), ("பிபி", "Hypertension"),
("डायबिटीज", "Diabetes"), ("शुगर", "Diabetes"), ("बीपी", "Hypertension"),
]
for kw, cond in history_keywords:
if re.search(rf"\b{re.escape(kw)}\b", msg_lower) or kw in msg:
h_obj = {"condition": cond}
h_dur_m = re.search(r"(?:for|since)\s+(\d+\s+(?:years|months|yrs))", msg_lower)
if h_dur_m:
h_obj["duration"] = h_dur_m.group(1).strip()
if not any(x.get("condition") == cond for x in med_history):
med_history.append(h_obj)
if med_history:
updates["medical_history"] = med_history
# 3. Medications Extraction (e.g. "Metformin 500 mg twice a day", "Paracetamol", "Dolo 650")
is_allergy_utterance = any(k in msg_lower or k in msg for k in ["allergy", "allergies", "allergic", "அலர்ஜி", "ஒவ்வாமை", "एलर्जी", "rash", "reaction"])
if updates.get("medications", {}).get("status") != "confirmed_none":
meds = []
med_patterns = [
(r"(metformin(?:\s+\d+\s*mg)?(?:\s+twice\s+a\s+day)?)", "Metformin"),
(r"(paracetamol(?:\s+\d+\s*mg)?)", "Paracetamol"),
(r"(dolo(?:\s*650)?)", "Dolo 650 (Paracetamol)"),
(r"(pantocid|pan-d|pan 40|omez|omeprazole)", "Pantoprazole / Antacid"),
(r"(telmisartan|telma\s*\d+)", "Telmisartan (BP)"),
(r"(amlodipine|amlong\s*\d+)", "Amlodipine (BP)"),
(r"((?:bp|பிபி|blood\s*pressure)\s*(?:மாத்திரை|medicine|tablet|tablets|மருந்து))", "BP Medication"),
(r"((?:sugar|சுகர்|சர்க்கரை)\s*(?:மாத்திரை|medicine|tablet|tablets|மருந்து))", "Diabetes Medication"),
(r"((?:thyroid|தைராய்டு)\s*(?:மாத்திரை|medicine|tablet|tablets|மருந்து))", "Thyroid Medication"),
]
if not is_allergy_utterance:
med_patterns.append((r"(tablets|goli|mathirai|மாத்திரை)", "Tablets (unspecified)"))
for pat, med_label in med_patterns:
m_match = re.search(pat, msg_lower)
if m_match:
matched_str = m_match.group(1).strip()
med_obj = {"name": med_label}
dosage_m = re.search(r"(\d+\s*mg|\d+\s*ml)", matched_str)
if dosage_m:
med_obj["dosage"] = dosage_m.group(1).strip()
freq_m = re.search(r"(twice\s+a\s+day|once\s+a\s+day|\d+\s+times\s+a\s+day|daily|twice)", matched_str)
if freq_m:
med_obj["frequency"] = freq_m.group(1).strip()
if not any(x.get("name") == med_obj["name"] for x in meds):
meds.append(med_obj)
if meds:
updates["medications"] = meds
# 4. Allergies Extraction (e.g. "allergic to penicillin", "causes rash", "penicillin எடுத்தா rash")
allergies = []
alg_patterns = [
(r"(penicillin)", "Penicillin"),
(r"(sulfa)", "Sulfa drugs"),
(r"(aspirin)", "Aspirin"),
(r"(dust)", "Dust allergy"),
(r"(peanuts)", "Peanuts"),
]
has_allergen = any(re.search(pat, msg_lower) for pat, _ in alg_patterns)
has_alg_indicator = any(k in msg_lower or k in msg for k in ["allergic", "allergy", "அலர்ஜி", "ஒவ்வாமை", "एलर्जी", "rash", "reaction", "side effect", "தடிப்பு", "எடுத்தா"])
if has_allergen or has_alg_indicator:
for pat, allergen in alg_patterns:
if re.search(pat, msg_lower):
alg_obj = {"name": allergen}
if "rash" in msg_lower or "தடிப்பு" in msg or "खुजली" in msg:
alg_obj["reaction"] = "Rash / Skin reaction"
allergies.append(alg_obj)
if not allergies and has_alg_indicator:
allergies.append({"name": "Reported allergy", "type": "unspecified"})
if allergies:
updates["allergies"] = allergies
# 5. Surgical History & Previous Episodes
if "surgery" in msg_lower or "operation" in msg_lower or "ஆபரேஷன்" in msg:
updates["surgical_history"] = [{"procedure": "Prior surgery reported", "approximate_date": duration or "Past"}]
if "same problem" in msg_lower or "had this before" in msg_lower or "மூன்று மாதங்களுக்கு முன்" in msg:
updates["previous_episodes"] = [{"description": "Similar symptoms previously", "time": duration or "Past"}]
# 6. Additional Information / Functional Impact
add_info = []
if "couldn't eat" in msg_lower or "not eaten" in msg_lower or "சாப்பிட முடியல" in msg:
add_info.append({"category": "functional_impact", "value": "Loss of appetite / difficulty eating"})
if "couldn't sleep" in msg_lower or "haven't slept" in msg_lower or "தூங்க முடியல" in msg:
add_info.append({"category": "functional_impact", "value": "Sleep disruption"})
if "weak" in msg_lower or "tired" in msg_lower or "ரொம்ப டயர்டா இருக்கு" in msg:
add_info.append({"category": "functional_impact", "value": "Severe weakness / fatigue"})
if add_info:
updates["additional_information"] = add_info
return updates