Initial commit

This commit is contained in:
Dom
2026-03-05 01:20:14 +01:00
commit 2163e574c1
184 changed files with 354881 additions and 0 deletions

326
scripts/import_ccam.py Executable file
View File

@@ -0,0 +1,326 @@
#!/usr/bin/env python3
"""
Script d'import du référentiel CCAM depuis un fichier Excel.
Ce script:
1. Lit le fichier Excel CCAM_V81.xls
2. Extrait les codes CCAM avec leurs descriptions
3. Convertit en format texte structuré
4. Importe dans le ReferentielsManager
5. Génère les chunks et l'index vectoriel
"""
import argparse
import logging
import sys
from pathlib import Path
# Add src to path
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
# Configuration du logging avant les imports qui l'utilisent
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
try:
import pandas as pd
import openpyxl
EXCEL_SUPPORT = True
except ImportError:
EXCEL_SUPPORT = False
logger.error("pandas ou openpyxl non installé. Installez avec: pip install pandas openpyxl")
sys.exit(1)
from pipeline_mco_pmsi.rag.referentiels_manager import ReferentielsManager
def extract_ccam_from_excel(excel_path: Path) -> str:
"""
Extrait le contenu du référentiel CCAM depuis un fichier Excel.
Args:
excel_path: Chemin vers le fichier Excel CCAM
Returns:
Texte structuré du référentiel CCAM
"""
logger.info(f"Lecture du fichier Excel: {excel_path}")
# Utiliser pandas pour lire le fichier Excel (supporte .xls et .xlsx)
try:
df = pd.read_excel(excel_path, engine='xlrd')
except Exception as e:
logger.warning(f"Échec avec xlrd, tentative avec openpyxl: {e}")
try:
df = pd.read_excel(excel_path, engine='openpyxl')
except Exception as e2:
logger.error(f"Impossible de lire le fichier Excel: {e2}")
raise RuntimeError(f"Échec de lecture du fichier Excel: {e2}")
logger.info(f"DataFrame chargé: {len(df)} lignes, {len(df.columns)} colonnes")
logger.info(f"Colonnes: {list(df.columns)}")
# Structure pour stocker le contenu
lines = []
current_chapter = ""
# Analyser la structure des colonnes
# Adapter selon la structure réelle du fichier CCAM
col_names = list(df.columns)
# Essayer de détecter les colonnes importantes
code_col = None
desc_col = None
for i, col in enumerate(col_names):
col_lower = str(col).lower()
if 'code' in col_lower and code_col is None:
code_col = i
elif any(keyword in col_lower for keyword in ['libellé', 'libelle', 'description', 'texte']) and desc_col is None:
desc_col = i
# Si pas trouvé, utiliser les premières colonnes par défaut
if code_col is None:
code_col = 0
logger.warning("Colonne 'code' non détectée, utilisation de la colonne 0")
if desc_col is None:
desc_col = 2 if len(col_names) > 2 else 1
logger.warning(f"Colonne 'description' non détectée, utilisation de la colonne {desc_col}")
logger.info(f"Colonnes utilisées: code={code_col}, description={desc_col}")
# Parcourir les lignes
for idx, row in df.iterrows():
# Ignorer les lignes vides
if row.isna().all():
continue
code = str(row.iloc[code_col]).strip() if pd.notna(row.iloc[code_col]) else ""
text = str(row.iloc[desc_col]).strip() if pd.notna(row.iloc[desc_col]) and desc_col < len(row) else ""
# Nettoyer les valeurs NaN
if code == "nan":
code = ""
if text == "nan":
text = ""
# Ligne d'en-tête (première ligne)
if idx == 0 and text and not code:
lines.append(f"# RÉFÉRENTIEL CCAM")
lines.append("")
continue
# Chapitre (numéro seul dans la colonne code)
if code and code.replace(".", "").replace(",", "").isdigit() and text:
current_chapter = text
lines.append(f"\n## CHAPITRE {code}: {text}")
lines.append("")
continue
# Note ou exclusion (pas de code, mais du texte)
if not code and text:
if "exclusion" in text.lower():
lines.append(f"**Exclusion**: {text}")
elif text.startswith("Par ") or text.startswith("Note"):
lines.append(f"**Note**: {text}")
else:
lines.append(text)
lines.append("")
continue
# Code CCAM (format: XXXX000 ou XXXX000+XXX pour extensions ATIH)
# Accepter aussi les codes avec extensions
if code and len(code) >= 7:
# Vérifier le format de base (4 lettres + 3 chiffres)
base_code = code[:7]
if len(base_code) == 7 and base_code[:4].isalpha() and base_code[4:].isdigit():
# Extraire les métadonnées supplémentaires si disponibles
activite = ""
phase = ""
if len(row) > 3 and pd.notna(row.iloc[3]):
activite = str(row.iloc[3]).strip()
if len(row) > 4 and pd.notna(row.iloc[4]):
phase = str(row.iloc[4]).strip()
# Formater l'entrée CCAM
lines.append(f"### {code}")
if text:
lines.append(f"**Description**: {text}")
if activite and activite != "nan":
lines.append(f"**Activité**: {activite}")
if phase and phase != "nan":
lines.append(f"**Phase**: {phase}")
# Ajouter le chapitre pour contexte
if current_chapter:
lines.append(f"**Chapitre**: {current_chapter}")
# Détecter les extensions ATIH (format +XXX)
if "+" in code:
extension = code.split("+")[1] if len(code.split("+")) > 1 else ""
if extension:
lines.append(f"**Extension ATIH**: +{extension}")
lines.append("")
full_text = "\n".join(lines)
logger.info(f"Extraction terminée: {len(lines)} lignes, {len(full_text)} caractères")
return full_text
def main():
"""Point d'entrée principal du script."""
parser = argparse.ArgumentParser(
description="Import du référentiel CCAM dans le système"
)
parser.add_argument(
"--excel-file",
type=Path,
default=Path("data/referentiels/CCAM_V81.xls"),
help="Chemin vers le fichier Excel CCAM (défaut: data/referentiels/CCAM_V81.xls)"
)
parser.add_argument(
"--version",
type=str,
default="V81",
help="Version du référentiel CCAM (défaut: V81)"
)
parser.add_argument(
"--data-dir",
type=Path,
default=Path("data/referentiels"),
help="Répertoire de stockage des référentiels (défaut: data/referentiels)"
)
parser.add_argument(
"--skip-indexing",
action="store_true",
help="Ne pas créer l'index vectoriel (seulement import et chunking)"
)
args = parser.parse_args()
# Vérifier que le fichier existe
if not args.excel_file.exists():
logger.error(f"Fichier Excel introuvable: {args.excel_file}")
sys.exit(1)
try:
# 1. Extraire le contenu du fichier Excel
logger.info("=" * 60)
logger.info("ÉTAPE 1: Extraction du contenu Excel")
logger.info("=" * 60)
ccam_text = extract_ccam_from_excel(args.excel_file)
# Sauvegarder le texte extrait
text_output_path = args.data_dir / f"ccam_{args.version}_extracted.txt"
with open(text_output_path, "w", encoding="utf-8") as f:
f.write(ccam_text)
logger.info(f"Texte extrait sauvegardé dans: {text_output_path}")
# 2. Importer dans le ReferentielsManager
logger.info("")
logger.info("=" * 60)
logger.info("ÉTAPE 2: Import dans ReferentielsManager")
logger.info("=" * 60)
manager = ReferentielsManager(data_dir=args.data_dir)
# Créer un fichier PDF temporaire pour l'import
# (le ReferentielsManager attend un PDF, mais on va contourner ça)
# Pour l'instant, on va directement sauvegarder le texte et créer la version
# Sauvegarder le texte pour chunking
text_file_path = args.data_dir / f"ccam_{args.version}_text.txt"
with open(text_file_path, "w", encoding="utf-8") as f:
f.write(ccam_text)
# Créer manuellement la version du référentiel
import hashlib
from datetime import datetime
from pipeline_mco_pmsi.models.metadata import ReferentielVersion
file_hash = hashlib.sha256(ccam_text.encode()).hexdigest()
placeholder_hash = "0" * 64
referentiel_version = ReferentielVersion(
type="ccam",
version=args.version,
import_date=datetime.now(),
file_hash=file_hash,
chunk_count=0,
index_hash=placeholder_hash,
)
logger.info(f"Référentiel CCAM {args.version} créé avec hash: {file_hash[:16]}...")
# 3. Chunking
logger.info("")
logger.info("=" * 60)
logger.info("ÉTAPE 3: Chunking du référentiel")
logger.info("=" * 60)
chunks = manager.chunk_referentiel(referentiel_version)
logger.info(f"Chunking terminé: {len(chunks)} chunks créés")
# Créer une nouvelle version avec le chunk_count mis à jour
referentiel_version = ReferentielVersion(
type=referentiel_version.type,
version=referentiel_version.version,
import_date=referentiel_version.import_date,
file_hash=referentiel_version.file_hash,
chunk_count=len(chunks),
index_hash=referentiel_version.index_hash,
)
# 4. Indexation (optionnel)
if not args.skip_indexing:
logger.info("")
logger.info("=" * 60)
logger.info("ÉTAPE 4: Construction de l'index vectoriel")
logger.info("=" * 60)
vector_index = manager.build_index(chunks)
logger.info(f"Index vectoriel créé:")
logger.info(f" - Hash: {vector_index.index_hash[:16]}...")
logger.info(f" - Dimension: {vector_index.dimension}")
logger.info(f" - Nombre de vecteurs: {vector_index.num_vectors}")
logger.info(f" - Type d'index: {vector_index.index_type}")
# Créer une nouvelle version avec l'index_hash mis à jour
referentiel_version = ReferentielVersion(
type=referentiel_version.type,
version=referentiel_version.version,
import_date=referentiel_version.import_date,
file_hash=referentiel_version.file_hash,
chunk_count=referentiel_version.chunk_count,
index_hash=vector_index.index_hash,
)
else:
logger.info("")
logger.info("Indexation ignorée (--skip-indexing)")
# 5. Résumé
logger.info("")
logger.info("=" * 60)
logger.info("IMPORT TERMINÉ AVEC SUCCÈS")
logger.info("=" * 60)
logger.info(f"Référentiel: CCAM {args.version}")
logger.info(f"Hash du fichier: {referentiel_version.file_hash[:16]}...")
logger.info(f"Nombre de chunks: {referentiel_version.chunk_count}")
if not args.skip_indexing:
logger.info(f"Hash de l'index: {referentiel_version.index_hash[:16]}...")
logger.info(f"Date d'import: {referentiel_version.import_date}")
except Exception as e:
logger.error(f"Erreur lors de l'import: {e}", exc_info=True)
sys.exit(1)
if __name__ == "__main__":
main()

