""" 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"