1126 lines
59 KiB
Python
1126 lines
59 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 "All details recorded. Thank you!"
|
|
|
|
field = missing_list[0]
|
|
|
|
# Tamil (Max 5 words)
|
|
if lang.startswith("ta"):
|
|
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 "ஏதேனும் மருந்து அலர்ஜி இருக்கா?"
|
|
|
|
# Hindi (Max 5 words)
|
|
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
|
|
|
|
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 = None
|
|
# Priority 1: OpenAI (if user paid for key)
|
|
if self.openai.api_key and self.openai.api_key.strip() not in ["your_openai_key", "your_openai_api_key", ""]:
|
|
try:
|
|
raw_text = await self.openai.generate_response(system_prompt, user_message)
|
|
except Exception as e:
|
|
print(f"[AIOrchestrator] OpenAI LLM call failed: {e}")
|
|
|
|
# Priority 2: Gemini fallback
|
|
if not raw_text and self.gemini.api_key and self.gemini.api_key.strip() not in ["your_gemini_key", "your_gemini_api_key", ""]:
|
|
try:
|
|
raw_text = await self.gemini.generate_response(system_prompt, user_message)
|
|
except Exception as e:
|
|
print(f"[AIOrchestrator] Gemini LLM call failed: {e}")
|
|
|
|
if not raw_text:
|
|
return None
|
|
|
|
# Parse JSON from 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
|
|
|
|
# Clean reply string generated directly by OpenAI
|
|
parsed["reply"] = parsed["reply"].strip()
|
|
|
|
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
|