Files
t2a/src/viewer/referentiels.py
dom f44216b95b feat: pass LLM hybride pour DAS + interface admin référentiels RAG
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>
2026-02-12 23:12:39 +01:00

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