feat(r5.1): socle apps/ai — ingestion anonymisée + recherche sémantique

ADR-004 : embeddings locaux sur CPU (fastembed ONNX,
paraphrase-multilingual-MiniLM-L12-v2, 384 dims — les textes du client
ne quittent jamais le serveur), pgvector dans le Postgres existant
(RagChunk possédé par Prisma, migration r5_ia + état de corpus sur
Document), génération opt-in (mode extractif par défaut : la recette
passe sans clé API), service siop2-ai jamais exposé — joint par l'API
NestJS seule (X-Service-Token).

apps/ai (FastAPI + uv) : pipeline PDF MinIO → texte paginé (pypdf) →
anonymisation D4 (e-mails, téléphones marocains, noms connus de la
base, insensible casse/accents — fonction pure testée) → découpage
avec chevauchement (testé) → embeddings → RagChunk localisé (« p. 42 »,
« bilan du 17/07 »). Bilans codés clôturés ingérés. Exclusion de
corpus (D3) appliquée à l'ingestion ET à la lecture.

14 pytest + ruff, embeddeur déterministe en CI (aucun téléchargement),
job CI ai (uv), deploy en dépend. Vérifié en réel avec le vrai modèle :
corpus seedé réindexé en 7 s (PDF réel → 31 extraits paginés + 3
bilans), recherche sémantique concluante, e-mails → ⟨contact⟩,
0 identité dans les chunks (contrôle SQL).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-17 12:22:15 +01:00
parent 199fce69d0
commit 837dcba1db
22 changed files with 2564 additions and 4 deletions

View File

