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:
pr-daaif
2026-07-17 14:38:36 +01:00
parent d4d73a6f76
commit 76c2ccdfb1
10 changed files with 364 additions and 8 deletions

20
apps/ai/.env.example Normal file
View File

@@ -0,0 +1,20 @@
# Copier en .env pour le dev local (défauts alignés sur infra/docker-compose.yml).
DATABASE_URL=postgresql://siop:siop@localhost:5432/siop
MINIO_ENDPOINT=localhost
MINIO_PORT=9000
MINIO_ACCESS_KEY=siop
MINIO_SECRET_KEY=siop-minio
MINIO_BUCKET=siop2
# Seule l'API NestJS connaît ce secret (ADR-004 §4 — service jamais public).
AI_SERVICE_TOKEN=dev-only-ai-token
# Embeddings : locale (fastembed ONNX, CPU) | deterministe (tests/CI)
AI_EMBEDDINGS=locale
# Génération (ADR-004 §3) : off = mode extractif (défaut — la recette passe
# sans clé) | api = rédaction par le LLM sur textes déjà anonymisés (D4).
# AI_GENERATION=api exige AI_API_KEY (le boot refuse sinon).
AI_GENERATION=off
AI_API_KEY=
AI_MODEL=claude-opus-4-8

View File

@@ -12,8 +12,12 @@ uv run uvicorn siop_ai.app:app --port 8000
# CI / tests : uv sync && uv run pytest (embeddeur déterministe, aucun téléchargement) # CI / tests : uv sync && uv run pytest (embeddeur déterministe, aucun téléchargement)
``` ```
Variables (défauts dev dans `config.py`) : `DATABASE_URL`, `MINIO_*`, Variables (défauts dev dans `config.py`, gabarit dans `.env.example`) :
`AI_SERVICE_TOKEN`, `AI_EMBEDDINGS=locale|deterministe`, `AI_GENERATION=off|api`. `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) ## Ce que porte R5.1 (socle)

View File

@@ -15,6 +15,8 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
# Le vrai modèle (ONNX, CPU) — absent des tests/CI (embeddeur déterministe). # Le vrai modèle (ONNX, CPU) — absent des tests/CI (embeddeur déterministe).
embeddings = ["fastembed>=0.4"] embeddings = ["fastembed>=0.4"]
# Génération opt-in (ADR-004 §3) — absente des tests/CI (repli extractif).
generation = ["anthropic>=0.75"]
[dependency-groups] [dependency-groups]
dev = [ dev = [

View File

@@ -12,6 +12,7 @@ from pydantic import BaseModel, Field
from .config import Reglages, charger_reglages from .config import Reglages, charger_reglages
from .embeddings import construire_embeddeur from .embeddings import construire_embeddeur
from .generation import construire_generateur
from .ingestion import reindexer_tout from .ingestion import reindexer_tout
from .recherche import chercher from .recherche import chercher
@@ -21,6 +22,9 @@ async def cycle_de_vie(app: FastAPI):
reglages = charger_reglages() reglages = charger_reglages()
app.state.reglages = reglages app.state.reglages = reglages
app.state.embeddeur = construire_embeddeur(reglages.ai_embeddings) app.state.embeddeur = construire_embeddeur(reglages.ai_embeddings)
app.state.generateur = construire_generateur(
reglages.ai_generation, reglages.ai_api_key, reglages.ai_model
)
app.state.pool = await asyncpg.create_pool(reglages.database_url, min_size=1, max_size=5) app.state.pool = await asyncpg.create_pool(reglages.database_url, min_size=1, max_size=5)
yield yield
await app.state.pool.close() await app.state.pool.close()
@@ -39,8 +43,13 @@ def verifier_jeton(
@app.get("/healthz") @app.get("/healthz")
async def sante() -> dict: async def sante() -> dict:
"""Sonde interne (compose/Dokploy) — ne révèle rien du corpus.""" """Sonde interne (compose/Dokploy) — ne révèle ni corpus ni secret."""
return {"status": "ok", "service": "siop2-ai"} reglages = getattr(app.state, "reglages", None) or charger_reglages()
return {
"status": "ok",
"service": "siop2-ai",
"generation": reglages.ai_generation, # « off » = extractif — jamais la clé
}
@app.post("/internal/reindex", dependencies=[Depends(verifier_jeton)]) @app.post("/internal/reindex", dependencies=[Depends(verifier_jeton)])

View File

@@ -18,8 +18,12 @@ class Reglages(BaseSettings):
ai_service_token: str = "dev-only-ai-token" ai_service_token: str = "dev-only-ai-token"
# Embeddings : « locale » (fastembed ONNX) ou « deterministe » (tests/CI) # Embeddings : « locale » (fastembed ONNX) ou « deterministe » (tests/CI)
ai_embeddings: str = "locale" ai_embeddings: str = "locale"
# Génération : « off » (mode extractif, défaut honnête) ou « api » (opt-in) # Génération (ADR-004 §3) : « off » = mode extractif (défaut honnête,
# la recette passe sans clé) ; « api » = rédaction par le LLM externe,
# sur textes DÉJÀ anonymisés (D4), citations obligatoires.
ai_generation: str = "off" ai_generation: str = "off"
ai_api_key: str = "" # requise seulement si ai_generation=api — jamais loguée
ai_model: str = "claude-opus-4-8"
model_config = {"env_prefix": "", "case_sensitive": False} model_config = {"env_prefix": "", "case_sensitive": False}
@@ -29,4 +33,9 @@ def charger_reglages() -> Reglages:
# asyncpg ne comprend pas le paramètre ?schema= de Prisma # asyncpg ne comprend pas le paramètre ?schema= de Prisma
if "?" in reglages.database_url: if "?" in reglages.database_url:
reglages.database_url = reglages.database_url.split("?")[0] reglages.database_url = reglages.database_url.split("?")[0]
# Même philosophie que l'API NestJS : une config invalide refuse de booter.
if reglages.ai_generation not in ("off", "api"):
raise ValueError("AI_GENERATION doit valoir « off » ou « api »")
if reglages.ai_generation == "api" and not reglages.ai_api_key:
raise ValueError("AI_GENERATION=api exige AI_API_KEY (voir ADR-004 §3)")
return reglages return reglages

View File

@@ -0,0 +1,94 @@
"""Génération des réponses rédigées (ADR-004 §3) — OPT-IN par configuration.
- « off » (défaut) : mode extractif — pas de LLM, l'assistant montrera les
extraits sourcés tels quels. La recette R5 passe entièrement dans ce mode.
- « api » : rédaction par Claude (SDK officiel), sur des extraits DÉJÀ
anonymisés (D4), avec l'obligation de ne rien affirmer hors extraits (D2).
Tout échec (refus, réseau, quota) retombe sur le mode extractif — jamais
d'erreur utilisateur à cause du LLM.
"""
import logging
from typing import Protocol
from .recherche import ExtraitTrouve
journal = logging.getLogger("siop_ai.generation")
CONSIGNE = """Tu es l'assistant SIOP d'une société de maintenance d'ascenseurs.
Tu réponds en français, UNIQUEMENT à partir des extraits fournis (notices et
historiques d'intervention du parc, déjà anonymisés).
Règles absolues :
- chaque affirmation porte sa citation [n] renvoyant à un extrait fourni ;
- si les extraits ne portent pas la réponse, dis-le et n'invente RIEN ;
- reste bref et opérationnel : un technicien te lit sur le terrain ;
- termine toujours par le rappel que l'humain vérifie avant d'agir."""
class Generateur(Protocol):
def rediger(self, question: str, extraits: list[ExtraitTrouve]) -> str | None: ...
class GenerateurExtractif:
"""Le contrat de base : pas de rédaction — l'appelant montre les extraits.
`None` signifie « pas de texte généré », jamais une erreur."""
def rediger(self, question: str, extraits: list[ExtraitTrouve]) -> str | None:
return None
def construire_invite(question: str, extraits: list[ExtraitTrouve]) -> str:
"""L'invite utilisateur — fonction pure, testée : la question et les
extraits numérotés, rien d'autre (les textes sont déjà anonymisés)."""
blocs = [
f"[{rang}] {e.titre} · {e.locator}\n{e.content}"
for rang, e in enumerate(extraits, start=1)
]
return "Extraits du corpus :\n\n" + "\n\n".join(blocs) + f"\n\nQuestion : {question}"
class GenerateurAPI:
"""Rédaction par Claude — import différé : la dépendance `anthropic` est
optionnelle (groupe `generation`), absente des tests/CI."""
def __init__(self, api_key: str, modele: str) -> None:
from anthropic import Anthropic
self._client = Anthropic(api_key=api_key)
self._modele = modele
def rediger(self, question: str, extraits: list[ExtraitTrouve]) -> str | None:
import anthropic
if not extraits:
return None # sourcé ou silencieux (D2) : rien à citer = rien à rédiger
try:
reponse = self._client.messages.create(
model=self._modele,
max_tokens=2048, # réponses courtes et sourcées, par conception
thinking={"type": "adaptive"},
system=CONSIGNE,
messages=[
{"role": "user", "content": construire_invite(question, extraits)}
],
)
if reponse.stop_reason == "refusal":
journal.warning("Génération refusée par le modèle — repli extractif")
return None
texte = "".join(b.text for b in reponse.content if b.type == "text").strip()
return texte or None
except anthropic.RateLimitError:
journal.warning("Quota API atteint — repli extractif")
return None
except anthropic.APIStatusError as e:
journal.warning("API génération %s — repli extractif", e.status_code)
return None
except anthropic.APIConnectionError:
journal.warning("API génération injoignable — repli extractif")
return None
def construire_generateur(mode: str, api_key: str, modele: str) -> Generateur:
if mode == "api":
return GenerateurAPI(api_key, modele)
return GenerateurExtractif()

View 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 ?")

138
apps/ai/uv.lock generated
View File

@@ -26,6 +26,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
] ]
[[package]]
name = "anthropic"
version = "0.117.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "docstring-parser" },
{ name = "httpx" },
{ name = "jiter" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/41/0d/8f71d535edb0d438f023bd825fb65f67c14fa88a2bd6b75f292a58a63de4/anthropic-0.117.0.tar.gz", hash = "sha256:98107f2b76439641e0ae2a1754087534b8f178dbab99d6eb1bc4b7bc8c744496", size = 989933, upload-time = "2026-07-16T19:36:13.07Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/4c/917d21d6619a4475cdafc6d13a69fdb3b901ddac57e76caca5a25c117b6d/anthropic-0.117.0-py3-none-any.whl", hash = "sha256:451a0a6905f11dff7663d13e4ee5dbf909eb8942b1d049803c7b937a13ac47ec", size = 998327, upload-time = "2026-07-16T19:36:11.225Z" },
]
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "4.14.2" version = "4.14.2"
@@ -332,6 +351,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
] ]
[[package]]
name = "distro"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
]
[[package]]
name = "docstring-parser"
version = "0.18.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" },
]
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.139.2" version = "0.139.2"
@@ -538,6 +575,92 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
] ]
[[package]]
name = "jiter"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" },
{ url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" },
{ url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" },
{ url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" },
{ url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" },
{ url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" },
{ url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" },
{ url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" },
{ url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" },
{ url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" },
{ url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" },
{ url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" },
{ url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" },
{ url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" },
{ url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" },
{ url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" },
{ url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" },
{ url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" },
{ url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" },
{ url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" },
{ url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" },
{ url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" },
{ url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" },
{ url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" },
{ url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" },
{ url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" },
{ url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" },
{ url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" },
{ url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" },
{ url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" },
{ url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" },
{ url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" },
{ url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" },
{ url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" },
{ url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" },
{ url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" },
{ url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" },
{ url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" },
{ url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" },
{ url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" },
{ url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" },
{ url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" },
{ url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" },
{ url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" },
{ url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" },
{ url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" },
{ url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" },
{ url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" },
{ url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" },
{ url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" },
{ url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" },
{ url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" },
{ url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" },
{ url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" },
{ url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" },
{ url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" },
{ url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" },
{ url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" },
{ url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" },
{ url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" },
{ url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" },
{ url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" },
{ url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" },
{ url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" },
{ url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" },
{ url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" },
{ url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" },
{ url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" },
{ url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" },
{ url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" },
{ url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" },
{ url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" },
{ url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" },
{ url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" },
{ url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" },
]
[[package]] [[package]]
name = "loguru" name = "loguru"
version = "0.7.3" version = "0.7.3"
@@ -1349,6 +1472,9 @@ dependencies = [
embeddings = [ embeddings = [
{ name = "fastembed" }, { name = "fastembed" },
] ]
generation = [
{ name = "anthropic" },
]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
@@ -1360,6 +1486,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "anthropic", marker = "extra == 'generation'", specifier = ">=0.75" },
{ name = "asyncpg", specifier = ">=0.30" }, { name = "asyncpg", specifier = ">=0.30" },
{ name = "fastapi", specifier = ">=0.115" }, { name = "fastapi", specifier = ">=0.115" },
{ name = "fastembed", marker = "extra == 'embeddings'", specifier = ">=0.4" }, { name = "fastembed", marker = "extra == 'embeddings'", specifier = ">=0.4" },
@@ -1368,7 +1495,7 @@ requires-dist = [
{ name = "pypdf", specifier = ">=5.1" }, { name = "pypdf", specifier = ">=5.1" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.32" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.32" },
] ]
provides-extras = ["embeddings"] provides-extras = ["embeddings", "generation"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
@@ -1378,6 +1505,15 @@ dev = [
{ name = "ruff", specifier = ">=0.8" }, { name = "ruff", specifier = ">=0.8" },
] ]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]] [[package]]
name = "starlette" name = "starlette"
version = "1.3.1" version = "1.3.1"

View File

@@ -21,8 +21,12 @@
- Sans clé (`AI_GENERATION=off`, défaut) : l'assistant fonctionne en **mode extractif** - 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, 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. 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 - Avec clé (`AI_GENERATION=api` + `AI_API_KEY`, modèle configurable par `AI_MODEL`,
DÉJÀ anonymisés (D4), avec l'obligation de citer les extraits fournis — jamais au-delà. défaut `claude-opus-4-8`) : rédaction par le SDK officiel Anthropic, sur textes DÉJÀ
anonymisés (D4), avec l'obligation de citer les extraits fournis — jamais au-delà.
La config est validée au boot (api sans clé = refus de démarrer) ; la clé n'apparaît
ni dans les journaux ni au `/healthz` (qui n'expose que le mode). Tout échec du LLM
(refus, quota, réseau) retombe silencieusement sur le mode extractif.
- La **suggestion de codes de bilan** n'utilise PAS de LLM : similarité sémantique - 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 (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 »). historiques du parc. Déterministe, testable, explicable (« 9 bilans similaires »).

View File

@@ -4,6 +4,22 @@ Trace chronologique des sessions (la plus récente en premier). Le **playbook**
--- ---
## 2026-07-17 — Pr. Daaif (+ Claude) — R5.1+ : clé API de génération configurable (demande du référent)
**Actions**
- **`generation.py`** : l'interface `Generateur` d'ADR-004 §3 prend corps — `GenerateurExtractif` (contrat de base, aucun LLM) et `GenerateurAPI` (SDK officiel `anthropic`, dépendance **optionnelle** `--extra generation`, absente des tests/CI). Consigne système : citations [n] obligatoires depuis les extraits fournis, « n'invente RIEN », rappel de validation humaine. Tout échec (refus du modèle, quota, réseau) **retombe sur l'extractif** — jamais d'erreur utilisateur imputable au LLM ; `stop_reason == "refusal"` traité explicitement.
- **Configuration complète et validée au boot** : `AI_GENERATION=off|api`, `AI_API_KEY` (exigée en mode api — le démarrage refuse sinon, message clair), `AI_MODEL` (défaut `claude-opus-4-8`). `.env.example` posé ; `/healthz` expose le **mode** (off/api), jamais la clé ; le générateur rejoint l'état de l'app (R5.2 le consommera).
- 5 tests ajoutés (19 pytest au total) : défaut extractif, refus de boot api-sans-clé, mode inconnu refusé, clé/modèle lus de l'env, invite pure numérotant les extraits. Vérifié en réel : boot refusé sans clé, `GenerateurAPI` construit avec le SDK, healthz sans secret.
**Décisions**
- La clé vit en variable d'environnement (Dokploy secrets), pas en base ni dans l'UI — cohérent avec `JWT_SECRET` et ADR-002.
**Prochaine étape** : R5.2 — assistant au contrat (proxy NestJS), le mode extractif d'abord, la rédaction branchée sur ce `Generateur`. Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`.
---
## 2026-07-17 — Pr. Daaif (+ Claude) — R5.1 : socle `apps/ai` — ingestion anonymisée + recherche sémantique ## 2026-07-17 — Pr. Daaif (+ Claude) — R5.1 : socle `apps/ai` — ingestion anonymisée + recherche sémantique
**Actions** **Actions**