mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
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:
37
apps/ai/tests/test_app.py
Normal file
37
apps/ai/tests/test_app.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Le service n'est jamais public : sans le jeton de service, 401 partout
|
||||
(la santé exceptée — sonde d'infra qui ne révèle rien)."""
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from siop_ai.app import app
|
||||
|
||||
|
||||
def test_sante_publique_interne():
|
||||
reponse = _client_sans_db().get("/healthz")
|
||||
assert reponse.status_code == 200
|
||||
assert reponse.json()["service"] == "siop2-ai"
|
||||
|
||||
|
||||
def test_endpoints_internes_refuses_sans_jeton():
|
||||
client = _client_sans_db()
|
||||
assert client.post("/internal/reindex").status_code == 401
|
||||
assert client.post("/internal/search", json={"question": "couple de serrage ?"}).status_code == 401
|
||||
|
||||
|
||||
def test_question_trop_courte_rejetee_avant_tout():
|
||||
reponse = _client_sans_db().post(
|
||||
"/internal/search",
|
||||
json={"question": "ab"},
|
||||
headers={"X-Service-Token": "dev-only-ai-token"},
|
||||
)
|
||||
assert reponse.status_code == 422
|
||||
|
||||
|
||||
def _client_sans_db() -> TestClient:
|
||||
"""TestClient HORS gestionnaire de contexte : le lifespan (pool DB,
|
||||
modèle d'embeddings) ne tourne pas — on pose l'état minimal. Les tests
|
||||
d'intégration DB se font en local (recette), pas en CI (convention R5.1)."""
|
||||
from siop_ai.config import charger_reglages
|
||||
|
||||
app.state.reglages = charger_reglages()
|
||||
return TestClient(app, raise_server_exceptions=False)
|
||||
Reference in New Issue
Block a user