@@ -0,0 +1,43 @@
# ADR-004 — Modèles et topologie du service IA (R5)
- **Statut** : acceptée (R5.1, 17 juillet 2026)
- **Contexte** : les maquettes R5 et les décisions D1-D5 sont validées (l'IA propose/l'humain
valide, sourcé ou silencieux, corpus fermé, anonymisation 09-08, voix purgée). Reste à
trancher COMMENT : quels modèles, où tournent-ils, qui parle à qui. Contraintes : serveur
Dokploy du partenaire (CPU, pas de GPU garanti), corpus en français, données d'un client
marocain (loi 09-08), CI sans secret, démo qui marche sans dépendance externe.
## Décision
1. **Embeddings : locaux, sur CPU**`fastembed` (ONNX, sans PyTorch) avec
`sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` (384 dimensions, multilingue, ~120 Mo, registre fastembed). Les textes du
client ne quittent JAMAIS le serveur pour l'indexation ni pour la recherche. Le modèle est
téléchargé au build de l'image (pas au démarrage). En **tests/CI : embeddeur déterministe
par hachage** (pas de téléchargement, pas de flottement) derrière la même interface.
2. **Stockage vectoriel : pgvector** dans le PostgreSQL existant (extension déjà installée
depuis R0) — table `RagChunk` gérée par la **migration Prisma** (le schéma reste la
propriété d'`apps/api`, source unique). Pas de base vectorielle de plus à opérer.
3. **Génération (réponses rédigées de l'assistant) : opt-in par configuration.**
- Sans clé (`AI_GENERATION=off`, défaut) : l'assistant fonctionne en **mode extractif**
il montre les meilleurs extraits sourcés et une synthèse templatée, sans LLM. La démo,
la CI et un déploiement sans budget API restent pleinement fonctionnels et honnêtes.
- Avec clé (`AI_GENERATION=api` + `AI_API_KEY`) : rédaction par un LLM externe, sur textes
DÉJÀ anonymisés (D4), avec l'obligation de citer les extraits fournis — jamais au-delà.
- La **suggestion de codes de bilan** n'utilise PAS de LLM : similarité sémantique
(embeddings locaux) entre la description et les libellés des référentiels + les bilans
historiques du parc. Déterministe, testable, explicable (« 9 bilans similaires »).
4. **Topologie** : `apps/ai` (FastAPI/uv, conteneur `siop2-ai`) n'est **jamais exposé**
réseau interne Dokploy uniquement, comme MinIO. Les utilisateurs passent par l'API NestJS
(auth JWT + matrice de permissions réutilisées) qui proxifie vers `siop2-ai`
(`AI_SERVICE_URL` + secret partagé `AI_SERVICE_TOKEN`). `apps/ai` lit Postgres
(métadonnées, bilans, écriture des chunks) et MinIO (octets des PDF) en direct sur le
réseau privé — la règle d'or ESLint d'ADR-001 concerne le code TypeScript de l'API,
la topologie « MinIO jamais exposé » du runbook reste respectée.
## Conséquences
- L'anonymisation (D4) s'applique **à l'ingestion** — les index ne contiennent jamais
d'identités ; le mode génératif n'envoie donc que des textes déjà nettoyés.
- Le mode extractif est le contrat de base : toute recette R5 doit passer SANS clé API.
- Si le partenaire veut un jour une génération 100 % locale (llama.cpp…), seul le point 3
change — interface `Generateur` prévue pour ça.

View File

@@ -4,6 +4,23 @@ Trace chronologique des sessions (la plus récente en premier). Le **playbook**
---
## 2026-07-17 — Pr. Daaif (+ Claude) — R5.1 : socle `apps/ai` — ingestion anonymisée + recherche sémantique
**Actions**
- **ADR-004 actée** : embeddings **locaux sur CPU** (fastembed ONNX, `paraphrase-multilingual-MiniLM-L12-v2`, 384 dims — les textes du client ne quittent jamais le serveur), pgvector dans le Postgres existant (table `RagChunk` **possédée par Prisma**, migration `r5_ia` + état de corpus sur Document), génération **opt-in** (`AI_GENERATION=off` par défaut : mode extractif honnête, la recette doit passer sans clé API), suggestion de bilan sans LLM (similarité sémantique, explicable). Topologie : `siop2-ai` jamais exposé, joint par l'API NestJS seule (`X-Service-Token`).
- **`apps/ai` posé** (FastAPI + uv, python 3.11) : config validée au démarrage, `/healthz`, `/internal/reindex` (idempotent) et `/internal/search` protégés par le jeton de service. **Pipeline** : PDF MinIO → texte par page (pypdf) → **anonymisation D4** (e-mails, téléphones marocains, noms connus de la base — insensible casse/accents, fonction pure) → découpage (~900 car., chevauchement, testé) → embeddings → `RagChunk` avec localisateur (« p. 42 », « bilan du 17/07 »). Bilans codés clôturés ingérés aussi (« sur votre parc… »). L'exclusion de corpus (D3) s'applique à l'ingestion ET à la lecture.
- **14 pytest verts + ruff** (embeddeur **déterministe** en test/CI — aucun téléchargement, même interface 384 dims) ; **job CI `ai`** (uv), deploy en dépend.
- **Vérifié en réel avec le vrai modèle ONNX** : réindexation du corpus seedé en 7 s (1 PDF réel → 31 extraits paginés, 3 bilans), recherche sémantique concluante (PDF trouvé par le sens, bilans du parc par « frottement des guides »), e-mails du PDF remplacés par ⟨contact⟩, **0 identité dans les chunks** (contrôle SQL sur les 9 noms seedés).
**Décisions**
- Convention de test R5 : pytest unitaires purs en CI (sans DB ni réseau) ; l'intégration réelle (DB + MinIO + modèle) se vérifie en local et en recette.
**Prochaine étape** : R5.2 — l'assistant au contrat (proxy NestJS authentifié → `siop2-ai`, mode extractif sourcé « sourcé ou silencieux ») + suggestion de codes de bilan. Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`.
---
## 2026-07-17 — Pr. Daaif (+ Claude) — R5 ouverte : maquettes IA (design d'abord)
**Actions**
@@ -13,9 +30,9 @@ Trace chronologique des sessions (la plus récente en premier). Le **playbook**
**Décisions**
- Aucune actée — les 5 décisions **attendent la validation du référent avec les écrans**. Aucune ligne de code `apps/ai` avant (principe n°1). L'architecture (pgvector, choix des modèles) se tranchera en **ADR-004** au lancement du socle R5.1.
- **→ Levé le 17/07/2026 : maquettes R5 et les 5 décisions (D1-D5) VALIDÉES par le référent.** Lancement R5.1 (socle `apps/ai` + ADR-004).
**Prochaine étape** : validation référent (maquettes + décisions) → R5.1 socle `apps/ai` (FastAPI/uv, ingestion + ADR-004). Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`.
**Prochaine étape** : R5.1 — ADR-004 (embeddings/modèles), migration `r5_ia` (chunks pgvector, état de corpus), pipeline d'ingestion anonymisé et testé. Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`.
---