mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r5.1): clé API de génération configurable (ADR-004 §3 en actes)
Demande du référent : la génération opt-in devient réellement configurable. AI_GENERATION=off|api, AI_API_KEY (exigée en mode api — le boot refuse sinon, jamais loguée, /healthz n'expose que le mode), AI_MODEL (défaut claude-opus-4-8). generation.py : interface Generateur — GenerateurExtractif (contrat de base sans LLM) et GenerateurAPI (SDK officiel anthropic, dépendance optionnelle --extra generation, absente des tests/CI). Consigne : citations [n] obligatoires depuis les extraits anonymisés, jamais d'invention, rappel de validation humaine. Tout échec (refus du modèle, quota, réseau) retombe silencieusement sur l'extractif. 5 tests ajoutés (19 pytest) + .env.example. Vérifié en réel : boot refusé api-sans-clé, générateur construit, healthz sans secret. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
62
apps/ai/tests/test_generation.py
Normal file
62
apps/ai/tests/test_generation.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""ADR-004 §3 — la génération est OPT-IN et configurable : clé, modèle, mode.
|
||||
Sans clé, le mode extractif est le contrat ; en mode api sans clé, le boot
|
||||
refuse (config validée au démarrage, comme l'API NestJS)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from siop_ai.config import charger_reglages
|
||||
from siop_ai.generation import GenerateurExtractif, construire_generateur, construire_invite
|
||||
from siop_ai.recherche import ExtraitTrouve
|
||||
|
||||
|
||||
def _extrait(titre: str, locator: str, content: str) -> ExtraitTrouve:
|
||||
return ExtraitTrouve(
|
||||
source_type="DOCUMENT",
|
||||
document_id="d-1",
|
||||
work_order_id=None,
|
||||
titre=titre,
|
||||
locator=locator,
|
||||
content=content,
|
||||
score=0.9,
|
||||
)
|
||||
|
||||
|
||||
def test_defaut_extractif_sans_cle():
|
||||
generateur = construire_generateur("off", "", "claude-opus-4-8")
|
||||
assert isinstance(generateur, GenerateurExtractif)
|
||||
assert generateur.rediger("couple de serrage ?", [_extrait("n", "p. 1", "x")]) is None
|
||||
|
||||
|
||||
def test_mode_api_sans_cle_refuse_au_boot(monkeypatch):
|
||||
monkeypatch.setenv("AI_GENERATION", "api")
|
||||
monkeypatch.delenv("AI_API_KEY", raising=False)
|
||||
with pytest.raises(ValueError, match="AI_API_KEY"):
|
||||
charger_reglages()
|
||||
|
||||
|
||||
def test_mode_inconnu_refuse_au_boot(monkeypatch):
|
||||
monkeypatch.setenv("AI_GENERATION", "toujours")
|
||||
with pytest.raises(ValueError, match="AI_GENERATION"):
|
||||
charger_reglages()
|
||||
|
||||
|
||||
def test_cle_et_modele_configurables(monkeypatch):
|
||||
monkeypatch.setenv("AI_GENERATION", "api")
|
||||
monkeypatch.setenv("AI_API_KEY", "sk-test-123")
|
||||
monkeypatch.setenv("AI_MODEL", "claude-opus-4-8")
|
||||
reglages = charger_reglages()
|
||||
assert reglages.ai_api_key == "sk-test-123"
|
||||
assert reglages.ai_model == "claude-opus-4-8"
|
||||
|
||||
|
||||
def test_invite_numerote_les_extraits_et_rien_d_autre():
|
||||
invite = construire_invite(
|
||||
"quel couple de serrage pour les guides ?",
|
||||
[
|
||||
_extrait("Notice Gen2.pdf", "p. 42", "Serrer à 25 N·m."),
|
||||
_extrait("OT-2026-0341", "bilan du 2026-07-17", "Coulisseaux remplacés."),
|
||||
],
|
||||
)
|
||||
assert "[1] Notice Gen2.pdf · p. 42" in invite
|
||||
assert "[2] OT-2026-0341 · bilan du 2026-07-17" in invite
|
||||
assert invite.endswith("Question : quel couple de serrage pour les guides ?")
|
||||
Reference in New Issue
Block a user