Stack RAG (Retrieval-Augmented Generation) pour interroger le BOFiP (Bulletin Officiel des Finances Publiques - Impôts) en langage naturel.
Basé sur le modèle louisbrulenaudet/lemone-embed-pro fine-tuné sur la doctrine fiscale française, PostgreSQL + pgvector pour la recherche vectorielle, et FastAPI pour l'exposition de l'API.
┌─────────────────────────────────────────────────┐
│ embedding-api │
│ FastAPI · Uvicorn · port 8000 │
│ louisbrulenaudet/lemone-embed-pro (768d) │
└───────────────────┬─────────────────────────────┘
│
┌───────────────────▼─────────────────────────────┐
│ postgres │
│ pgvector/pgvector:pg17 · port 5432 │
│ HNSW index · cosine similarity │
└─────────────────────────────────────────────────┘
Le service ingest (profil Docker Compose séparé) télécharge les documents depuis l'API officielle data.economie.gouv.fr, les découpe en chunks et les vectorise.
- Docker + Docker Compose
- (Optionnel) NVIDIA GPU + nvidia-container-toolkit pour accélérer l'ingestion
# 1. Configuration
cp .env.example .env
# 2. Démarrer l'API + PostgreSQL
docker compose up -d
# 3. Ingérer le BOFiP (one-shot, ~30 min sur CPU / ~5 min sur GPU)
docker compose --profile ingest run ingest
# 4. Tester
curl http://localhost:8000/healthRecherche sémantique dans le BOFiP.
curl -X POST http://localhost:8000/search \
-H "Content-Type: application/json" \
-d '{
"query": "TVA intracommunautaire livraison de biens",
"k": 5,
"serie": "TVA"
}'Paramètres :
| Champ | Type | Description |
|---|---|---|
query |
string | Question en langage naturel |
k |
int | Nombre de résultats (défaut : 5) |
serie |
string | Filtrer par série : IR, IS, TVA, BIC, BNC… |
doc_type |
string | Filtrer par type : Contenu, Barème… |
Réponse :
{
"results": [
{
"id": 18671,
"boi_id": "BOI-TVA-CHAMP-10-10-40-20",
"title": "TVA - Champ d'application...",
"serie": "TVA",
"doc_type": "Contenu",
"published_at": "2022-01-19",
"chunk_index": 3,
"content": "...",
"score": 0.73
}
]
}Vectorise une liste de textes (documents).
curl -X POST http://localhost:8000/embed \
-H "Content-Type: application/json" \
-d '{"texts": ["texte à embedder"]}'Vectorise une liste de textes (requêtes de recherche).
Stocke des textes avec leurs embeddings en base.
curl -X POST http://localhost:8000/documents \
-H "Content-Type: application/json" \
-d '{
"texts": ["contenu du document"],
"metadata": [{"source": "custom"}]
}'{ "status": "ok", "model": "louisbrulenaudet/lemone-embed-pro" }Le script d'ingestion consomme l'API ODS officielle du ministère de l'Économie.
Dataset : bofip-vigueur — 6 309 documents Contenu à jour (màj hebdomadaire)
Séries disponibles : BIC · TVA · IS · IF · IR · ENR · INT · RPPM · RSA · CF · RFPI · BA · REC · CTX · PAT · BNC…
# Ingestion standard (Contenu uniquement)
docker compose --profile ingest run ingest
# Filtrer sur une série pour tester
API_FILTER='type="Contenu" AND serie="TVA"' \
docker compose --profile ingest run ingest
# Suivre les logs
docker logs -f <container_id>L'ingestion est idempotente : les documents déjà présents en base (par boi_id) sont ignorés.
| Variable | Défaut | Description |
|---|---|---|
MODEL_NAME |
louisbrulenaudet/lemone-embed-pro |
Modèle HuggingFace |
MAX_BATCH_SIZE |
64 |
Batch max pour l'API embedding |
POSTGRES_DB |
embeddings |
Nom de la base |
POSTGRES_USER |
embeduser |
Utilisateur PostgreSQL |
POSTGRES_PASSWORD |
embedpass |
Mot de passe PostgreSQL |
API_FILTER |
type="Contenu" |
Filtre ODS (exclut les Actualités) |
CHUNK_WORDS |
400 |
Taille des chunks en mots (~500 tokens) |
CHUNK_OVERLAP |
50 |
Overlap entre chunks en mots |
EMBED_BATCH |
32 |
Batch d'embedding pendant l'ingestion |
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
boi_id TEXT, -- ex: BOI-TVA-CHAMP-10-10-40-20
title TEXT,
doc_type TEXT, -- Contenu, Barème, Formulaire…
serie TEXT, -- IR, IS, TVA, BIC…
published_at DATE,
chunk_index INTEGER NOT NULL DEFAULT 0,
content TEXT NOT NULL,
embedding VECTOR(768), -- lemone-embed-pro (gte-multilingual-base)
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);Index : boi_id · serie · HNSW cosine sur embedding
Le workflow n8n est disponible dans n8n/fiscal-rag.json.
Chat Trigger → AI Agent ──┬── OpenRouter (mistral-small-2603)
├── Simple Memory (buffer window)
└── search_bofip (HTTP Tool → POST /search)
| Nœud | Type | Rôle |
|---|---|---|
| Chat Trigger | chatTrigger |
Interface chat publique avec streaming |
| AI Agent | agent |
Orchestre les appels à search_bofip et formule la réponse |
| OpenRouter Chat Model | lmChatOpenRouter |
LLM : mistralai/mistral-small-2603 |
| Simple Memory | memoryBufferWindow |
Mémoire conversationnelle |
| search_bofip | httpRequestTool |
Appelle POST /search avec k=5 |
- Dans n8n, aller dans Workflows → Import from file
- Sélectionner
n8n/fiscal-rag.json - Configurer les credentials OpenRouter (
openRouterApi) - Mettre à jour l'URL de
search_bofippour pointer vers votre instance (http://<host>:8000/search)
.
├── Dockerfile # Image API (Python 3.11 + torch CPU)
├── docker-compose.yml # Stack complète (api + postgres + ingest)
├── preload_model.py # Préchargement du modèle au build
├── .env.example
├── app/
│ ├── main.py # FastAPI — endpoints embedding + recherche
│ └── requirements.txt
├── ingest/
│ ├── ingest.py # Pipeline d'ingestion BOFiP
│ └── requirements.txt
├── docker/
│ └── init.sql # Schéma PostgreSQL + pgvector
└── n8n/
└── fiscal-rag.json # Workflow n8n (chat RAG fiscal)
# Démarrer
docker compose up -d
# Rebuild après modification
docker compose up -d --build
# Logs de l'API
docker compose logs -f embedding-api
# Reset complet (supprime les données)
docker compose down -v
# Accès psql
docker exec -it embedding-postgres psql -U embeduser -d embeddings
# Nombre de chunks en base
docker exec embedding-postgres psql -U embeduser -d embeddings \
-c "SELECT serie, count(*) FROM documents GROUP BY serie ORDER BY count DESC;"MIT