Files
aivanov_CIM/tests/test_alphabetical_index_mapping.py
2026-03-05 01:20:14 +01:00

253 lines
8.5 KiB
Python

"""
Tests pour le mapping des termes cliniques vers les codes via les index alphabétiques.
Exigences: 27.3, 27.4
"""
import pytest
from pathlib import Path
from pipeline_mco_pmsi.rag.rag_engine import RAGEngine
from pipeline_mco_pmsi.rag.referentiels_manager import ReferentielsManager, Chunk
@pytest.fixture
def rag_engine_with_alpha_index(tmp_path):
"""Crée un RAGEngine avec des index alphabétiques de test."""
# Créer des chunks d'index alphabétique simulés
alpha_chunks_cim10 = [
Chunk(
chunk_id="cim10_alpha_2026_0",
referentiel_type="cim10",
referentiel_version="2026",
content="""Gastrite - voir K29.7
Gastrite aiguë - K29.0
Gastrite chronique - K29.5
Gastro-entérite - A09""",
metadata={"chunk_type": "alphabetical_index", "letter": "G", "codes": "K29.7,K29.0,K29.5,A09"},
chunk_index=0
),
Chunk(
chunk_id="cim10_alpha_2026_1",
referentiel_type="cim10",
referentiel_version="2026",
content="""Appendicite - K35
Appendicite aiguë - K35.8
Appendicite chronique - K36""",
metadata={"chunk_type": "alphabetical_index", "letter": "A", "codes": "K35,K35.8,K36"},
chunk_index=1
)
]
# Créer des chunks de codes analytiques simulés
code_chunks_cim10 = [
Chunk(
chunk_id="cim10_2026_0",
referentiel_type="cim10",
referentiel_version="2026",
content="""K29.0 Gastrite aiguë hémorragique
Gastrite aiguë avec hémorragie
Exclut: érosion gastrique (K25.-)""",
metadata={"chunk_type": "code", "code": "K29.0"},
chunk_index=0
),
Chunk(
chunk_id="cim10_2026_1",
referentiel_type="cim10",
referentiel_version="2026",
content="""K29.7 Gastrite, sans précision
Gastrite SAI""",
metadata={"chunk_type": "code", "code": "K29.7"},
chunk_index=1
)
]
# Sauvegarder les chunks dans le bon répertoire (data/ et non data/referentiels/)
data_dir = tmp_path / "data"
data_dir.mkdir(parents=True, exist_ok=True)
import json
all_chunks = alpha_chunks_cim10 + code_chunks_cim10
chunks_data = [
{
"chunk_id": c.chunk_id,
"referentiel_type": c.referentiel_type,
"referentiel_version": c.referentiel_version,
"content": c.content,
"metadata": c.metadata,
"chunk_index": c.chunk_index
}
for c in all_chunks
]
# Utiliser le bon nom de fichier dans data/
chunks_file = data_dir / "cim10_2026_chunks.json"
with open(chunks_file, "w", encoding="utf-8") as f:
json.dump(chunks_data, f, ensure_ascii=False, indent=2)
# Créer un index FAISS vide (pour les tests)
import faiss
import numpy as np
dimension = 768
index = faiss.IndexFlatL2(dimension)
# Ajouter des vecteurs aléatoires pour chaque chunk
vectors = np.random.rand(len(all_chunks), dimension).astype('float32')
index.add(vectors)
index_file = data_dir / "cim10_2026_index.faiss"
faiss.write_index(index, str(index_file))
# Créer le ReferentielsManager et RAGEngine
ref_manager = ReferentielsManager(data_dir=str(tmp_path / "data"))
engine = RAGEngine(data_dir=str(tmp_path / "data"), referentiels_manager=ref_manager)
return engine
def test_map_clinical_term_to_codes_from_alpha_index(rag_engine_with_alpha_index):
"""
Test que le mapping d'un terme clinique vers des codes fonctionne
en utilisant les index alphabétiques.
Exigence: 27.3
"""
# Mapper "Gastrite" vers des codes
candidates = rag_engine_with_alpha_index.map_clinical_term_to_codes(
clinical_term="Gastrite",
referentiel_type="cim10",
version="2026",
top_k=3
)
# Vérifier qu'on a des résultats
assert len(candidates) > 0, "Aucun code trouvé pour 'Gastrite'"
# Vérifier que les codes sont pertinents
codes = [c.code for c in candidates]
assert any(code.startswith("K29") for code in codes), "Les codes K29.x (gastrite) devraient être trouvés"
# Vérifier que les résultats de l'index alphabétique ont un boost
alpha_results = [c for c in candidates if c.source == "alphabetical_index"]
if alpha_results:
assert alpha_results[0].similarity_score > 0, "Le score devrait être positif"
def test_map_clinical_term_handles_synonyms(rag_engine_with_alpha_index):
"""
Test que le mapping gère les synonymes et variations.
Exigence: 27.4
"""
# Mapper "Gastrite aiguë" (variation de "Gastrite")
candidates = rag_engine_with_alpha_index.map_clinical_term_to_codes(
clinical_term="Gastrite aiguë",
referentiel_type="cim10",
version="2026",
top_k=3
)
# Vérifier qu'on trouve le code spécifique
codes = [c.code for c in candidates]
assert "K29.0" in codes or any(code.startswith("K29") for code in codes), \
"Le code K29.0 (gastrite aiguë) devrait être trouvé"
def test_map_clinical_term_returns_top_k(rag_engine_with_alpha_index):
"""
Test que le mapping retourne au maximum top_k résultats.
Exigence: 27.3
"""
top_k = 2
candidates = rag_engine_with_alpha_index.map_clinical_term_to_codes(
clinical_term="Gastrite",
referentiel_type="cim10",
version="2026",
top_k=top_k
)
# Vérifier qu'on ne dépasse pas top_k
assert len(candidates) <= top_k, f"Le nombre de résultats devrait être ≤ {top_k}"
def test_map_clinical_term_deduplicates_codes(rag_engine_with_alpha_index):
"""
Test que le mapping déduplique les codes trouvés dans plusieurs sources.
Exigence: 27.3
"""
candidates = rag_engine_with_alpha_index.map_clinical_term_to_codes(
clinical_term="Gastrite",
referentiel_type="cim10",
version="2026",
top_k=10
)
# Vérifier qu'il n'y a pas de doublons
codes = [c.code for c in candidates]
assert len(codes) == len(set(codes)), "Les codes ne devraient pas être dupliqués"
def test_get_synonyms_and_variations(rag_engine_with_alpha_index):
"""
Test que la récupération de synonymes fonctionne.
Exigence: 27.4
"""
synonyms = rag_engine_with_alpha_index.get_synonyms_and_variations(
term="Gastrite",
referentiel_type="cim10",
version="2026"
)
# Vérifier qu'on a des synonymes
# Note: Peut être vide si les chunks de test ne contiennent pas de synonymes
assert isinstance(synonyms, list), "Le résultat devrait être une liste"
def test_map_clinical_term_with_no_results(rag_engine_with_alpha_index):
"""
Test que le mapping gère correctement l'absence de résultats.
Exigence: 27.3
"""
candidates = rag_engine_with_alpha_index.map_clinical_term_to_codes(
clinical_term="TermeInexistantXYZ123",
referentiel_type="cim10",
version="2026",
top_k=5
)
# Vérifier qu'on retourne une liste vide ou des résultats peu pertinents
assert isinstance(candidates, list), "Le résultat devrait être une liste"
# Les scores devraient être faibles si des résultats sont retournés
if candidates:
assert all(c.similarity_score < 0.5 for c in candidates), \
"Les scores devraient être faibles pour un terme inexistant"
def test_alphabetical_index_priority_in_reranking(rag_engine_with_alpha_index):
"""
Test que les résultats de l'index alphabétique sont priorisés dans le reranking.
Exigence: 27.6 (via Propriété 57)
"""
candidates = rag_engine_with_alpha_index.map_clinical_term_to_codes(
clinical_term="Gastrite",
referentiel_type="cim10",
version="2026",
top_k=5
)
# Séparer les résultats par source
alpha_results = [c for c in candidates if c.source == "alphabetical_index"]
code_results = [c for c in candidates if c.source == "analytical_code"]
# Si on a des résultats des deux sources, vérifier la priorisation
if alpha_results and code_results:
# Les résultats de l'index alphabétique devraient avoir des scores plus élevés
max_alpha_score = max(c.similarity_score for c in alpha_results)
max_code_score = max(c.similarity_score for c in code_results)
assert max_alpha_score >= max_code_score, \
"Les résultats de l'index alphabétique devraient avoir des scores plus élevés"