"""Jev (TypeSafe) client — structured `choice` matching over HTTP (phases 2-5). Contract (docs.typesafe.ai): POST /v1/systemone with {"state": ..., "model": , "questions": {key: {type, instructions, criteria}}} → {"answers": {key: {choice, confidence, probabilities}}, "usage": {...}, "model": "..."} Design rules (final plan v5): - The model version is PINNED (JEV_MODEL=jev-1.13.0 from phase 0); a response whose `model` differs logs a drift warning and is still returned (recorded). - Fail-soft: any error/timeout → None. Callers fall back per field to the existing difflib path; with JEV_ENABLED=false nothing here is called at all. - One retry only for 429/5xx (after a short backoff), then give up. """ import asyncio import logging import time import httpx from app.config import settings logger = logging.getLogger("bot.jev") _URL = "https://api.typesafe.ai/v1/systemone" async def ask(state: dict, questions: dict, *, timeout: float | None = None) -> dict | None: """One systemone request. Returns {"answers", "usage", "model", "latency_ms"} or None.""" if not settings.TYPESAFE_API_KEY: logger.warning("jev: TYPESAFE_API_KEY is not configured") return None payload = {"state": state, "model": settings.JEV_MODEL, "questions": questions} headers = { "Authorization": f"Bearer {settings.TYPESAFE_API_KEY}", "Content-Type": "application/json", } t0 = time.perf_counter() for attempt in (1, 2): try: async with httpx.AsyncClient(timeout=timeout or settings.JEV_TIMEOUT) as client: resp = await client.post(_URL, json=payload, headers=headers) if resp.status_code == 200: body = resp.json() or {} actual = str(body.get("model") or "") if actual and actual != settings.JEV_MODEL: logger.warning("jev: model drift — got %s, pinned %s", actual, settings.JEV_MODEL) return { "answers": body.get("answers") or {}, "usage": body.get("usage") or {}, "model": actual or settings.JEV_MODEL, "latency_ms": int((time.perf_counter() - t0) * 1000), } if resp.status_code in (429, 500, 502, 503, 504) and attempt == 1: logger.warning("jev: HTTP %s — retrying once", resp.status_code) await asyncio.sleep(1.0) continue logger.error("jev: HTTP %s: %s", resp.status_code, resp.text[:300]) return None except Exception as exc: if attempt == 1: logger.warning("jev: request failed (%s) — retrying once", exc) await asyncio.sleep(1.0) continue logger.error("jev: request failed: %s", exc) return None return None