346
scripts/load_referentiels.py Executable file
View File

@@ -0,0 +1,346 @@
#!/usr/bin/env python
"""
Script pour charger et indexer tous les référentiels médicaux.
Ce script :
1. Charge et indexe CIM-10 2026 depuis le PDF
2. Convertit/vérifie CCAM V81 → 2025
3. Extrait le Guide Méthodologique MCO 2026
4. Utilise le GPU pour l'indexation FAISS
Usage:
python scripts/load_referentiels.py
"""
import json
import logging
import sys
from pathlib import Path
from typing import List, Dict
# Ajouter le répertoire parent au path
sys.path.insert(0, str(Path(__file__).parent.parent))
try:
import pypdf
PDF_SUPPORT = True
except ImportError:
PDF_SUPPORT = False
print("⚠️ pypdf non installé. Installez-le avec: pip install pypdf")
sys.exit(1)
try:
import pandas as pd
EXCEL_SUPPORT = True
except ImportError:
EXCEL_SUPPORT = False
print("⚠️ pandas non installé. Installez-le avec: pip install pandas openpyxl")
sys.exit(1)
from pipeline_mco_pmsi.rag.referentiels_manager import ReferentielsManager
# Configuration du logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def extract_text_from_pdf(pdf_path: Path) -> str:
"""Extrait le texte d'un fichier PDF."""
logger.info(f"📄 Extraction du PDF: {pdf_path.name}")
text_parts = []
try:
with open(pdf_path, 'rb') as f:
reader = pypdf.PdfReader(f)
if reader.is_encrypted:
try:
reader.decrypt('')
except:
raise RuntimeError(f"PDF protégé par mot de passe: {pdf_path.name}")
total_pages = len(reader.pages)
logger.info(f" {total_pages} pages à traiter...")
for i, page in enumerate(reader.pages, 1):
if i % 50 == 0:
logger.info(f" Progression: {i}/{total_pages} pages")
text = page.extract_text()
if text:
text_parts.append(text)
full_text = '\n\n'.join(text_parts)
logger.info(f"✅ Extraction terminée: {len(full_text)} caractères")
return full_text
except Exception as e:
logger.error(f"❌ Erreur extraction PDF: {e}")
raise
def load_cim10(data_dir: Path, manager: ReferentielsManager) -> bool:
"""Charge et indexe le référentiel CIM-10 2026."""
logger.info("\n" + "="*60)
logger.info("🏥 CHARGEMENT CIM-10 2026")
logger.info("="*60)
pdf_path = Path("cim-10-fr_2026_a_usage_pmsi_version_provisoire_111225.pdf")
if not pdf_path.exists():
logger.error(f"❌ Fichier CIM-10 introuvable: {pdf_path}")
return False
try:
# Extraire le texte du PDF
text = extract_text_from_pdf(pdf_path)
# Sauvegarder le texte brut
text_file = data_dir / "cim10_2026_text.txt"
logger.info(f"💾 Sauvegarde du texte: {text_file}")
with open(text_file, 'w', encoding='utf-8') as f:
f.write(text)
# Découper en chunks et indexer
logger.info("🔪 Découpage en chunks...")
chunks = manager.chunk_cim10(text, "2026")
logger.info(f" {len(chunks)} chunks créés")
# Construire l'index FAISS
logger.info("🔍 Construction de l'index FAISS...")
index = manager.build_index(chunks)
logger.info(f" Index créé avec {index.dimension} dimensions")
# Sauvegarder les chunks
chunks_file = data_dir / "cim10_2026_chunks.json"
logger.info(f"💾 Sauvegarde des chunks: {chunks_file}")
chunks_data = [chunk.model_dump() for chunk in chunks]
with open(chunks_file, 'w', encoding='utf-8') as f:
json.dump(chunks_data, f, ensure_ascii=False, indent=2)
logger.info("✅ CIM-10 2026 chargé et indexé avec succès!")
return True
except Exception as e:
logger.error(f"❌ Erreur chargement CIM-10: {e}")
import traceback
traceback.print_exc()
return False
def load_ccam(data_dir: Path, manager: ReferentielsManager) -> bool:
"""Charge et indexe le référentiel CCAM 2025 (V81)."""
logger.info("\n" + "="*60)
logger.info("🔧 CHARGEMENT CCAM 2025 (V81)")
logger.info("="*60)
# Vérifier si les fichiers V81 existent
v81_chunks = data_dir / "ccam_V81_chunks.json"
v81_text = data_dir / "ccam_V81_text.txt"
if v81_chunks.exists() and v81_text.exists():
logger.info("📦 Fichiers CCAM V81 trouvés, conversion en version 2025...")
try:
# Charger les chunks V81
with open(v81_chunks, 'r', encoding='utf-8') as f:
chunks_data = json.load(f)
logger.info(f" {len(chunks_data)} chunks trouvés")
# Convertir en version 2025
logger.info("🔄 Conversion V81 → 2025...")
for chunk in chunks_data:
chunk['referentiel_version'] = '2025'
chunk['chunk_id'] = chunk['chunk_id'].replace('V81', '2025')
# Sauvegarder avec le nouveau nom
chunks_2025 = data_dir / "ccam_2025_chunks.json"
with open(chunks_2025, 'w', encoding='utf-8') as f:
json.dump(chunks_data, f, ensure_ascii=False, indent=2)
# Copier le fichier texte
text_2025 = data_dir / "ccam_2025_text.txt"
with open(v81_text, 'r', encoding='utf-8') as f:
text_content = f.read()
with open(text_2025, 'w', encoding='utf-8') as f:
f.write(text_content)
# Réindexer avec FAISS
logger.info("🔍 Construction de l'index FAISS...")
# Recréer les objets Chunk
from pipeline_mco_pmsi.rag.referentiels_manager import Chunk
chunks_objects = [Chunk(**chunk) for chunk in chunks_data]
# Construire l'index
index = manager.build_index(chunks_objects)
logger.info(f" Index créé avec {index.dimension} dimensions")
logger.info("✅ CCAM 2025 converti et indexé avec succès!")
return True
except Exception as e:
logger.error(f"❌ Erreur conversion CCAM: {e}")
logger.info(" Tentative de rechargement depuis le fichier Excel...")
# Si conversion échoue ou fichiers absents, charger depuis Excel
excel_path = Path("CCAM_V81.xls")
if not excel_path.exists():
excel_path = data_dir / "CCAM_V81.xls"
if not excel_path.exists():
logger.error(f"❌ Fichier CCAM introuvable: CCAM_V81.xls")
return False
try:
logger.info(f"📊 Lecture du fichier Excel: {excel_path.name}")
# Lire le fichier Excel
df = pd.read_excel(excel_path)
logger.info(f" {len(df)} lignes trouvées")
# Extraire le texte (adapter selon la structure du fichier)
text_parts = []
for _, row in df.iterrows():
# Adapter selon les colonnes du fichier CCAM
row_text = ' '.join(str(val) for val in row.values if pd.notna(val))
text_parts.append(row_text)
text = '\n\n'.join(text_parts)
# Sauvegarder le texte
text_file = data_dir / "ccam_2025_text.txt"
logger.info(f"💾 Sauvegarde du texte: {text_file}")
with open(text_file, 'w', encoding='utf-8') as f:
f.write(text)
# Découper et indexer
logger.info("🔪 Découpage en chunks...")
chunks = manager.chunk_ccam(text, "2025")
logger.info(f" {len(chunks)} chunks créés")
# Construire l'index FAISS
logger.info("🔍 Construction de l'index FAISS...")
index = manager.build_index(chunks)
logger.info(f" Index créé avec {index.dimension} dimensions")
# Sauvegarder les chunks
chunks_file = data_dir / "ccam_2025_chunks.json"
logger.info(f"💾 Sauvegarde des chunks: {chunks_file}")
chunks_data = [chunk.model_dump() for chunk in chunks]
with open(chunks_file, 'w', encoding='utf-8') as f:
json.dump(chunks_data, f, ensure_ascii=False, indent=2)
logger.info("✅ CCAM 2025 chargé et indexé avec succès!")
return True
except Exception as e:
logger.error(f"❌ Erreur chargement CCAM: {e}")
import traceback
traceback.print_exc()
return False
def load_guide_mco(data_dir: Path, manager: ReferentielsManager) -> bool:
"""Charge et indexe le Guide Méthodologique MCO 2026."""
logger.info("\n" + "="*60)
logger.info("📚 CHARGEMENT GUIDE MÉTHODOLOGIQUE MCO 2026")
logger.info("="*60)
pdf_path = Path("guide_methodo_mco_2026_version_provisoire.pdf")
if not pdf_path.exists():
logger.warning(f"⚠️ Fichier Guide MCO introuvable: {pdf_path}")
logger.info(" Le système fonctionnera sans le guide (optionnel)")
return False
try:
# Extraire le texte du PDF
text = extract_text_from_pdf(pdf_path)
# Sauvegarder le texte brut
text_file = data_dir / "guide_mco_2026_text.txt"
logger.info(f"💾 Sauvegarde du texte: {text_file}")
with open(text_file, 'w', encoding='utf-8') as f:
f.write(text)
# Découper en chunks et indexer
logger.info("🔪 Découpage en chunks...")
chunks = manager.chunk_guide_mco(text, "2026")
logger.info(f" {len(chunks)} chunks créés")
# Construire l'index FAISS
logger.info("🔍 Construction de l'index FAISS...")
index = manager.build_index(chunks)
logger.info(f" Index créé avec {index.dimension} dimensions")
# Sauvegarder les chunks
chunks_file = data_dir / "guide_mco_2026_chunks.json"
logger.info(f"💾 Sauvegarde des chunks: {chunks_file}")
chunks_data = [chunk.model_dump() for chunk in chunks]
with open(chunks_file, 'w', encoding='utf-8') as f:
json.dump(chunks_data, f, ensure_ascii=False, indent=2)
logger.info("✅ Guide MCO 2026 chargé et indexé avec succès!")
return True
except Exception as e:
logger.error(f"❌ Erreur chargement Guide MCO: {e}")
import traceback
traceback.print_exc()
return False
def main():
"""Point d'entrée principal."""
logger.info("\n" + "🚀 "*30)
logger.info("CHARGEMENT DES RÉFÉRENTIELS MÉDICAUX")
logger.info("🚀 "*30 + "\n")
# Créer le répertoire de données si nécessaire
data_dir = Path("data/referentiels")
data_dir.mkdir(parents=True, exist_ok=True)
# Initialiser le ReferentielsManager
logger.info(f"📁 Répertoire de données: {data_dir.absolute()}")
manager = ReferentielsManager(data_dir=data_dir)
# Charger les référentiels
results = {
"CIM-10 2026": load_cim10(data_dir, manager),
"CCAM 2025": load_ccam(data_dir, manager),
"Guide MCO 2026": load_guide_mco(data_dir, manager),
}
# Résumé
logger.info("\n" + "="*60)
logger.info("📊 RÉSUMÉ DU CHARGEMENT")
logger.info("="*60)
for name, success in results.items():
status = "✅ OK" if success else "❌ ÉCHEC"
logger.info(f" {name}: {status}")
# Vérifier les fichiers créés
logger.info("\n📦 Fichiers créés:")
for file in sorted(data_dir.glob("*_2025_*")) + sorted(data_dir.glob("*_2026_*")):
size_mb = file.stat().st_size / (1024 * 1024)
logger.info(f" {file.name} ({size_mb:.1f} MB)")
# Statut final
all_success = all(results.values())
if all_success:
logger.info("\n🎉 Tous les référentiels ont été chargés avec succès!")
logger.info(" Le système est prêt à traiter des séjours.")
else:
logger.warning("\n⚠️ Certains référentiels n'ont pas pu être chargés.")
logger.info(" Le système fonctionnera avec les référentiels disponibles.")
return 0 if all_success else 1
if __name__ == "__main__":
sys.exit(main())

