import os import logging import joblib import json import numpy as np import torch import pickle from transformers import AutoTokenizer, AutoModelForSequenceClassification from textblob import TextBlob # --- 1. SUPPRESS WARNINGS --- os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' logging.getLogger('tensorflow').setLevel(logging.FATAL) MODEL_PATH = "models/mental_bert_model" META_MODEL_PATH = "models/model_meta_fusion.pkl" # Added Meta-Fusion Model Path def load_config(): with open("config.json", "r") as f: return json.load(f) class ClinicalAI: def __init__(self): self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # --- LOAD NLP MODEL --- self.bert_model = None if os.path.exists(MODEL_PATH): try: self.tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH) self.bert_model = AutoModelForSequenceClassification.from_pretrained(MODEL_PATH) self.bert_model.to(self.device) self.bert_model.eval() self.labels = {0: 'ADHD', 1: 'Depression', 2: 'Anxiety', 3: 'Autism'} except Exception as e: print(f"Error loading Mental-BERT: {e}") # --- LOAD META-FUSION MODEL (.pkl) --- self.meta_model = None if os.path.exists(META_MODEL_PATH): try: self.meta_model = pickle.load(open(META_MODEL_PATH, "rb")) except Exception as e: print(f"Error loading Meta-Fusion Model: {e}") self.labels_list = ['ADHD', 'Depression', 'Anxiety', 'Autism'] def _get_sentiment_score(self, text): return TextBlob(str(text)).sentiment.polarity def analyze_single_nlp_response(self, text, question_category=None): if not text or len(text) < 2: return {"diagnosis": "Insufficient Data", "confidence": 0.0, "probs": np.zeros(4), "analysis": "Insufficient data to analyze."} # --- GUARDRAIL 1: SENTIMENT & NEGATION CHECK --- blob = TextBlob(str(text)) sentiment = blob.sentiment.polarity text_lower = text.lower().strip() # --- NEW GUARDRAIL A: LEXICAL NEGATION CHECK (Fixes Keyword Bias) --- # If the answer is relatively short and contains explicit denial, override Mental-BERT is_short_answer = len(text_lower.split()) < 30 if is_short_answer: # 1. Direct clear-cut denials if text_lower.startswith("no ") or text_lower.startswith("no,") or text_lower == "no": # Check to ensure they aren't saying "No, I am depressed" negative_confirmations = ["am depressed", "am anxious", "do worry", "struggle"] if not any(nc in text_lower for nc in negative_confirmations): return { "diagnosis": "No Significant Risk", "confidence": 0.95, "probs": np.array([0.05, 0.05, 0.05, 0.05]), "analysis": "No indicators detected (Patient explicitly denied symptom)." } # 2. Symptom-specific negations (e.g., "not worry", "did not struggle", "do not panic") symptom_negations = ["not struggle", "did not struggle", "don't struggle", "not worry", "do not worry", "don't worry", "mind relaxes", "i am fine", "nothing like that"] if any(sn in text_lower for sn in symptom_negations): return { "diagnosis": "No Significant Risk", "confidence": 0.90, "probs": np.array([0.05, 0.05, 0.05, 0.05]), "analysis": "No indicators detected (Patient explicitly negated symptom triggers)." } # --- GUARDRAIL B: PHYSICAL/MEDICAL AILMENT CHECK --- # If user mentions physical pain, don't jump to psychiatric conclusions physical_keywords = ["pain", "ache", "arthritis", "ra", "joint", "migraine", "thyroid", "insomnia", "physical", "doctor", "disease", "illness", "injury", "headache"] cognitive_keywords = ["focus", "concentrate", "worry", "anxious", "sad", "depress", "fidget", "panic", "sleep", "tired", "energy", "attention", "memory", "forget", "stress"] has_physical = any(word in text_lower for word in physical_keywords) has_cognitive = any(word in text_lower for word in cognitive_keywords) # Guardrail triggers ONLY if it's purely a physical complaint with NO cognitive distress mentioned if has_physical and not has_cognitive and question_category == 'General': return { "diagnosis": "No Significant Risk", "confidence": 0.85, "probs": np.array([0.01, 0.01, 0.01, 0.01]), "analysis": f"User described a physical condition without explicit cognitive distress markers." } negations = ["not", "don't", "do not", "never", "rarely", "no ", "stop", "quit", "manage", "fine", "good"] has_negation = any(neg in text_lower for neg in negations) # High positive sentiment = No Risk if sentiment > 0.25: return { "diagnosis": "No Significant Risk", "confidence": 0.95, "probs": np.array([0.01, 0.01, 0.01, 0.01]), "analysis": f"User indicated no issues (Sentiment: {sentiment:.2f}, Conf: 95.0%)" } # Increased threshold to 0.05 so "I don't relax well" (which has 0.0 sentiment) bypasses this and goes to BERT. if has_negation and sentiment > 0.05: return { "diagnosis": "No Significant Risk", "confidence": 0.90, "probs": np.array([0.01, 0.01, 0.01, 0.01]), "analysis": f"User indicated no issues (Negation detected, Sentiment: {sentiment:.2f})" } # --- LOGIC 3: CONTEXT AWARENESS --- if question_category and sentiment < 0.1: cat_map = {'A': 'ADHD', 'D': 'Depression', 'E': 'Anxiety', 'B': 'Autism', 'C': 'Social Communication Disorder'} mapped_diag = cat_map.get(question_category, None) if mapped_diag: probs = np.zeros(4) if mapped_diag == 'ADHD': probs[0] = 0.85 elif mapped_diag == 'Depression': probs[1] = 0.85 elif mapped_diag == 'Anxiety': probs[2] = 0.85 elif mapped_diag == 'Autism': probs[3] = 0.85 return { "diagnosis": mapped_diag, "confidence": 0.85 + abs(sentiment)/2, "probs": probs, "analysis": f"Detected {mapped_diag} indicators (Context-Aware)" } # --- GUARDRAIL 4: BERT MODEL INFERENCE (FALLBACK) --- if not self.bert_model: return {"diagnosis": "Model Error", "confidence": 0.0, "probs": np.zeros(4), "analysis": "Model not loaded."} try: inputs = self.tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding=True).to(self.device) with torch.no_grad(): outputs = self.bert_model(**inputs) probs = torch.nn.functional.softmax(outputs.logits, dim=-1).cpu().numpy()[0] pred_id = np.argmax(probs) confidence = float(probs[pred_id]) diagnosis = self.labels[pred_id] if confidence < 0.45: diagnosis = "No Significant Risk" confidence = 1.0 - confidence return { "diagnosis": diagnosis, "confidence": confidence, "probs": probs, "analysis": f"AI Detected {diagnosis} ({confidence*100:.1f}%)" } except Exception as e: return {"diagnosis": "Error", "confidence": 0.0, "probs": np.zeros(4), "analysis": f"Processing error: {str(e)}"} def predict_symptom_category(self, text): if not self.bert_model: return None if TextBlob(text).sentiment.polarity > 0.2: return None inputs = self.tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding=True).to(self.device) with torch.no_grad(): outputs = self.bert_model(**inputs) probs = torch.nn.functional.softmax(outputs.logits, dim=-1) pred_id = torch.argmax(probs).item() if probs[0][pred_id].item() > 0.4: return self.labels[pred_id] return None def run_meta_fusion(self, rf_probs, nlp_probs_list): """ Fuses Questionnaire + NLP results using the saved Meta-Fusion .pkl model. Falls back to mathematical fusion if the model fails. """ rf_vector = np.array([ rf_probs.get('ADHD', 0.0), rf_probs.get('Depression', 0.0), rf_probs.get('Anxiety', 0.0), rf_probs.get('Autism', 0.0) ]) if nlp_probs_list and len(nlp_probs_list) > 0: nlp_vector = np.mean(nlp_probs_list, axis=0) # --- INTEGRATING THE .PKL META-MODEL --- if self.meta_model is not None: try: # Combine RF and NLP features into a single array for the model combined_features = np.concatenate([rf_vector, nlp_vector]).reshape(1, -1) final_vector = self.meta_model.predict_proba(combined_features)[0] method = "Machine Learning Meta-Fusion (.pkl)" except Exception as e: # Fallback to math if features don't match exactly final_vector = (rf_vector * 0.6) + (nlp_vector * 0.4) method = "Mathematical Meta-Fusion (Fallback)" else: final_vector = (rf_vector * 0.6) + (nlp_vector * 0.4) method = "Mathematical Meta-Fusion" else: final_vector = rf_vector method = "Questionnaire Only" max_prob = np.max(final_vector) pred_id = np.argmax(final_vector) if max_prob < 0.35: diagnosis = "No Significant Risk" confidence = 1.0 - max_prob else: diagnosis = self.labels_list[pred_id] confidence = max_prob all_probs = {self.labels_list[i]: final_vector[i] for i in range(4)} return { "diagnosis": diagnosis, "confidence": confidence, "all_probs": all_probs, "method": method } def get_suggestions(self, diagnosis, severity): # Reads dynamically from config.json to support Admin Panel config = load_config() if diagnosis == "No Significant Risk": return ("🌟 Great News! Your screening suggests you are currently in a healthy cognitive state. " "Keep up your good habits: maintain a balanced sleep schedule, eat well, and continue socializing.") diag_key = diagnosis.split()[0] if "Autism" in diagnosis: diag_key = "Autism" return config["suggestions"].get(diag_key, {}).get(severity, "Consult a specialist.") # --- THE LOGIC TREE FUNCTION --- def get_logic_driven_questions(flags): """ STRICT LOGIC TREE (DSM-5 Rules). Reads dynamically from config.json. """ config = load_config() nlp_qs = config.get("nlp_questions", {}) active_sections = set() # 1. ADHD Logic if flags['S1']: active_sections.add('A') active_sections.add('E') # Comorbidity rule # 2. Depression Logic if flags['S2']: active_sections.add('D') active_sections.add('E') # Comorbidity rule # 3. Anxiety Logic if flags['S3']: active_sections.add('E') # 4. Differential Diagnosis (ASD vs SPCD) if flags['S4']: if flags['S5']: active_sections.add('B') # ASD if 'C' in active_sections: active_sections.remove('C') else: active_sections.add('C') # SPCD if 'B' in active_sections: active_sections.remove('B') # 5. Standalone S5 (Rigidity without Social) -> ASD indicators elif flags['S5']: active_sections.add('B') # GENERATE QUESTIONS WITH CATEGORY METADATA questions = [] if 'A' in active_sections: questions.append({'text': nlp_qs.get('A', "Describe a time recently when you had to do a boring task. Did you struggle to start?"), 'cat': 'A'}) if 'D' in active_sections: questions.append({'text': nlp_qs.get('D', "Do you still enjoy your favorite hobbies, or does everything feel like too much effort?"), 'cat': 'D'}) if 'E' in active_sections: questions.append({'text': nlp_qs.get('E', "When you have a quiet moment, does your mind relax, or do you constantly worry about the future?"), 'cat': 'E'}) if 'B' in active_sections: # ASD questions.append({'text': nlp_qs.get('B', "How do you react when your daily routine is suddenly changed or if you are in a loud, crowded place?"), 'cat': 'B'}) if 'C' in active_sections: # SPCD questions.append({'text': nlp_qs.get('C', "Do people ever tell you that you are blunt or rude when you don't mean to be?"), 'cat': 'C'}) # FALLBACK: HEALTHY PATIENT CHECK if not questions: questions.append({'text': nlp_qs.get('General', "Please describe your general mood and mental state over the past two weeks. Do you feel happy and energetic?"), 'cat': 'General'}) return questions