diff --git a/src/control/cpam_context.py b/src/control/cpam_context.py index 4441a97..4143274 100644 --- a/src/control/cpam_context.py +++ b/src/control/cpam_context.py @@ -191,7 +191,12 @@ def _build_tagged_context(dossier: DossierMedical) -> tuple[str, dict[str, str]] if not lines: return "", tag_map - text = "ÉLÉMENTS CLINIQUES RÉFÉRENCÉS (cite le tag [XX-N] dans tes preuves) :\n" + "\n".join(lines) + available = ", ".join(f"[{t}]" for t in sorted(tag_map.keys())) + text = ( + "ÉLÉMENTS CLINIQUES RÉFÉRENCÉS :\n" + + "\n".join(lines) + + f"\n\nTAGS DISPONIBLES pour ce dossier (liste EXHAUSTIVE, n'en invente aucun) : {available}" + ) return text, tag_map @@ -839,6 +844,10 @@ def _build_cpam_prompt( + "\n".join(ext_lines) ) + tags_disponibles_str = ( + ", ".join(f"[{t}]" for t in sorted(tag_map.keys())) + if tag_map else "(aucun)" + ) prompt = CPAM_ARGUMENTATION.format( dossier_str=dossier_str, asymetrie_str=asymetrie_str, @@ -853,5 +862,6 @@ def _build_cpam_prompt( extraction_str=extraction_str, bio_confrontation_str=bio_confrontation, numero_ogc=controle.numero_ogc, + tags_disponibles_str=tags_disponibles_str, ) return prompt, tag_map diff --git a/src/control/cpam_validation.py b/src/control/cpam_validation.py index 83dc1fc..888edb2 100644 --- a/src/control/cpam_validation.py +++ b/src/control/cpam_validation.py @@ -32,6 +32,48 @@ def _fuzzy_match_ref(ref: str, tag_map: dict[str, str]) -> str | None: return None +GENERIC_TAG_RE = re.compile(r"\[([A-Z]+)-N\]") + + +def _resolve_generic_tag( + prefix: str, fait: str, tag_map: dict[str, str] +) -> str | None: + """Résout un tag générique [PREFIX-N] vers le vrai tag le plus proche. + + Cherche dans *tag_map* les tags commençant par *prefix* dont le contenu + partage des mots-clés significatifs avec *fait*. + """ + fait_lower = fait.lower() + fait_words = set(fait_lower.split()) + # Mots trop communs à ignorer + stop = {"de", "du", "le", "la", "les", "un", "une", "des", "et", "ou", "en", "à", "=", "mg", "l", "g", "ml"} + fait_words -= stop + + best_tag: str | None = None + best_score = 0 + + for tag, content in tag_map.items(): + if not tag.startswith(prefix + "-"): + continue + content_lower = content.lower() + content_words = set(content_lower.split()) - stop + # Score = nombre de mots en commun + bonus substring + score = len(fait_words & content_words) + if fait_lower in content_lower or content_lower in fait_lower: + score += 3 + if score > best_score: + best_score = score + best_tag = tag + + # Fallback : si un seul tag de ce prefix existe, le prendre + if best_tag is None: + candidates = [t for t in tag_map if t.startswith(prefix + "-")] + if len(candidates) == 1: + best_tag = candidates[0] + + return best_tag + + def _validate_grounding(response_data: dict, tag_map: dict[str, str]) -> list[str]: """Vérifie que les références dans preuves correspondent à des tags existants. @@ -856,6 +898,28 @@ def _guardian_deterministic( report["preuves_invalid_tags"].append(tag) penalties += 0.5 + # ===== 4b. Corriger les tags génériques [TYPE-N] → vrai tag ===== + if tag_map: + for moyen in result.get("moyens_defense", []): + if not isinstance(moyen, dict): + continue + for preuve in moyen.get("preuves", []): + if not isinstance(preuve, dict): + continue + ref = str(preuve.get("ref", "")) + m = GENERIC_TAG_RE.search(ref) + if m: + prefix = m.group(1) # ex: "BIO" + fait = str(preuve.get("fait", "")).lower() + best_tag = _resolve_generic_tag(prefix, fait, tag_map) + if best_tag: + preuve["ref"] = f"[{best_tag}]" + report["tags_corrected"] = report.get("tags_corrected", 0) + 1 + logger.info( + "Guardian 4b: [%s-N] → [%s] (fait: %s)", + prefix, best_tag, fait[:60], + ) + # ===== 5. Nettoyage des champs texte libre ===== # Remplacer les valeurs bio hallucinées dans les strings (conclusion, rappel, etc.) text_fields = [ diff --git a/src/prompts/templates.py b/src/prompts/templates.py index 8f9f012..dbd8483 100644 --- a/src/prompts/templates.py +++ b/src/prompts/templates.py @@ -329,7 +329,8 @@ CONSIGNES DE RÉDACTION DES MOYENS 6. JAMAIS d'argument sans preuve traçable — si tu n'as pas la preuve, NE FAIS PAS l'argument 7. Ton ASSERTIF mais factuel — pas de formules creuses ("il convient de noter que...") 8. Si un point CPAM est légitime, le reconnaître CLAIREMENT (R4) -9. Tags valides UNIQUEMENT : [DP], [DAS-N], [BIO-N], [IMG-N], [TRT-N], [ACTE-N], [ANT-N], [COMPL-N] +9. Tags valides UNIQUEMENT ceux listés ci-dessus : {tags_disponibles_str} + Si un élément n'a pas de tag, décris le fait en clair SANS inventer de tag Réponds UNIQUEMENT avec un objet JSON : {{ @@ -346,7 +347,7 @@ Réponds UNIQUEMENT avec un objet JSON : {{ "numero": 1, "titre": "Titre court du moyen (ex: Le DP N17.8 est justifié par la biologie)", - "argument": "Développement avec preuves tagées [XX-N], valeurs bio avec seuils, sources réglementaires", + "argument": "Développement avec preuves tagées (utiliser les tags listés ci-dessus), valeurs bio avec seuils, sources réglementaires", "preuves": [ {{"ref": "[BIO-1]", "fait": "Créatinine = 280 µmol/L [norme 50-120]", "signification": "IRA confirmée"}} ],