mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 20:51:53 +00:00
Recette (mode extractif, aucune clé) — elle a invalidé le modèle R5.1 : - MiniLM-384 classait la page-réponse DERRIÈRE des passages sans rapport (0,24 vs 0,41 sur la question type du plan) → bascule mesurée vers paraphrase-multilingual-mpnet-base-v2 (768 d, local/CPU), ADR-004 amendé avec le banc comparatif (e5-large écarté : 2,2 Go, scores compressés). - Migration r5_embeddings_mpnet : pgvector 384 → 768, index vidé (re-dérivable par « Réindexer tout »). - Découpage affiné (~350 caractères) : la phrase-réponse ne se noie plus, l'extrait cité est lisible ; seuils par défaut recalés 0,45/0,40/0,55. - Rejouée après bascule : réponse sourcée p. 2 en tête, refus honnête chiffré, suggestions étagées — 16/16 Playwright, 78 API, 23 pytest. - Revue pixel publiée (6 écrans réels vs maquettes, 3 arbitrages). Durcissement : - apps/ai/Dockerfile : uv, modèle ONNX téléchargé AU BUILD (ADR-004 §1), non-root, healthcheck ; répétition locale conteneurisée validée (healthz, reindex via MinIO/pgvector, 401 sans jeton, refus de boot api-sans-clé, réponse sourcée depuis le conteneur). - Compose Dokploy : siop2-ai interne (jamais sur dokploy-network, AI_SERVICE_TOKEN requis, génération opt-in, seuils par env) ; siop2-api branché (AI_SERVICE_URL). - Runbook §5-6 : service IA en production, calibrage des seuils sur le corpus client, réindexation post-déploiement. Le tag release/r5 attend la validation de la revue pixel par le référent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
52 lines
1.8 KiB
Python
52 lines
1.8 KiB
Python
"""Logique de l'assistant testée sans base : le seuil « sourcé ou silencieux »
|
|
et la sélection des suggestions — la partie SQL est couverte par la recette
|
|
réelle (convention R5.1 : pytest purs en CI)."""
|
|
|
|
from siop_ai.assistant import (
|
|
CHAMPS_BILAN,
|
|
SEUIL_CONFIANCE_FORTE,
|
|
SEUIL_PERTINENCE,
|
|
SEUIL_SUGGESTION,
|
|
_cosinus,
|
|
)
|
|
from siop_ai.embeddings import EmbeddeurDeterministe
|
|
|
|
|
|
def test_les_seuils_sont_ordonnes():
|
|
# Pertinence et suggestion vivent dans des pipelines distincts (question →
|
|
# passages vs description → libellés) : pas d'ordre imposé entre eux.
|
|
# L'invariant : une suggestion tout juste retenue n'est jamais « forte ».
|
|
assert 0 < SEUIL_PERTINENCE < 1
|
|
assert 0 < SEUIL_SUGGESTION < SEUIL_CONFIANCE_FORTE < 1
|
|
|
|
|
|
def test_champs_bilan_couvrent_les_six_champs_du_contrat():
|
|
assert set(CHAMPS_BILAN) == {
|
|
"DOOR_STATE",
|
|
"CABIN_POSITION",
|
|
"ANOMALY",
|
|
"EXTERNAL_CAUSE",
|
|
"ACTION_TAKEN",
|
|
"COMPONENT_CONCERNED",
|
|
}
|
|
|
|
|
|
def test_similarite_discrimine_le_bon_code():
|
|
"""Le cœur de la suggestion : une description de panne de porte doit être
|
|
plus proche du code « portes » que d'un code sans rapport."""
|
|
embeddeur = EmbeddeurDeterministe()
|
|
description, porte, treuil = embeddeur.encoder(
|
|
[
|
|
"la porte cabine rebondit, cellule encrassée, nettoyage barrière porte",
|
|
"anomalie constatée : cellule ou barrière de porte encrassée",
|
|
"élément concerné : treuil et moteur de traction",
|
|
]
|
|
)
|
|
assert _cosinus(description, porte) > _cosinus(description, treuil)
|
|
|
|
|
|
def test_cosinus_de_vecteurs_normes():
|
|
embeddeur = EmbeddeurDeterministe()
|
|
[v] = embeddeur.encoder(["contrôle mensuel des portes palières"])
|
|
assert abs(_cosinus(v, v) - 1.0) < 1e-6
|