diff --git a/src/medical/diagnostic_extraction.py b/src/medical/diagnostic_extraction.py index 4621329..c150d57 100644 --- a/src/medical/diagnostic_extraction.py +++ b/src/medical/diagnostic_extraction.py @@ -256,9 +256,29 @@ def _find_diagnostics_associes( text_norm = normalize_text(text_lower) + # Patterns de négation pour filtrer les faux positifs regex + _NEG_PATTERNS = ( + r"pas\s+d[e']?\s*", + r"(?:absence|aucun[e]?)\s+d[e']?\s*", + r"(?:sans|ni)\s+", + r"(?:elimine[er]?|exclut?|ecarte[er]?)\s+", + r"ne\s+retrouv\w+\s+pas\s+d[e']?\s*", + r"ne\s+montr\w+\s+pas\s+d[e']?\s*", + r"pas\s+(?:mis\s+en\s+evidence|objective?|retrouve)", + r"infirm\w+\s+", + r"sans\s+(?:signe|argument|evidence)\s+d[e']?\s*", + ) + _neg_rx = re.compile("|".join(_NEG_PATTERNS), re.IGNORECASE) + # Patterns DAS for pat, label, code in _DAS_PATTERNS: - if re.search(pat, text_norm) and code not in existing_codes: + m = re.search(pat, text_norm) + if m and code not in existing_codes: + # Vérifier que le match n'est pas précédé d'une négation (40 chars avant) + start = max(0, m.start() - 40) + context_before = text_norm[start:m.start()] + if _neg_rx.search(context_before): + continue das.append(Diagnostic(texte=label, cim10_suggestion=code, source="regex")) existing_codes.add(code) diff --git a/src/prompts/templates.py b/src/prompts/templates.py index d544d7a..659ca03 100644 --- a/src/prompts/templates.py +++ b/src/prompts/templates.py @@ -40,6 +40,8 @@ RÈGLES IMPÉRATIVES : - Si le diagnostic est un DP, il doit refléter le motif principal de prise en charge du séjour - Si c'est un DAS, il doit avoir mobilisé des ressources supplémentaires pendant le séjour - EXCLUSION SYMPTÔME : Si le diagnostic est un symptôme (R00-R99) et qu'un diagnostic précis (Chapitres I-XIV, A00-N99) expliquant ce symptôme est présent, le symptôme ne doit PAS être codé comme DAS +- Un diagnostic "suspecté", "probable", "à éliminer" ne doit PAS être codé comme confirmé sauf s'il a été traité +- Si le texte dit "pas de X", "absence de X" → ne PAS coder X DIAGNOSTIC À CODER : "{texte}" TYPE : {type_diag} @@ -120,6 +122,13 @@ RÈGLES IMPÉRATIVES : - Ne propose que des diagnostics CLAIREMENT mentionnés dans le texte - ATTENTION aux valeurs biologiques : ne code PAS un diagnostic si les valeurs sont dans les normes indiquées entre crochets [N: min-max]. Exemple : Créatinine 76 [N: 50-120] = NORMAL, pas d'insuffisance rénale. +ANTI-HALLUCINATION — CRITIQUE : +- ZERO INVENTION : ne code JAMAIS un diagnostic qui n'est pas explicitement écrit dans le texte +- Ne PAS inférer de comorbidités (ex: patient obèse ≠ forcément diabétique ou hypertendu) +- Si le texte dit "pas de X", "absence de X", "élimine X" → ne PAS coder X +- Un diagnostic "suspecté", "probable", "à éliminer" ne doit PAS être codé sauf s'il a été traité +- Chaque DAS proposé DOIT avoir une justification citant un passage EXACT du texte + DÉNUTRITION — CRITÈRES HAS/FFN 2021 : - Diagnostic = 1 critère phénotypique + 1 critère étiologique - Seuils IMC : adulte <18.5 modéré / ≤17 sévère ; ≥70 ans <22 modéré / <20 sévère diff --git a/src/quality/veto_engine.py b/src/quality/veto_engine.py index 68864e0..9ad3844 100644 --- a/src/quality/veto_engine.py +++ b/src/quality/veto_engine.py @@ -40,12 +40,25 @@ _NEGATION_CUES = ( "pas de", "pas d", "absence de", + "absence d", "non retenu", "exclu", - "a eliminer", - "a éliminer", + "ecarte", + "écarté", + "ne retrouv", + "ne montr", + "ne met pas en evidence", + "pas objective", + "pas retrouve", + "pas mis en evidence", + "sans signe", + "sans argument", + "sans evidence", + "aucun signe", + "aucun argument", "negatif", "négatif", + "infirme", ) _CONDITIONAL_CUES = ( @@ -57,6 +70,15 @@ _CONDITIONAL_CUES = ( "probable", "hypothese", "hypothèse", + "a eliminer", + "a éliminer", + "a rechercher", + "a confirmer", + "possible", + "potentiel", + "risque de", + "aspect de", + "aspect d", "?", ) @@ -142,7 +164,21 @@ def _analyze_neg_cond(excerpts: Iterable[str], label: str) -> tuple[bool, bool, pre = ns[max(0, hit_pos - 40):hit_pos] has_neg = any(cue in pre for cue in _NEGATION_CUES) - has_cond = any(cue in ns for cue in _CONDITIONAL_CUES) + + # Conditionnel : chercher PRÈS du concept (40 chars avant + 20 après) + # et exclure "si" isolé qui est trop ambigu (ex: "si besoin", "si allergie") + context_cond = ns[max(0, hit_pos - 40):min(len(ns), hit_pos + len(kws[0]) + 20)] + has_cond = False + for cue in _CONDITIONAL_CUES: + if cue == "si": + # "si" seulement s'il est directement suivi du concept + # (ex: "si embolie" mais pas "si allergie connue ... embolie") + if f"si {kws[0]}" in context_cond or f"s il {kws[0]}" in context_cond: + has_cond = True + break + elif cue in context_cond: + has_cond = True + break if has_neg: negated = True