apps/ai : /internal/ask — seuil de pertinence, extraits sourcés ou refus honnête portant la taille du corpus cherché (D2), rédaction via le Generateur opt-in ; /internal/suggest — similarité sémantique entre la description libre et les libellés ACTIFS des référentiels, un code par champ, confiance FORTE/MOYENNE, « N bilans similaires sur ce parc ». Sans LLM : déterministe, explicable. 23 pytest. Contrat (74 opérations) : POST /assistant/ask → AssistantAnswer (EXTRACTIVE/GENERATED/REFUSAL, extraits cités, corpus cherché) et POST /assistant/suggest-bilan (codes existants seulement) ; clients web/mobile régénérés. API NestJS : module assistant — proxy vers siop2-ai (AI_SERVICE_URL/ AI_SERVICE_TOKEN, ADR-004 §4), permissions matrice (ask=view, suggest=edit), traduction interne→contrat, 503 propre si service éteint. 6 e2e sur stub HTTP (76 tests API). Bug débusqué par la vraie chaîne : fastembed ne norme pas ses vecteurs — la similarité des suggestions dépassait 1 (pgvector normalisait dans son opérateur, masquant l'écart). Normalisation à l'encodage + réindexation : bilans en tête (0.41), refus hors corpus, scores cosinus ≤ 1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@siop/ai — service IA (R5)
FastAPI + uv (ADR-001), jamais exposé : seule l'API NestJS le contacte avec
X-Service-Token (ADR-004 §4). Maquettes et décisions D1-D5 validées le 17/07/2026.
Lancer (dev)
cd apps/ai
uv sync --extra embeddings # le vrai modèle ONNX (CPU, ~120 Mo au premier run)
uv run uvicorn siop_ai.app:app --port 8000
# CI / tests : uv sync && uv run pytest (embeddeur déterministe, aucun téléchargement)
Variables (défauts dev dans config.py, gabarit dans .env.example) :
DATABASE_URL, MINIO_*, AI_SERVICE_TOKEN, AI_EMBEDDINGS=locale|deterministe,
et la génération opt-in (ADR-004 §3) : AI_GENERATION=off|api, AI_API_KEY
(exigée en mode api — le boot refuse sinon, jamais loguée ni exposée),
AI_MODEL (défaut claude-opus-4-8). Mode api : uv sync --extra generation
(SDK officiel anthropic) ; tout échec du LLM retombe sur le mode extractif.
Ce que porte R5.1 (socle)
- Ingestion anonymisée (D4) : PDF de la bibliothèque (MinIO) page par page +
bilans codés clôturés → anonymisation (e-mails, téléphones, noms connus de la
base) → découpage → embeddings locaux →
RagChunk(pgvector, schéma Prisma). - Recherche sémantique
/internal/search: extraits sourcés (document + page ou bilan daté) avec score — la brique de « sourcé ou silencieux » (D2). /internal/reindexidempotent ; l'exclusion de corpus (Document.inCorpus, D3) s'applique à l'ingestion ET à la lecture.
La suite : R5.2 assistant (mode extractif puis génération opt-in) + suggestion de
bilan ; R5.3 écrans ; durcissement : Dockerfile + compose Dokploy (siop2-ai).