Chantier 1 — Extraction DAS par LLM : - Nouveau prompt expert DIM dans rag_search.py (extract_das_llm) - Phase 4 dans cim10_extractor.py : détection DAS supplémentaires avant enrichissement RAG - Cache persistant (clé hash du texte), validation CIM-10, déduplication - Activé uniquement avec use_rag=True (--no-rag le désactive) Chantier 2 — Admin référentiels : - Config : REFERENTIELS_DIR, UPLOAD_MAX_SIZE_MB, ALLOWED_EXTENSIONS - Chunking générique (PDF/CSV/Excel/TXT) + ajout incrémental FAISS dans rag_index.py - ReferentielManager CRUD dans viewer/referentiels.py - 5 routes Flask (listing, upload, indexation, suppression, rebuild) - Template admin avec tableau interactif + lien sidebar Fix : if cache → if cache is not None (OllamaCache vide évaluait à False) 410 tests passent (27 nouveaux, 0 régression). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
156 lines
4.7 KiB
Python
156 lines
4.7 KiB
Python
"""Gestionnaire de référentiels utilisateur pour le RAG."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import shutil
|
|
import uuid
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from ..config import REFERENTIELS_DIR, ALLOWED_EXTENSIONS, UPLOAD_MAX_SIZE_MB
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ReferentielManager:
|
|
"""CRUD pour les fichiers de référentiels utilisateur.
|
|
|
|
Stocke les fichiers dans REFERENTIELS_DIR avec un index.json
|
|
pour les métadonnées.
|
|
"""
|
|
|
|
def __init__(self, referentiels_dir: Path | None = None):
|
|
self._dir = referentiels_dir or REFERENTIELS_DIR
|
|
self._dir.mkdir(parents=True, exist_ok=True)
|
|
self._index_path = self._dir / "index.json"
|
|
self._index: list[dict] = self._load_index()
|
|
|
|
def _load_index(self) -> list[dict]:
|
|
if self._index_path.exists():
|
|
try:
|
|
return json.loads(self._index_path.read_text(encoding="utf-8"))
|
|
except (json.JSONDecodeError, KeyError):
|
|
logger.warning("Index référentiels corrompu, réinitialisé")
|
|
return []
|
|
|
|
def _save_index(self) -> None:
|
|
self._index_path.write_text(
|
|
json.dumps(self._index, ensure_ascii=False, indent=2),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
def list_all(self) -> list[dict]:
|
|
"""Retourne la liste de tous les référentiels."""
|
|
return list(self._index)
|
|
|
|
def get(self, ref_id: str) -> dict | None:
|
|
"""Retourne un référentiel par son ID."""
|
|
for ref in self._index:
|
|
if ref["id"] == ref_id:
|
|
return ref
|
|
return None
|
|
|
|
def add_file(self, filename: str, file_data: bytes) -> dict:
|
|
"""Ajoute un fichier de référentiel.
|
|
|
|
Args:
|
|
filename: Nom original du fichier.
|
|
file_data: Contenu binaire du fichier.
|
|
|
|
Returns:
|
|
Métadonnées du référentiel créé.
|
|
|
|
Raises:
|
|
ValueError: Extension non autorisée ou taille dépassée.
|
|
"""
|
|
ext = Path(filename).suffix.lower()
|
|
if ext not in ALLOWED_EXTENSIONS:
|
|
raise ValueError(f"Extension '{ext}' non autorisée. Extensions valides : {ALLOWED_EXTENSIONS}")
|
|
|
|
size_mb = len(file_data) / (1024 * 1024)
|
|
if size_mb > UPLOAD_MAX_SIZE_MB:
|
|
raise ValueError(f"Fichier trop volumineux ({size_mb:.1f} Mo > {UPLOAD_MAX_SIZE_MB} Mo)")
|
|
|
|
ref_id = uuid.uuid4().hex[:12]
|
|
safe_name = f"{ref_id}_{Path(filename).stem}{ext}"
|
|
file_path = self._dir / safe_name
|
|
|
|
file_path.write_bytes(file_data)
|
|
|
|
ref = {
|
|
"id": ref_id,
|
|
"filename": filename,
|
|
"stored_name": safe_name,
|
|
"extension": ext,
|
|
"size_bytes": len(file_data),
|
|
"date_added": datetime.now().isoformat(),
|
|
"status": "uploaded",
|
|
"chunks_count": 0,
|
|
}
|
|
self._index.append(ref)
|
|
self._save_index()
|
|
|
|
logger.info("Référentiel ajouté : %s (%s)", filename, ref_id)
|
|
return ref
|
|
|
|
def remove(self, ref_id: str) -> bool:
|
|
"""Supprime un référentiel (fichier + métadonnées).
|
|
|
|
Returns:
|
|
True si trouvé et supprimé, False sinon.
|
|
"""
|
|
ref = self.get(ref_id)
|
|
if not ref:
|
|
return False
|
|
|
|
file_path = self._dir / ref["stored_name"]
|
|
if file_path.exists():
|
|
file_path.unlink()
|
|
|
|
self._index = [r for r in self._index if r["id"] != ref_id]
|
|
self._save_index()
|
|
|
|
logger.info("Référentiel supprimé : %s (%s)", ref["filename"], ref_id)
|
|
return True
|
|
|
|
def index_referentiel(self, ref_id: str) -> int:
|
|
"""Indexe un référentiel dans FAISS.
|
|
|
|
Args:
|
|
ref_id: ID du référentiel à indexer.
|
|
|
|
Returns:
|
|
Nombre de chunks indexés.
|
|
|
|
Raises:
|
|
ValueError: Référentiel introuvable.
|
|
"""
|
|
ref = self.get(ref_id)
|
|
if not ref:
|
|
raise ValueError(f"Référentiel {ref_id} introuvable")
|
|
|
|
file_path = self._dir / ref["stored_name"]
|
|
if not file_path.exists():
|
|
raise ValueError(f"Fichier {ref['stored_name']} introuvable")
|
|
|
|
from ..medical.rag_index import chunk_user_file, add_chunks_to_index
|
|
|
|
doc_name = f"ref:{ref['filename']}"
|
|
chunks = chunk_user_file(file_path, doc_name)
|
|
|
|
if not chunks:
|
|
ref["status"] = "empty"
|
|
ref["chunks_count"] = 0
|
|
self._save_index()
|
|
return 0
|
|
|
|
count = add_chunks_to_index(chunks)
|
|
ref["status"] = "indexed"
|
|
ref["chunks_count"] = count
|
|
self._save_index()
|
|
|
|
logger.info("Référentiel indexé : %s → %d chunks", ref["filename"], count)
|
|
return count
|