# executives/nexus_qualification.py import joblib import os import re MODEL_UNIFIED_PATH = "../pickle_result/nexus_modele_final_V35.pkl" # ===================================================================== # MATRICE LÉGALE DE L'ÉTAT FRANÇAIS (SGDSN / DISPOSITIF ORSEC) # ===================================================================== MATRICE_CIBLES_ISOLEES = { "MÉDICAL": {"CITOYEN": 1, "VIP_LOCAL": 2, "PRESIDENT": 5}, "POLICE": {"CITOYEN": 1, "VIP_LOCAL": 2, "PRESIDENT": 5}, "POMPIER": {"CITOYEN": 1, "VIP_LOCAL": 2, "PRESIDENT": 5}, "CYBERSÉCURITÉ": {"CITOYEN": 1, "VIP_LOCAL": 3, "PRESIDENT": 4}, "ÉNERGIE & INFRASTRUCTURES": {"CITOYEN": 1, "VIP_LOCAL": 3, "PRESIDENT": 4}, "TRANSPORT & MOBILITÉ": {"CITOYEN": 1, "VIP_LOCAL": 3, "PRESIDENT": 4}, "RISQUES ENVIRONNEMENTAUX": {"CITOYEN": 1, "VIP_LOCAL": 3, "PRESIDENT": 3}, "SERVICES URBAINS": {"CITOYEN": 1, "VIP_LOCAL": 2, "PRESIDENT": 2}, "ASSISTANCE SOCIALE": {"CITOYEN": 1, "VIP_LOCAL": 2, "PRESIDENT": 2}, "ANIMALIER": {"CITOYEN": 1, "VIP_LOCAL": 2, "PRESIDENT": 2}, "DIGITAL SUPPORT": {"CITOYEN": 1, "VIP_LOCAL": 1, "PRESIDENT": 1} } class QualificationEngine: def __init__(self): if os.path.exists(MODEL_UNIFIED_PATH): self.model = joblib.load(MODEL_UNIFIED_PATH) self.mode = "ML" print(f"🔌 Moteur d'inférence chargé : {MODEL_UNIFIED_PATH}") else: self.model = None self.mode = "SIMULATION" print(f"⚠️ ERREUR FATALE : Modèle ML introuvable à {MODEL_UNIFIED_PATH}. Mode SIMULATION (0.2) activé.") def traduire_cible_ml(self, cible_id): cible_id = int(cible_id) if cible_id == 0: return "CITOYEN" elif cible_id == 2: return "VIP_LOCAL" elif cible_id == 4: return "PRESIDENT" return "CITOYEN" def calculer_priorite_sgdsn(self, domaine, severite, impact, profil_cible, texte_brut=""): t_lower = str(texte_brut).lower() # 1. COUPE-CIRCUIT ABSOLU (PROTOCOLE JUPITER : 18.0 - 20.0) # CORRECTION : Une centrale nucléaire ou un site souverain est traité au même niveau que le Chef de l'État is_souverain = (profil_cible == "PRESIDENT") or bool(re.search(r'\b(centrale|nucléaire|nucleaire)\b', t_lower)) if is_souverain: if domaine in ["MÉDICAL", "POLICE", "POMPIER", "RISQUES ENVIRONNEMENTAUX"] and severite >= 3: return 20.0 elif domaine in ["CYBERSÉCURITÉ", "ÉNERGIE & INFRASTRUCTURES", "TRANSPORT & MOBILITÉ"]: return 20.0 # 2. PLAFONDS DE VERRE (Niveau 1 : Domaines Bridés) if domaine in ["NON_URGENT", "EN_ATTENTE"]: return 0.2 if domaine == "DIGITAL SUPPORT": return min((0.5 + severite * 0.5 + impact * 0.2), 2.9) if profil_cible == "CITOYEN" else 4.9 if domaine == "ANIMALIER": return min((0.5 + severite * 1.0 + impact * 0.5), 2.9) if domaine in ["SERVICES URBAINS", "ASSISTANCE SOCIALE"]: return min((0.5 + severite * 1.0 + impact * 1.0), 4.9) # SCANNER DE MASSE (PLAN NOVI) has_groupe = re.search(r'\b(groupe|foule|plusieurs|masse|nombreux|personnes|gens|équipe|classe|centaine|cinquantaine)\b', t_lower) has_transport = re.search(r'\b(bus|cars|car|train|métro|metro|tram|avion|navire)\b', t_lower) # 3. DOCTRINE DE GUERRE / MASSE (PLAN NOVI / CIBLES STRATÉGIQUES : 10.1 - 15.0) if (severite == 4 and domaine in ["MÉDICAL", "POMPIER", "POLICE", "RISQUES ENVIRONNEMENTAUX", "CYBERSÉCURITÉ"]) or \ (severite >= 3 and profil_cible == "VIP_LOCAL" and domaine in ["CYBERSÉCURITÉ", "ÉNERGIE & INFRASTRUCTURES", "MÉDICAL", "TRANSPORT & MOBILITÉ"]) or \ (has_groupe and (domaine in ["POMPIER", "MÉDICAL", "POLICE"])): base_vitale = 10.0 delta_masse = 0.0 if impact == 2: delta_masse = 1.5 elif impact == 3: delta_masse = 3.0 elif impact == 4: delta_masse = 4.5 if has_groupe: delta_masse = max(delta_masse, 3.5) if has_transport: delta_masse = max(delta_masse, 4.0) delta_infra = 0.0 if profil_cible == "VIP_LOCAL" or re.search(r'\b(hôpital|hopital|clinique|école|ecole|gare|aéroport)\b', t_lower): delta_infra = 4.0 score_final = base_vitale + delta_masse + delta_infra if domaine in ["CYBERSÉCURITÉ", "ÉNERGIE & INFRASTRUCTURES"]: return min(round(score_final, 1), 15.0) return min(round(score_final, 1), 19.9) # 4. ÉCHELLE CIVILE DYNAMIQUE (Urgence Vitale Individuelle : 5.0 - 10.0) poids_cible = MATRICE_CIBLES_ISOLEES.get(domaine, {}).get(profil_cible, 1) score_base = 0.5 + (severite * 1.5) + (impact * 1.0) + (poids_cible * 0.5) bonus_gravite = 0.0 mots_stress = ['très', 'tres', 'ultra', 'grave', 'gravement', 'fort', 'forte', 'énorme', 'enorme', 'extrême', 'extreme', 'pire', 'vite', 'urgence'] if any(mot in t_lower for mot in mots_stress): bonus_gravite += 1.2 mots_cliniques = ['sang', 'saigne', 'douleur', 'pression', 'brûlure', 'brule', 'coupé', 'coupe', 'cassé', 'casse', 'tomber', 'tombé'] if any(mot in t_lower for mot in mots_cliniques): bonus_gravite += 1.8 mots_liste = t_lower.split() if len(mots_liste) <= 4 and bonus_gravite > 0: bonus_gravite += 0.8 score_normalise = score_base + bonus_gravite return min(round(score_normalise, 1), 10.0) def evaluer_ticket(self, texte): if self.mode == "ML": prediction = self.model.predict([texte])[0] domaine = str(prediction[0]).upper() severite = int(float(prediction[1])) impact = int(float(prediction[2])) cible_id = int(float(prediction[3])) friction = str(prediction[4]).upper() profil_cible = self.traduire_cible_ml(cible_id) score = self.calculer_priorite_sgdsn(domaine, severite, impact, profil_cible, str(texte)) return domaine, score, friction else: return "DIGITAL SUPPORT", 0.2, "MANQUE_LIEU"