AVIS / core /llm /__init__.py
X2-0's picture
HF Clean Deploy
1c0c94d
Raw
History Blame Contribute Delete
4.28 kB
"""Provider-agnostic VLM client. The VLM is an *auditor*: it verifies a proposed
violation crop or abstains. Swapping Gemini (free) -> Claude/GPT-4o later is a config
change here, with no call-site edits.
Contract returned by ``verify``:
{
"verified": bool,
"confidence": float,
"reason": str,
"insufficient_evidence": bool,
"verifier_unavailable": bool,
}
"""
from __future__ import annotations
import json
import sys
import time
from collections.abc import Callable
from typing import Protocol
from core.config import Settings, get_settings
_PROMPT = (
"You are a traffic-enforcement auditor. Given the image and a PROPOSED violation, "
"decide ONLY whether the visible evidence supports it. Don't add new ones. "
"If the image is too unclear to judge, set insufficient_evidence=true.\n"
"Proposed violation: {vtype}\nEvidence note: {reason}\n"
'Reply with ONLY JSON: {{"verified": bool, "confidence": 0.0-1.0, '
'"reason": "<one sentence>", "insufficient_evidence": bool}}'
)
_UNAVAILABLE = {
"verified": False,
"confidence": 0.0,
"reason": "VLM unavailable or could not judge the evidence.",
"insufficient_evidence": False,
"verifier_unavailable": True,
}
def call_with_retry(fn: Callable, attempts: int = 3, base_delay: float = 2.0):
"""Run ``fn``; retry transient API errors (e.g. 429) with linear backoff."""
last: Exception | None = None
for i in range(attempts):
try:
return fn()
except Exception as e: # noqa: BLE001
last = e
if i < attempts - 1:
time.sleep(base_delay * (i + 1))
raise last # type: ignore[misc]
class LLMClient(Protocol):
enabled: bool
def verify(self, image_path: str, vtype: str, reason: str) -> dict: ...
class NullLLM:
"""No VLM configured. Disabled -> the pipeline escalates to human review."""
enabled = False
def verify(self, image_path: str, vtype: str, reason: str) -> dict:
return dict(_UNAVAILABLE)
class GeminiLLM:
"""Wraps the current ``google-genai`` SDK. Crops/images are sent as JPEG bytes."""
enabled = True
def __init__(self, api_key: str, model: str) -> None:
self._api_key = api_key
self._model_name = model
self._client = None
def _client_obj(self): # noqa: ANN202
if self._client is None:
from google import genai
self._client = genai.Client(api_key=self._api_key)
return self._client
def verify(self, image_path: str, vtype: str, reason: str) -> dict:
try:
from io import BytesIO
from google.genai import types
from PIL import Image
buf = BytesIO()
Image.open(image_path).convert("RGB").save(buf, format="JPEG")
prompt = _PROMPT.format(vtype=vtype, reason=reason)
resp = call_with_retry(
lambda: self._client_obj().models.generate_content(
model=self._model_name,
contents=[
prompt,
types.Part.from_bytes(
data=buf.getvalue(), mime_type="image/jpeg"
),
],
)
)
text = (resp.text or "").strip().removeprefix("```json").removeprefix("```")
text = text.removesuffix("```").strip()
data = json.loads(text)
return {
"verified": bool(data.get("verified", False)),
"confidence": float(data.get("confidence", 0.0)),
"reason": str(data.get("reason", "")),
"insufficient_evidence": bool(data.get("insufficient_evidence", False)),
"verifier_unavailable": False,
}
except Exception as e: # noqa: BLE001
print(f"[gemini.verify] {type(e).__name__}: {e}", file=sys.stderr)
return dict(_UNAVAILABLE)
def get_llm(settings: Settings | None = None) -> LLMClient:
settings = settings or get_settings()
if settings.llm_provider == "gemini" and settings.gemini_api_key:
return GeminiLLM(settings.gemini_api_key, settings.gemini_model)
return NullLLM()