Initial commit
This commit is contained in:
326
scripts/import_ccam.py
Executable file
326
scripts/import_ccam.py
Executable 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
346
scripts/load_referentiels.py
Executable 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
328
scripts/process_stay.py
Normal 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
53
scripts/start_api.py
Normal 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",
|
||||
)
|
||||
59
scripts/update_test_data_reasoning.py
Normal file
59
scripts/update_test_data_reasoning.py
Normal 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()
|
||||
Reference in New Issue
Block a user