562 lines
27 KiB
Python
562 lines
27 KiB
Python
import json
|
|
import re
|
|
import os
|
|
from typing import Dict, Any, List, Optional, Union
|
|
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,
|
|
)
|
|
from app.ai.prompts import build_system_prompt
|
|
|
|
# Import domain and sub-modules
|
|
from app.ai.safety.emergency import check_emergency
|
|
from app.ai.safety.crisis import check_crisis
|
|
from app.ai.extraction.multilingual_normalizer import normalize_text, detect_language
|
|
from app.ai.extraction.symptom_extractor import extract_symptoms
|
|
from app.ai.extraction.medication_extractor import extract_medications
|
|
from app.ai.extraction.allergy_extractor import extract_allergies
|
|
from app.ai.extraction.history_extractor import extract_history
|
|
from app.ai.extraction.negation_engine import is_negated, extract_family_subject
|
|
from app.ai.conversation.completion_engine import check_required_missing, field_status as _field_status, is_onboarding_complete
|
|
from app.ai.conversation.question_engine import pick_best_question as _pick_best_question
|
|
from app.ai.conversation.intent_tracker import get_last_ai_intent
|
|
from app.ai.conversation.response_formatter import format_conversational_response
|
|
from app.ai.conversation.next_best_action import NextBestActionEngine
|
|
from app.ai.conversation.response_generator import ResponseGenerator
|
|
from app.domain.models.contracts import ResponseContext
|
|
from app.ai.providers.openai import OpenAIProvider
|
|
from app.ai.providers.gemini import GeminiProvider
|
|
|
|
|
|
SPECIALTY_RULES = [
|
|
{
|
|
"code": "mental_health",
|
|
"name": "Mental Health & Psychiatry",
|
|
"keywords": [
|
|
"depression", "depressed", "mental", "anxiety", "panic", "stress", "sadness", "suicidal", "suicide", "sleep", "insomnia", "emotional", "mood", "lonely", "hopeless",
|
|
"மன அழுத்தம்", "மனச்சோர்வு", "தூக்கமின்மை", "கவலை", "உணர்ச்சி", "தற்கொலை", "டிப்ரெஷன்", "டிப்ரஷன்",
|
|
"डिप्रेशन", "तनाव", "चिंता", "उदासी", "नींद न आना", "आत्महत्या",
|
|
"మానసిక ఒత్తిడి", "డిప్రెషన్", "నిద్రలేమి",
|
|
"ಮನೋಒತ್ತಡ", "ಖಿನ್ನತೆ", "ನಿದ್ರಾಹೀನತೆ"
|
|
]
|
|
},
|
|
{
|
|
"code": "diabetology",
|
|
"name": "Diabetology & Endocrinology (Sugar / Diabetes)",
|
|
"keywords": [
|
|
"sugar level", "diabetes", "diabetic", "hba1c", "glycomet", "blood sugar", "high glucose", "low glucose", "insulin", "chakkara noyu", "cheeni bimari", "fasting sugar", "ppbs", "metformin", "teneligliptin", "glimepiride", "janumet",
|
|
"சர்க்கரை", "நீரிழிவு", "சர்க்கரை நோய்", "शुगर", "मधुमेह", "చక్కెర", "సక్కರೆ"
|
|
]
|
|
},
|
|
{
|
|
"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",
|
|
"name": "General Physician & OPD Intake",
|
|
"keywords": [
|
|
"fever", "weakness", "body pain", "fatigue", "tired", "chills", "weight loss", "bp",
|
|
"காய்ச்சல்", "உடல் வலி", "உடம்பு வலி", "சோர்வு", "குளிர்", "बुखार", "कमजोरी", "बदन दर्द"
|
|
]
|
|
}
|
|
]
|
|
|
|
|
|
|
|
def classify_specialty_from_symptoms(text: str) -> Dict[str, str]:
|
|
if not text:
|
|
return {"code": "general", "name": "General Physician & OPD Intake"}
|
|
|
|
msg_lower = text.lower()
|
|
for spec in SPECIALTY_RULES:
|
|
if spec["code"] in ("general", "general_medicine"):
|
|
continue
|
|
for kw in spec["keywords"]:
|
|
pattern = rf"\b{re.escape(kw.lower())}\b"
|
|
if re.search(pattern, msg_lower) or kw.lower() in msg_lower:
|
|
code = spec["code"]
|
|
if code == "psychiatry":
|
|
code = "mental_health"
|
|
elif code == "general_medicine":
|
|
code = "general"
|
|
return {"code": code, "name": spec["name"]}
|
|
|
|
return {"code": "general", "name": "General Physician & OPD Intake"}
|
|
|
|
|
|
def _normalize_key(name: str) -> str:
|
|
if not name:
|
|
return ""
|
|
n = name.lower().strip()
|
|
synonyms = {
|
|
"headache": "headache", "head pain": "headache", "migraine": "headache",
|
|
"தலைவலி": "headache", "தலை பாரம்": "headache", "सिरदर्द": "headache", "తలనొప్పి": "headache", "ತಲೆನೋವು": "headache",
|
|
"fever": "fever", "feverish": "fever", "காய்ச்சல்": "fever", "बुखार": "fever", "జ్వరం": "fever", "ಜ್ವರ": "fever",
|
|
"cough": "cough", "coughing": "cough", "இருமல்": "cough", "खांसी": "cough", "దగ్గు": "cough", "ಕೆಮ್ಮು": "cough",
|
|
"chest pain": "chest pain", "நெஞ்சு வலி": "chest pain", "छाती में दर्द": "chest pain",
|
|
"stomach pain": "stomach pain", "வயிற்று வலி": "stomach pain", "पेट दर्द": "stomach pain",
|
|
"vomiting": "vomiting", "vomit": "vomiting", "vomited": "vomiting", "வாந்தி": "vomiting", "உल्टी": "vomiting",
|
|
"dizziness": "dizziness", "giddiness": "dizziness", "தலை சுத்துது": "dizziness", "தலைசுற்றல்": "dizziness", "மயக்கம்": "dizziness", "चक्कर": "dizziness",
|
|
"diabetes": "diabetes", "sugar": "diabetes", "சர்க்கரை": "diabetes",
|
|
"hypertension": "hypertension", "bp": "hypertension", "high bp": "hypertension", "பிபி": "hypertension",
|
|
}
|
|
return synonyms.get(n, n)
|
|
|
|
|
|
def merge_state_updates(existing_state: Dict[str, Any], new_updates: Dict[str, Any]) -> Dict[str, Any]:
|
|
if not existing_state:
|
|
existing_state = {}
|
|
if not new_updates:
|
|
return existing_state
|
|
|
|
merged = dict(existing_state)
|
|
|
|
for key, val in new_updates.items():
|
|
if val is None:
|
|
continue
|
|
|
|
if key == "reason_for_visit":
|
|
if not merged.get("reason_for_visit") or (isinstance(val, str) and val.strip()):
|
|
merged["reason_for_visit"] = val
|
|
continue
|
|
|
|
if key in ["medical_history", "medications", "allergies"]:
|
|
if isinstance(val, dict) and val.get("status") == "confirmed_none":
|
|
existing_val = merged.get(key)
|
|
if isinstance(existing_val, list) and len(existing_val) > 0:
|
|
conflicts = merged.get("conflicts", [])
|
|
conflicts.append({"field": key, "type": "conflict_none_vs_reported", "item": existing_val[0]})
|
|
merged["conflicts"] = conflicts
|
|
if not existing_val or _field_status(existing_val) == "not_collected":
|
|
merged[key] = val
|
|
continue
|
|
if isinstance(val, dict) and val.get("status") == "reported":
|
|
merged[key] = val
|
|
continue
|
|
|
|
if key == "symptoms":
|
|
existing_syms = merged.get("symptoms", [])
|
|
if not isinstance(existing_syms, list):
|
|
existing_syms = []
|
|
|
|
new_syms = val if isinstance(val, list) else [val]
|
|
merged_syms_map: Dict[str, Dict[str, Any]] = {}
|
|
|
|
for s in existing_syms:
|
|
if isinstance(s, dict) and s.get("name"):
|
|
norm_k = _normalize_key(s["name"])
|
|
merged_syms_map[norm_k] = dict(s)
|
|
|
|
for s in new_syms:
|
|
if isinstance(s, dict) and s.get("name"):
|
|
norm_k = _normalize_key(s["name"])
|
|
if norm_k in merged_syms_map:
|
|
existing_obj = merged_syms_map[norm_k]
|
|
for attr_k, attr_v in s.items():
|
|
if attr_v:
|
|
existing_obj[attr_k] = attr_v
|
|
else:
|
|
merged_syms_map[norm_k] = dict(s)
|
|
elif isinstance(s, dict):
|
|
if merged_syms_map:
|
|
first_k = list(merged_syms_map.keys())[0]
|
|
for attr_k, attr_v in s.items():
|
|
if attr_v:
|
|
merged_syms_map[first_k][attr_k] = attr_v
|
|
|
|
merged["symptoms"] = list(merged_syms_map.values()) if merged_syms_map else existing_syms
|
|
continue
|
|
|
|
if key in ["medical_history", "medications", "allergies"]:
|
|
existing_field = merged.get(key)
|
|
if isinstance(existing_field, dict) and existing_field.get("status") == "confirmed_none":
|
|
new_items = val if isinstance(val, list) else [val]
|
|
if new_items:
|
|
conflicts = merged.get("conflicts", [])
|
|
conflicts.append({"field": key, "type": "conflict_reported_vs_none", "item": new_items[0]})
|
|
merged["conflicts"] = conflicts
|
|
merged[key] = new_items
|
|
continue
|
|
|
|
existing_list = merged.get(key, [])
|
|
if isinstance(existing_field, dict) and "items" in existing_field:
|
|
existing_list = existing_field.get("items", [])
|
|
|
|
if not isinstance(existing_list, list):
|
|
existing_list = [existing_list] if existing_list else []
|
|
|
|
new_items = val if isinstance(val, list) else [val]
|
|
|
|
# Merge items by condition/name key to prevent duplicates and support enrichment
|
|
item_map: Dict[str, Dict[str, Any]] = {}
|
|
for item in existing_list:
|
|
if isinstance(item, dict):
|
|
k = item.get("condition") or item.get("name")
|
|
if k:
|
|
item_map[k] = dict(item)
|
|
|
|
for item in new_items:
|
|
if isinstance(item, dict):
|
|
k = item.get("condition") or item.get("name")
|
|
if k:
|
|
if k in item_map:
|
|
for ik, iv in item.items():
|
|
if iv:
|
|
item_map[k][ik] = iv
|
|
else:
|
|
item_map[k] = dict(item)
|
|
|
|
if item_map:
|
|
merged[key] = list(item_map.values())
|
|
else:
|
|
merged[key] = existing_list + [v for v in new_items if v not in existing_list]
|
|
continue
|
|
|
|
if key in ["surgical_history", "previous_episodes", "family_history", "additional_information"]:
|
|
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_has_items(val: Any) -> bool:
|
|
return _field_status(val) in ["reported", "confirmed_none"]
|
|
|
|
|
|
def _any_symptom_has_duration(symptoms: Any) -> bool:
|
|
if isinstance(symptoms, list):
|
|
return any(isinstance(s, dict) and bool(s.get("duration")) for s in symptoms)
|
|
return False
|
|
|
|
|
|
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")
|
|
|
|
|
|
class AIOrchestrator:
|
|
def __init__(self):
|
|
self.openai_provider = OpenAIProvider(timeout_seconds=4.0)
|
|
self.gemini_provider = GeminiProvider(timeout_seconds=4.0)
|
|
self.openai = OpenAIClient()
|
|
self.gemini = GeminiClient()
|
|
self.nba_engine = NextBestActionEngine()
|
|
self.response_generator = ResponseGenerator()
|
|
|
|
async def process(
|
|
self,
|
|
session: Any,
|
|
state: Any,
|
|
conversation_history: List[Dict[str, Any]],
|
|
user_message: str
|
|
) -> Dict[str, Any]:
|
|
lang = getattr(session, "language", "en") or "en"
|
|
current_state = json.loads(state.state_json) if hasattr(state, "state_json") and state.state_json else {}
|
|
|
|
# -------------------------------------------------------------
|
|
# STEP 1: CRISIS DETECTION (PRECEDENCE OVER ALL ONBOARDING)
|
|
# -------------------------------------------------------------
|
|
is_crisis, crisis_reason, crisis_reply = check_crisis(user_message, lang)
|
|
if is_crisis:
|
|
return {
|
|
"reply": format_conversational_response(crisis_reply, lang, scenario="crisis"),
|
|
"next_stage": "crisis_handling",
|
|
"state_updates": {},
|
|
"completed": True,
|
|
"is_crisis": True,
|
|
"suggested_specialty": None
|
|
}
|
|
|
|
# -------------------------------------------------------------
|
|
# STEP 2: EMERGENCY DETECTION (PRECEDENCE OVER ALL ONBOARDING)
|
|
# -------------------------------------------------------------
|
|
is_emergency, emergency_reason, emergency_reply = check_emergency(user_message, lang)
|
|
if is_emergency:
|
|
return {
|
|
"reply": format_conversational_response(emergency_reply, lang, scenario="emergency"),
|
|
"next_stage": "emergency_referral",
|
|
"state_updates": {},
|
|
"completed": True,
|
|
"is_emergency": True,
|
|
"suggested_specialty": None
|
|
}
|
|
|
|
# -------------------------------------------------------------
|
|
# STEP 3: MULTILINGUAL NORMALIZATION & FACT EXTRACTION
|
|
# -------------------------------------------------------------
|
|
normalized_msg = normalize_text(user_message)
|
|
last_ai_intent = get_last_ai_intent(conversation_history)
|
|
last_ai_message = conversation_history[-1].get("message") if conversation_history else None
|
|
|
|
extracted_updates = self._extract_facts_from_message(
|
|
user_message=user_message,
|
|
normalized_msg=normalized_msg,
|
|
last_ai_message=last_ai_message,
|
|
current_state=current_state
|
|
)
|
|
|
|
# -------------------------------------------------------------
|
|
# STEP 4: STATE MERGE & SPECIALTY SELECTION
|
|
# -------------------------------------------------------------
|
|
merged_preview = merge_state_updates(current_state, extracted_updates)
|
|
|
|
# Specialty classification (preserving existing non-general specialty)
|
|
existing_code = getattr(session, "category_code", "general_medicine")
|
|
existing_name = getattr(session, "category_name", "General Medicine")
|
|
|
|
is_duration_msg = is_duration_expression(user_message) or bool(normalize_duration_from_message(user_message))
|
|
if existing_code and existing_code != "general_medicine" and is_duration_msg:
|
|
spec_result = {"code": existing_code, "name": existing_name}
|
|
else:
|
|
raw_spec = classify_specialty_from_symptoms(user_message)
|
|
if existing_code and existing_code != "general_medicine" and raw_spec["code"] == "general_medicine":
|
|
spec_result = {"code": existing_code, "name": existing_name}
|
|
else:
|
|
spec_result = raw_spec
|
|
|
|
is_psych = (spec_result.get("code") == "psychiatry")
|
|
|
|
# -------------------------------------------------------------
|
|
# STEP 5: DETERMINISTIC COMPLETION & MISSING FIELD EVALUATION
|
|
# -------------------------------------------------------------
|
|
required_missing = check_required_missing(merged_preview)
|
|
action_type, target_field = self.nba_engine.determine_next_action(
|
|
current_state=merged_preview,
|
|
is_emergency=False,
|
|
is_crisis=False,
|
|
is_psych=is_psych,
|
|
missing_fields=required_missing
|
|
)
|
|
completed = (action_type == "COMPLETE" or len(required_missing) == 0)
|
|
|
|
if completed:
|
|
conclusion_reply = _pick_best_question([], lang, is_psych=is_psych, current_state=merged_preview)
|
|
formatted_reply = format_conversational_response(conclusion_reply, lang, scenario="completion")
|
|
return {
|
|
"reply": formatted_reply,
|
|
"next_stage": "confirmation",
|
|
"state_updates": extracted_updates,
|
|
"completed": True,
|
|
"suggested_specialty": spec_result
|
|
}
|
|
|
|
# Select single next question
|
|
next_q = _pick_best_question(required_missing, lang, is_psych=is_psych, current_state=merged_preview)
|
|
next_stage = _stage_for_missing(target_field or required_missing[0])
|
|
|
|
# -------------------------------------------------------------
|
|
# STEP 6: AI RESPONSE GENERATION WITH HARD 4s TIMEOUT & FALLBACK
|
|
# -------------------------------------------------------------
|
|
context = {
|
|
"specialty_name": spec_result.get("name", "General Medicine"),
|
|
"patient": {"name": getattr(session, "patient_name", "Patient"), "age": 30, "gender": "Unknown"},
|
|
"language": lang,
|
|
"current_state": merged_preview,
|
|
"conversation_history": conversation_history or [],
|
|
"new_patient_message": user_message,
|
|
"target_question": next_q,
|
|
}
|
|
system_prompt = build_system_prompt(context)
|
|
|
|
raw_text = await self.openai_provider.generate_response_with_timeout(system_prompt, user_message)
|
|
if not raw_text:
|
|
raw_text = await self.gemini_provider.generate_response_with_timeout(system_prompt, user_message)
|
|
|
|
if raw_text:
|
|
try:
|
|
cleaned = raw_text.strip().replace("```json", "").replace("```", "").strip()
|
|
parsed = json.loads(cleaned)
|
|
validated = AIOnboardingResponse(**parsed)
|
|
llm_dict = validated.model_dump()
|
|
llm_dict["state_updates"] = merge_state_updates(extracted_updates, llm_dict.get("state_updates") or {})
|
|
llm_dict["reply"] = format_conversational_response(llm_dict["reply"], lang, scenario="normal")
|
|
llm_dict["completed"] = False
|
|
llm_dict["next_stage"] = next_stage
|
|
llm_dict["suggested_specialty"] = spec_result
|
|
return llm_dict
|
|
except Exception as e:
|
|
print(f"[AIOrchestrator LLM Validation Error]: {e}")
|
|
|
|
# -------------------------------------------------------------
|
|
# STEP 7: DETERMINISTIC FALLBACK (INSTANT)
|
|
# -------------------------------------------------------------
|
|
formatted_reply = format_conversational_response(next_q, lang, scenario="normal")
|
|
return {
|
|
"reply": formatted_reply,
|
|
"next_stage": next_stage,
|
|
"state_updates": extracted_updates,
|
|
"completed": False,
|
|
"suggested_specialty": spec_result
|
|
}
|
|
|
|
def _extract_facts_from_message(
|
|
self,
|
|
user_message: str,
|
|
normalized_msg: str,
|
|
last_ai_message: Optional[str] = None,
|
|
current_state: Optional[Dict[str, Any]] = None
|
|
) -> Dict[str, Any]:
|
|
msg_lower = normalized_msg.lower().strip()
|
|
updates: Dict[str, Any] = {}
|
|
|
|
duration = normalize_duration_from_message(user_message)
|
|
severity = extract_severity_from_message(user_message)
|
|
location = extract_location_from_message(user_message)
|
|
last_intent = detect_previous_question_intent(last_ai_message) if last_ai_message else None
|
|
|
|
# 1. Symptoms & Reason for Visit
|
|
found_symptoms, primary_rfv = extract_symptoms(user_message, normalized_msg, current_state)
|
|
if found_symptoms:
|
|
updates["symptoms"] = found_symptoms
|
|
if primary_rfv:
|
|
updates["reason_for_visit"] = primary_rfv
|
|
elif (duration or severity or location) and current_state and current_state.get("symptoms"):
|
|
# Duration, Severity, or Location standalone follow-up turn
|
|
existing_syms = current_state.get("symptoms", [])
|
|
if isinstance(existing_syms, list) and len(existing_syms) > 0 and isinstance(existing_syms[0], dict):
|
|
updated_syms = [dict(s) for s in existing_syms]
|
|
if duration:
|
|
updated_syms[0]["duration"] = duration
|
|
if severity:
|
|
updated_syms[0]["severity"] = severity
|
|
if location:
|
|
updated_syms[0]["location"] = location
|
|
updates["symptoms"] = updated_syms
|
|
|
|
# 2. Medical History
|
|
history_updates = extract_history(user_message, normalized_msg, duration)
|
|
updates.update(history_updates)
|
|
|
|
# 3. Medications
|
|
is_alg_uttr = "allergy" in msg_lower or "அலர்ஜி" in user_message or "एलर्जी" in user_message
|
|
meds_res = extract_medications(user_message, normalized_msg, last_ai_message, is_allergy_utterance=is_alg_uttr)
|
|
if meds_res is not None:
|
|
updates["medications"] = meds_res
|
|
|
|
# 4. Allergies
|
|
alg_res = extract_allergies(user_message, normalized_msg, last_ai_message)
|
|
if alg_res is not None:
|
|
updates["allergies"] = alg_res
|
|
|
|
# Contextual intent follow-up mapping
|
|
if last_intent == "medical_history" and is_negative_expression(user_message):
|
|
updates["medical_history"] = {"status": "confirmed_none", "items": []}
|
|
if last_intent == "medications" and is_negative_expression(user_message):
|
|
updates["medications"] = {"status": "confirmed_none", "items": []}
|
|
if last_intent == "allergies" and is_negative_expression(user_message):
|
|
updates["allergies"] = {"status": "confirmed_none", "items": []}
|
|
if last_intent == "medications_and_allergies" and is_negative_expression(user_message):
|
|
updates["medications"] = {"status": "confirmed_none", "items": []}
|
|
updates["allergies"] = {"status": "confirmed_none", "items": []}
|
|
|
|
return updates
|
|
|
|
def _simulate_specialty_response(
|
|
self,
|
|
session: Any,
|
|
specialty: str,
|
|
user_message: str,
|
|
history_length: int
|
|
) -> Dict[str, Any]:
|
|
lang = getattr(session, "language", "en") or "en"
|
|
is_psych = (specialty == "psychiatry")
|
|
spec_info = {"code": specialty, "name": specialty.replace("_", " ").title()}
|
|
|
|
is_crisis, _, crisis_reply = check_crisis(user_message, lang)
|
|
if is_crisis:
|
|
return {"reply": crisis_reply, "next_stage": "crisis_handling", "state_updates": {}, "completed": True, "suggested_specialty": spec_info}
|
|
|
|
is_emergency, _, emergency_reply = check_emergency(user_message, lang)
|
|
if is_emergency:
|
|
return {"reply": emergency_reply, "next_stage": "emergency_referral", "state_updates": {}, "completed": True, "suggested_specialty": spec_info}
|
|
|
|
updates = self._extract_facts_from_message(user_message, normalize_text(user_message))
|
|
missing = check_required_missing(updates)
|
|
completed = (len(missing) == 0)
|
|
|
|
if completed:
|
|
reply = _pick_best_question([], lang, is_psych=is_psych)
|
|
return {"reply": format_conversational_response(reply, lang), "next_stage": "confirmation", "state_updates": updates, "completed": True, "suggested_specialty": spec_info}
|
|
|
|
reply = _pick_best_question(missing, lang, is_psych=is_psych)
|
|
stage = _stage_for_missing(missing[0])
|
|
return {"reply": format_conversational_response(reply, lang), "next_stage": stage, "state_updates": updates, "completed": False, "suggested_specialty": spec_info}
|