feat: méthode TIM experte CPAM + moteur de règles étendu

CPAM — Méthode TIM (mémoire en défense) :
- Réécriture CPAM_ARGUMENTATION avec raisonnement 5 passes TIM
  (contexte admin → motif réel → confrontation bio → hiérarchie → validation défensive)
- _BIO_THRESHOLDS (19 entrées) + _build_bio_confrontation() pour
  confrontation biologie/diagnostic avec seuils chiffrés et verdicts
- _format_response() dual format : nouveau TIM (moyens numérotés, tableau
  bio, codes non défendables, conclusion dispositive) + rétrocompat legacy
- CPAM_ADVERSARIAL mis à jour pour vérifier honnêteté intellectuelle
- Tests adaptés + 12 nouveaux tests (bio confrontation, format TIM)

Moteur de règles :
- Nouvelles règles YAML : demographic, diagnostic_conflicts,
  procedure_diagnosis, temporal, parcours
- Bio extraction FAISS (synonymes vectoriels)
- Veto engine enrichi (citations, Trackare skip, règles démographiques)
- Decision engine : _apply_bio_rules_gen() + matchers analytiques

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-03-04 11:57:07 +01:00
parent 795110d2e6
commit ce7a9650af
19 changed files with 1681 additions and 418 deletions

View File

@@ -28,6 +28,11 @@ CONFIG_DIR = BASE_DIR / "config"
REFERENCE_RANGES_PATH = CONFIG_DIR / "reference_ranges.yaml"
BIO_RULES_PATH = CONFIG_DIR / "bio_rules.yaml"
LAB_SANITY_PATH = CONFIG_DIR / "lab_value_sanity.yaml"
DEMOGRAPHIC_RULES_PATH = CONFIG_DIR / "demographic_rules.yaml"
DIAGNOSTIC_CONFLICTS_PATH = CONFIG_DIR / "diagnostic_conflicts.yaml"
PROCEDURE_DIAGNOSIS_RULES_PATH = CONFIG_DIR / "procedure_diagnosis_rules.yaml"
TEMPORAL_RULES_PATH = CONFIG_DIR / "temporal_rules.yaml"
PARCOURS_RULES_PATH = CONFIG_DIR / "parcours_rules.yaml"
RULES_DIR = CONFIG_DIR / "rules"
RULES_BASE_PATH = RULES_DIR / "base.yaml"
RULES_ENABLED_PATH = RULES_DIR / "enabled.yaml"
@@ -128,6 +133,7 @@ UPLOAD_MAX_SIZE_MB = 50
ALLOWED_EXTENSIONS = {".pdf", ".csv", ".xlsx", ".xls", ".txt"}
CIM10_DICT_PATH = BASE_DIR / "data" / "cim10_dict.json"
CIM10_SUPPLEMENTS_PATH = BASE_DIR / "data" / "cim10_supplements.json"
BIO_CONCEPTS_PATH = BASE_DIR / "data" / "bio_concepts.json"
CMA_LEVELS_PATH = BASE_DIR / "data" / "cma_levels.json"
CCAM_DICT_PATH = BASE_DIR / "data" / "ccam_dict.json"
CIM10_PDF = Path(os.environ.get("T2A_CIM10_PDF", "/home/dom/ai/aivanov_CIM/cim-10-fr_2026_a_usage_pmsi_version_provisoire_111225.pdf"))
@@ -247,6 +253,124 @@ def load_bio_rules() -> Dict[str, Any]:
return defaults
@lru_cache(maxsize=1)
def load_demographic_rules() -> Dict[str, Any]:
"""Charge les règles démographiques (sexe/âge) depuis config/demographic_rules.yaml."""
defaults: Dict[str, Any] = {
"version": 1,
"sex_rules": {},
"age_rules": {},
}
path = DEMOGRAPHIC_RULES_PATH
if not path.exists():
return defaults
try:
import yaml # type: ignore
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
return defaults
merged = dict(defaults)
for k, v in data.items():
merged[k] = v
return merged
except Exception:
return defaults
@lru_cache(maxsize=1)
def load_diagnostic_conflicts() -> Dict[str, Any]:
"""Charge les conflits diagnostics depuis config/diagnostic_conflicts.yaml."""
defaults: Dict[str, Any] = {
"version": 1,
"mutual_exclusions": [],
"incompatibilities": [],
}
path = DIAGNOSTIC_CONFLICTS_PATH
if not path.exists():
return defaults
try:
import yaml # type: ignore
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
return defaults
merged = dict(defaults)
for k, v in data.items():
merged[k] = v
return merged
except Exception:
return defaults
@lru_cache(maxsize=1)
def load_procedure_diagnosis_rules() -> Dict[str, Any]:
"""Charge les règles de corrélation actes/diagnostics depuis config/procedure_diagnosis_rules.yaml."""
defaults: Dict[str, Any] = {
"version": 1,
"rules": [],
}
path = PROCEDURE_DIAGNOSIS_RULES_PATH
if not path.exists():
return defaults
try:
import yaml # type: ignore
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
return defaults
merged = dict(defaults)
for k, v in data.items():
merged[k] = v
return merged
except Exception:
return defaults
@lru_cache(maxsize=1)
def load_temporal_rules() -> Dict[str, Any]:
"""Charge les règles temporelles depuis config/temporal_rules.yaml."""
defaults: Dict[str, Any] = {
"version": 1,
"rules": [],
}
path = TEMPORAL_RULES_PATH
if not path.exists():
return defaults
try:
import yaml # type: ignore
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
return defaults
merged = dict(defaults)
for k, v in data.items():
merged[k] = v
return merged
except Exception:
return defaults
@lru_cache(maxsize=1)
def load_parcours_rules() -> Dict[str, Any]:
"""Charge les règles de parcours patient depuis config/parcours_rules.yaml."""
defaults: Dict[str, Any] = {
"version": 1,
"documentary_rules": {},
"pathway_rules": {},
}
path = PARCOURS_RULES_PATH
if not path.exists():
return defaults
try:
import yaml # type: ignore
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
return defaults
merged = dict(defaults)
for k, v in data.items():
merged[k] = v
return merged
except Exception:
return defaults
# --- Garde-fous de parsing des valeurs biologiques (anti-OCR) ---
@@ -827,6 +951,7 @@ class VetoIssue(BaseModel):
severity: str # HARD | MEDIUM | LOW
where: str
message: str
citation: Optional[str] = None
class VetoReport(BaseModel):