whassan's picture
Deploy Sensorflow Studio
6b3874e verified
Raw History Blame Contribute Delete
5.06 kB
"""FastAPI router for the Agentic Retrospective Safety Analyzer.
Mounted at /api/retro by app_backend.py. Artifacts persist under runs/retro/.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Dict, Optional
from fastapi import APIRouter, Body, HTTPException, Query
from sensorflow.retro import store
from sensorflow.retro.agent.orchestrator import analyze
from sensorflow.retro.inference.client import all_backend_statuses
from sensorflow.retro.inference.compat import check_vllm_compatibility, format_report
from sensorflow.retro.inference.env_detect import detect_environment
from sensorflow.retro.rag.evalharness import run_retrieval_eval
from sensorflow.retro.rag.retriever import get_index
from sensorflow.retro.tools.builtin import FIXTURES_DIR, build_registry
router = APIRouter(prefix="/api/retro", tags=["retro"])
@router.get("/env")
def env_report():
env = detect_environment()
return {"status": "ok", "environment": env.model_dump()}
@router.get("/compat")
def compat_report(model_name: str = "meta-llama/Llama-3.1-8B-Instruct",
quantization: str = "none"):
env = detect_environment(probe_ollama=False)
report = check_vllm_compatibility(env, model_name=model_name,
quantization=quantization)
return {"status": "ok", "report": report.model_dump(),
"formatted": format_report(report)}
@router.get("/backends")
def backends_status():
return {"status": "ok",
"backends": [s.model_dump() for s in all_backend_statuses()]}
@router.get("/rag/search")
def rag_search(q: str = Query(min_length=3), k: int = Query(4, ge=1, le=10)):
index = get_index()
hits = index.search(q, k=k)
return {"status": "ok", "store_backend": index.backend_name,
"hits": [h.model_dump() for h in hits]}
@router.get("/rag/eval")
def rag_eval(k: int = Query(4, ge=1, le=10)):
return {"status": "ok", "report": run_retrieval_eval(k=k).model_dump()}
@router.get("/fixtures")
def list_fixtures():
out = []
for p in sorted(Path(FIXTURES_DIR).glob("*.json")):
try:
data = json.loads(p.read_text())
out.append({
"fixture_id": p.stem,
"evaluation_id": data.get("evaluation_id"),
"description": (data.get("scenario") or {}).get("description", ""),
"weather": (data.get("scenario") or {}).get("weather"),
})
except (json.JSONDecodeError, OSError):
continue
return {"status": "ok", "fixtures": out}
@router.post("/analyze")
def analyze_failure(fixture_id: Optional[str] = None,
backend: str = Query("mock", pattern="^(mock|ollama|vllm)$"),
log: Optional[Dict[str, Any]] = Body(None)):
"""Analyze a fixture by id, or an uploaded JSON eval log in the body."""
if not fixture_id and not log:
raise HTTPException(status_code=400,
detail="provide ?fixture_id= or a JSON log body")
try:
if fixture_id:
scorecard = analyze(fixture_id=fixture_id, backend=backend)
else:
eval_id = log.get("evaluation_id", "uploaded")
safe = "".join(c for c in eval_id if c.isalnum() or c in "-_")[:64]
upload_path = store.uploads_dir() / f"{safe or 'uploaded'}.json"
upload_path.write_text(json.dumps(log, indent=2))
scorecard = analyze(path=str(upload_path), backend=backend)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc))
except RuntimeError as exc: # backend unavailable
raise HTTPException(status_code=503, detail=str(exc))
return {"status": "ok",
"scorecard": json.loads(scorecard.model_dump_json()),
"markdown": scorecard.render_markdown()}
@router.get("/analyses")
def list_analyses():
return {"status": "ok", "analyses": store.list_analyses()}
@router.get("/analyses/{evaluation_id}")
def get_analysis(evaluation_id: str):
data = store.load_analysis(evaluation_id)
if data is None:
raise HTTPException(status_code=404,
detail=f"no analysis for {evaluation_id}")
return {"status": "ok", **data}
@router.get("/analyses/{evaluation_id}/audit")
def get_audit(evaluation_id: str):
data = store.load_analysis(evaluation_id)
if data is None:
raise HTTPException(status_code=404,
detail=f"no analysis for {evaluation_id}")
audit_id = data.get("audit_analysis_id", evaluation_id)
return {"status": "ok", "audit_analysis_id": audit_id,
"records": store.read_audit(audit_id)}
@router.get("/tools")
def tool_registry_listing():
reg = build_registry(persist_audit=False)
return {"status": "ok",
"tools": [s.model_dump() for s in reg.specs()],
"note": "write tools require policy_authorization=true per call; "
"all calls are audited"}