RAG Pro v3 : pourquoi j'ai tout restructuré autour d'un système de plugins

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.

python
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.

Quand le projet devient multi-utilisateurs

Depuis les premiers billets sur ce projet, j’ai surtout parlé de modèles, d’embeddings et de pipelines RAG. Tout ce qui se passe côté machine. Mais il y a un autre axe d’évolution que j’ai mené en parallèle ces dernières semaines, moins spectaculaire techniquement mais tout aussi important en  […]

Lire la suite

Pourquoi j’ai ajouté un mode RAG piloté par agents dans Internal LLM

Au départ, un LLM local peut déjà aider pour écrire, expliquer ou générer du code. Mais il a une limite simple : il ne connaît pas automatiquement mes documents, mes scripts, mes exports ou la structure réelle de mes projets. C’est pour ça que j’ai ajouté un vrai mode RAG dans Internal LLM. L’idée  […]

Lire la suite

Internal LLM : pourquoi l’architecture repose sur trois moteurs spécialisés

Dans Internal LLM, je n’ai pas choisi un seul modèle pour tout faire. J’ai préféré une architecture en trois services spécialisés, chacun avec un rôle précis. Le service principal du code repose sur Qwen3-Coder-30B-A3B-Instruct en Q5_K_M sur le port 8080, le service de raisonnement sur Qwen3-32B en  […]

Lire la suite

Internal LLM : pourquoi j’ai choisi Ubuntu, un kernel récent et une architecture LLM en plusieurs modèles

Après avoir présenté le projet, je voulais revenir sur un point essentiel : dans un environnement d’IA locale, le choix du système compte presque autant que le choix des modèles. Pour Internal LLM, j’ai retenu Ubuntu 24.04.4 LTS avec un kernel 6.14.0-37, sur une machine basée sur AMD Ryzen AI Max+ 395 avec Radeon 8060S. Ce n’est pas un choix “par défaut”, mais un compromis entre compatibilité matérielle, stabilité et capacité d’expérimentation.

Lire la suite

Internal LLM : pourquoi je construis un assistant IA local.

Je démarre ce blog pour partager l’évolution de Internal LLM, un projet centré sur l’IA locale et l’exploitation intelligente de documentation technique, de code, de requêtes SQL et de connaissances internes. L’objectif est de construire un assistant capable de retrouver, comprendre et exploiter  […]

Lire la suite

Haut de page