328
scripts/process_stay.py Normal file
View File

@@ -0,0 +1,328 @@
#!/usr/bin/env python
"""
Script pour traiter un séjour avec ses documents cliniques.
Usage:
python scripts/process_stay.py --stay-id STAY001 --documents doc1.txt doc2.txt
python scripts/process_stay.py --stay-id STAY001 --documents-dir /path/to/docs/
"""
import argparse
import sys
from datetime import datetime
from pathlib import Path
try:
import pypdf
PDF_SUPPORT = True
except ImportError:
PDF_SUPPORT = False
from pipeline_mco_pmsi.database.base import get_engine, create_all_tables, get_session
from pipeline_mco_pmsi.database.models import StayDB, ClinicalDocumentDB
from pipeline_mco_pmsi.pipeline import Pipeline
from pipeline_mco_pmsi.models.clinical import ClinicalDocument
def extract_text_from_pdf(file_path: Path) -> str:
"""Extrait le texte d'un fichier PDF."""
if not PDF_SUPPORT:
raise ImportError("pypdf n'est pas installé. Installez-le avec: pip install pypdf")
text_parts = []
try:
with open(file_path, 'rb') as f:
reader = pypdf.PdfReader(f)
# Vérifier si le PDF est chiffré
if reader.is_encrypted:
# Tenter de déchiffrer avec mot de passe vide
try:
reader.decrypt('')
except:
raise RuntimeError(f"Le PDF est protégé par mot de passe: {file_path.name}")
for page in reader.pages:
text = page.extract_text()
if text:
text_parts.append(text)
full_text = '\n\n'.join(text_parts)
# Vérifier que du texte a été extrait
if not full_text.strip():
raise RuntimeError(f"Aucun texte extrait du PDF (peut-être un PDF image): {file_path.name}")
return full_text
except RuntimeError:
raise
except Exception as e:
raise RuntimeError(f"Erreur lors de l'extraction du PDF {file_path.name}: {e}")
def load_document(file_path: Path, document_type: str = "cr_operatoire") -> str:
"""Charge le contenu d'un document (txt ou pdf)."""
if file_path.suffix.lower() == '.pdf':
return extract_text_from_pdf(file_path)
else:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
def infer_document_type(filename: str) -> str:
"""Infère le type de document depuis le nom de fichier."""
filename_lower = filename.lower()
if 'cro' in filename_lower or 'operatoire' in filename_lower:
return 'cr_operatoire'
elif 'crm' in filename_lower or 'medical' in filename_lower:
return 'cr_medical'
elif 'hospit' in filename_lower:
return 'cr_hospitalisation'
elif 'consult' in filename_lower:
return 'cr_consultation'
elif 'urgence' in filename_lower:
return 'cr_urgences'
elif 'imagerie' in filename_lower or 'radio' in filename_lower:
return 'imagerie'
elif 'bio' in filename_lower or 'labo' in filename_lower:
return 'biologie'
elif 'courrier' in filename_lower:
return 'courrier'
else:
return 'autre'
def main():
parser = argparse.ArgumentParser(
description="Traite un séjour avec ses documents cliniques"
)
parser.add_argument(
'--stay-id',
required=True,
help="Identifiant du séjour (ex: STAY001)"
)
parser.add_argument(
'--documents',
nargs='+',
help="Liste de fichiers de documents à traiter"
)
parser.add_argument(
'--documents-dir',
type=Path,
help="Répertoire contenant les documents à traiter"
)
parser.add_argument(
'--specialty',
default='chirurgie',
help="Spécialité médicale (défaut: chirurgie)"
)
parser.add_argument(
'--admission-date',
help="Date d'admission (format: YYYY-MM-DD)"
)
parser.add_argument(
'--discharge-date',
help="Date de sortie (format: YYYY-MM-DD)"
)
parser.add_argument(
'--db-url',
default='sqlite:///pipeline_mco_pmsi.db',
help="URL de la base de données (défaut: SQLite local)"
)
args = parser.parse_args()
# Collecter les fichiers de documents
document_files = []
if args.documents:
document_files.extend([Path(d) for d in args.documents])
if args.documents_dir:
if not args.documents_dir.exists():
print(f"❌ Répertoire introuvable: {args.documents_dir}")
sys.exit(1)
document_files.extend(args.documents_dir.glob('*.txt'))
document_files.extend(args.documents_dir.glob('*.pdf'))
if not document_files:
print("❌ Aucun document à traiter. Utilisez --documents ou --documents-dir")
sys.exit(1)
# Vérifier le support PDF si nécessaire
has_pdf = any(f.suffix.lower() == '.pdf' for f in document_files)
if has_pdf and not PDF_SUPPORT:
print("⚠️ Des fichiers PDF ont été détectés mais pypdf n'est pas installé.")
print(" Installez-le avec: pip install pypdf")
print(" Les fichiers PDF seront ignorés.\n")
print(f"📄 {len(document_files)} document(s) à traiter")
# Initialiser la base de données
print(f"🗄️ Connexion à la base de données: {args.db_url}")
engine = get_engine(args.db_url)
create_all_tables(engine)
# Créer ou récupérer le séjour
with get_session(engine) as session:
stay = session.query(StayDB).filter(StayDB.stay_id == args.stay_id).first()
if not stay:
print(f"✨ Création du séjour {args.stay_id}")
# Dates par défaut
admission_date = datetime.now()
if args.admission_date:
admission_date = datetime.strptime(args.admission_date, '%Y-%m-%d')
discharge_date = datetime.now()
if args.discharge_date:
discharge_date = datetime.strptime(args.discharge_date, '%Y-%m-%d')
stay = StayDB(
stay_id=args.stay_id,
admission_date=admission_date,
discharge_date=discharge_date,
specialty=args.specialty,
status='processing'
)
session.add(stay)
session.flush()
else:
print(f"📋 Séjour {args.stay_id} existant trouvé")
# Charger les documents
documents = []
skipped_files = []
# Récupérer les document_ids existants pour éviter les doublons
existing_doc_ids = {doc.document_id for doc in session.query(ClinicalDocumentDB).filter(
ClinicalDocumentDB.stay_id == stay.id
).all()}
doc_counter = len(existing_doc_ids) + 1
for doc_file in document_files:
print(f"📖 Chargement: {doc_file.name}")
# Ignorer les fichiers .oxps (format Microsoft non supporté)
if doc_file.suffix.lower() == '.oxps':
print(f"⚠️ Format .oxps non supporté. Ignoré: {doc_file.name}")
skipped_files.append(doc_file.name)
continue
try:
content = load_document(doc_file)
# Vérifier que le contenu n'est pas vide
if not content.strip():
print(f"⚠️ Document vide. Ignoré: {doc_file.name}")
skipped_files.append(doc_file.name)
continue
doc_type = infer_document_type(doc_file.name)
doc_id = f"{args.stay_id}_DOC{doc_counter:03d}"
# Vérifier si le document existe déjà
if doc_id in existing_doc_ids:
print(f"⚠️ Document déjà existant. Ignoré: {doc_file.name}")
skipped_files.append(doc_file.name)
continue
# Créer le document en base
doc_db = ClinicalDocumentDB(
stay_id=stay.id,
document_id=doc_id,
document_type=doc_type,
content=content,
creation_date=datetime.now(),
author="Import automatique",
priority=doc_counter
)
session.add(doc_db)
# Créer le modèle Pydantic pour le pipeline
doc = ClinicalDocument(
document_id=doc_db.document_id,
document_type=doc_type,
content=content,
creation_date=datetime.now(),
author="Import automatique",
priority=doc_counter
)
documents.append(doc)
doc_counter += 1
except Exception as e:
print(f"❌ Erreur lors du chargement de {doc_file.name}: {e}")
skipped_files.append(doc_file.name)
continue
session.commit()
print(f"{len(documents)} document(s) enregistré(s)")
if skipped_files:
print(f"⚠️ {len(skipped_files)} fichier(s) ignoré(s): {', '.join(skipped_files)}")
# Traiter le séjour avec le pipeline
print(f"\n🚀 Traitement du séjour {args.stay_id}...")
print("⏳ Cela peut prendre quelques minutes...\n")
if not documents:
print("❌ Aucun document valide à traiter")
sys.exit(1)
try:
# Créer une session pour le pipeline
with get_session(engine) as session:
# Créer le RAG engine avec un ReferentielsManager mock
from pipeline_mco_pmsi.rag.rag_engine import RAGEngine
from pipeline_mco_pmsi.rag.referentiels_manager import ReferentielsManager
from pipeline_mco_pmsi.models.metadata import StayMetadata
# Créer un ReferentielsManager (mock pour l'instant)
referentiels_manager = ReferentielsManager(data_dir=Path("data/referentiels"))
rag_engine = RAGEngine(referentiels_manager=referentiels_manager)
# Créer le pipeline
pipeline = Pipeline(
db_session=session,
rag_engine=rag_engine
)
# Créer les métadonnées du séjour
stay_metadata = StayMetadata(
stay_id=args.stay_id,
admission_date=stay.admission_date,
discharge_date=stay.discharge_date,
specialty=stay.specialty
)
result = pipeline.process_stay(
documents=documents,
stay_metadata=stay_metadata
)
print("\n✅ Traitement terminé !")
print(f"\n📊 Résultats:")
print(f" - DP: {result.coding_proposal.dp.code if result.coding_proposal.dp else 'Non proposé'}")
print(f" - DR: {result.coding_proposal.dr.code if result.coding_proposal.dr else 'Non proposé'}")
print(f" - DAS: {len(result.coding_proposal.das)} code(s)")
print(f" - CCAM: {len(result.coding_proposal.ccam)} acte(s)")
print(f" - Questions: {len(result.questions)}")
print(f" - Problèmes de validation: {len(result.validation_issues)}")
if result.verification_result:
print(f" - Décision vérificateur: {result.verification_result.decision}")
print(f"\n🌐 Consultez les résultats sur: http://localhost:8001")
print(f" Recherchez le séjour: {args.stay_id}")
except Exception as e:
print(f"\n❌ Erreur lors du traitement: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

53
scripts/start_api.py Normal file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python
"""
Script pour démarrer l'API TIM.
Usage:
python scripts/start_api.py [--port PORT]
"""
import socket
import sys
import uvicorn
def find_free_port(start_port=8001, max_attempts=10):
"""Trouve un port libre à partir de start_port."""
for port in range(start_port, start_port + max_attempts):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', port))
return port
except OSError:
continue
raise RuntimeError(f"Aucun port libre trouvé entre {start_port} et {start_port + max_attempts}")
if __name__ == "__main__":
# Vérifier si un port est spécifié en argument
port = None
if len(sys.argv) > 1:
if sys.argv[1] == "--port" and len(sys.argv) > 2:
try:
port = int(sys.argv[2])
except ValueError:
print(f"❌ Port invalide: {sys.argv[2]}")
sys.exit(1)
# Trouver un port libre si non spécifié
if port is None:
port = find_free_port()
print("🚀 Démarrage de l'API TIM...")
print(f"📍 Interface web disponible sur: http://localhost:{port}")
print(f"📚 Documentation API disponible sur: http://localhost:{port}/docs")
print(f"📖 Documentation alternative sur: http://localhost:{port}/redoc")
print("\nAppuyez sur Ctrl+C pour arrêter le serveur\n")
uvicorn.run(
"pipeline_mco_pmsi.api.tim_api:app",
host="0.0.0.0",
port=port,
reload=True, # Rechargement automatique en développement
log_level="info",
)

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""
Script pour améliorer le reasoning des codes de test.
"""
from pipeline_mco_pmsi.database.base import get_engine, get_session_factory
from pipeline_mco_pmsi.database.models import StayDB, CodeDB
def update_reasoning():
"""Mettre à jour le reasoning des codes de test."""
engine = get_engine()
session_factory = get_session_factory(engine)
db = session_factory()
try:
# Récupérer le séjour TEST001
stay = db.query(StayDB).filter(StayDB.stay_id == "TEST001").first()
if not stay:
print("Séjour TEST001 non trouvé")
return
# Récupérer les codes
codes = db.query(CodeDB).filter(CodeDB.stay_id == stay.id).all()
for code in codes:
if code.type == "dp" and code.code == "K35.8":
code.reasoning = """Le diagnostic principal K35.8 (Appendicite aiguë, autres et sans précision) est justifié par :
- Présence de douleurs abdominales aiguës dans la fosse iliaque droite
- Signes cliniques d'appendicite confirmés à l'examen
- Intervention chirurgicale réalisée (appendicectomie)
- Confirmation anatomopathologique de l'appendicite aiguë
- Absence de complications (pas de péritonite, pas d'abcès)
Ce code correspond au motif principal d'hospitalisation et à la pathologie ayant mobilisé l'essentiel des ressources."""
elif code.type == "ccam":
code.reasoning = """L'acte CCAM est justifié par :
- Réalisation effective de l'intervention chirurgicale
- Technique chirurgicale : appendicectomie par laparoscopie
- Durée opératoire : environ 45 minutes
- Anesthésie générale
- Absence de complications per-opératoires
- Geste technique principal du séjour
Cet acte correspond à la prise en charge chirurgicale du diagnostic principal."""
db.commit()
print("✅ Reasoning mis à jour avec succès")
# Afficher les résultats
for code in codes:
print(f"\n{code.type.upper()} {code.code}:")
print(f" Reasoning: {code.reasoning[:100]}...")
finally:
db.close()
if __name__ == "__main__":
update_reasoning()