La version 2 de RAG Pro fonctionnait. Les documents s'indexaient, le chat répondait, les fichiers tabulaires étaient analysés. Mais le code ressemblait de plus en plus à une accumulation de cas particuliers. Des if is_data_project, des if project_type == "rag_document" dispersés dans les routes FastAPI, dans le moteur RAG, dans les services. Chaque nouveau type de fichier demandait de toucher cinq endroits différents. C'était maintenable à l'effort, pas à la durée.
La v3 repose sur un principe différent : le Core gère l'infrastructure, les plugins gèrent le métier.
Ce que fait le Core — et uniquement ça
Le Core FastAPI sait authentifier une requête, router vers le bon endpoint, stocker un fichier sur le disque, créer un job d'ingestion en base, streamer une réponse SSE au frontend. Il ne sait pas ce que signifie "indexer un PDF". Il ne sait pas qu'un XLSX peut contenir des données ERP ou du code VBA selon le contexte. Ce n'est pas son rôle.
Quand un document arrive, le Core détermine quel plugin appeler — par project_type, affiné par file_type si nécessaire — et lui passe un contexte. C'est tout. La logique métier reste dans le plugin.
Quatre types de projets, quatre plugins
La v3 distingue quatre familles :
rag_document— PDF, DOCX, TXT, images avec OCR. Pipeline classique : extraction, chunking, embedding dans Qdrant, hybrid search RRF + reranker.data_analysis— CSV et XLSX bruts. Profiling des colonnes, injection de contexte structuré, génération de code pandas validé par AST.code_project— sources Python, JS, TS. Index de symboles, graphe de dépendances, recherche sémantique sur le code.code_migration— fichiers XLSM contenant du VBA. Extraction de la logique, cartographie, génération Python.
Un XLSX dans un projet data_analysis va vers le plugin data. Le même XLSX dans un projet code_migration va vers le plugin migration. L'ambiguïté est résolue par le project_type du projet, jamais par l'extension seule.
L'interface que chaque plugin doit respecter
Chaque plugin implémente une classe abstraite avec cinq points d'entrée : on_upload, on_chat, on_report, on_delete, on_project_delete. Le Core appelle ces méthodes sans connaître leur implémentation. Un plugin qui gère un nouveau format de fichier n'oblige à modifier aucune route existante.
class PluginBase(ABC):
@abstractmethod
def can_handle(self, project_type: str, file_type: str) -> bool: ...
async def on_upload(self, ctx: UploadContext) -> PluginResult: ...
async def on_chat(self, ctx: ChatContext) -> AsyncIterator[str]: ...
Le PluginManager enregistre les plugins au démarrage dans main.py et résout le bon plugin à chaque requête. Pas de registre dynamique, pas de découverte automatique — les plugins sont déclarés explicitement. C'est voulu : moins de magie, plus de lisibilité au moment du debug.
Ce qui a été migré, pas réécrit
Le profiler de colonnes, les domain hints LLM, le service de contexte tabulaire — tout ce code existait en v2 dans src/services/. Il a été déplacé dans src/plugins/data_analysis/ sans réécriture. C'est un point important : la migration v3 n'est pas un grand remplacement. C'est un déplacement de responsabilités avec une interface propre autour.
Qdrant dès la création, pas en rattrapage
Un point qui m'a coûté du temps en v2 : on ne peut pas ajouter un index sparse à une collection Qdrant déjà créée en dense uniquement. Il faut recréer. En v3, toutes les collections sont créées d'emblée avec dense 4096 dimensions (Qwen3-Embedding-8B) et sparse BM25, versionnées par modèle d'embedding dans le nom de la collection. Le hybrid search RRF est activé dès le départ. Pas de dette à rembourser plus tard.