whassan's picture
Deploy Sensorflow Studio (part 2)
e1c927b verified
Raw History Blame Contribute Delete
14.9 kB
"""REST surface for the ROTR capability: APIRouter under /api/rotr.
Request-scoped over file persistence (platform convention); no background
services. Endpoints cover the full loop: bank generation, detection runs,
violation inspection with taxonomy queries, the attribution matrix,
consequence replay, metrics + regression, clusters, HITL actions, flywheel
artifacts + contamination guard, stop-ship policy, and docs serving."""
from __future__ import annotations
from pathlib import Path
from typing import Dict, List, Optional
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from sensorflow.rotr import SOFTWARE_VERSION, pipeline, store
from sensorflow.rotr import flywheel as flywheel_mod
from sensorflow.rotr.flywheel import ContaminationError
from sensorflow.rotr.metrics import evaluate_candidate
from sensorflow.rotr.models import now_iso
from sensorflow.rotr.rules import CONFIG, RULES, RULESET_VERSION
from sensorflow.rotr.scenes import MODEL_PROFILES
from sensorflow.rotr.stopship import STOPSHIP_POLICY, policy_version
from sensorflow.rotr.taxonomy import ROTRQuery, matches, parse_query
router = APIRouter(prefix="/api/rotr", tags=["rotr"])
@router.get("/health")
def health() -> Dict:
return {"status": "ok", "software_version": SOFTWARE_VERSION,
"ruleset_version": RULESET_VERSION,
"stopship_policy_version": policy_version(),
"time": now_iso()}
# ------------------------------------------------------------ banks
class BankRequest(BaseModel):
n_scenarios: int = 28
seed: int = 7
model_version: str = "stack-v1"
@router.post("/banks")
def create_bank(req: BankRequest) -> Dict:
if req.model_version not in MODEL_PROFILES:
raise HTTPException(422, f"unknown model_version {req.model_version}; "
f"known: {sorted(MODEL_PROFILES)}")
if not (1 <= req.n_scenarios <= 500):
raise HTTPException(422, "n_scenarios must be in [1, 500]")
return pipeline.create_bank(req.n_scenarios, req.seed, req.model_version)
@router.get("/banks")
def list_banks() -> List[Dict]:
return pipeline.list_banks()
@router.get("/banks/{bank_id}")
def get_bank(bank_id: str) -> Dict:
doc = pipeline.load_bank(bank_id)
if doc is None:
raise HTTPException(404, f"unknown bank {bank_id}")
return pipeline.bank_summary(doc)
@router.get("/banks/{bank_id}/scenarios/{scenario_id}")
def get_scenario(bank_id: str, scenario_id: str) -> Dict:
sc = pipeline.load_scenario(bank_id, scenario_id)
if sc is None:
raise HTTPException(404, f"unknown scenario {scenario_id}")
return sc.model_dump()
# ------------------------------------------------------------ runs
class RunRequest(BaseModel):
bank_id: Optional[str] = None
n_scenarios: int = 28
seed: int = 7
model_version: str = "stack-v1"
forward_to_agentic: bool = False
@router.post("/runs")
def create_run(req: RunRequest) -> Dict:
try:
doc = pipeline.execute_run(req.bank_id, req.n_scenarios, req.seed,
req.model_version,
forward_to_agentic=req.forward_to_agentic)
except KeyError as e:
raise HTTPException(404, str(e))
except ValueError as e:
raise HTTPException(422, str(e))
return _run_summary(doc)
@router.get("/runs")
def list_runs() -> List[Dict]:
return pipeline.list_runs()
def _run_summary(doc: Dict) -> Dict:
return {k: doc[k] for k in
("run_id", "bank_id", "model_version", "ruleset_version",
"n_scenarios", "scenario_summaries", "metrics", "clusters",
"gate")} | {"n_violations": len(doc["violations"])}
@router.get("/runs/{run_id}")
def get_run(run_id: str) -> Dict:
doc = pipeline.load_run(run_id)
if doc is None:
raise HTTPException(404, f"unknown run {run_id}")
return _run_summary(doc)
# ------------------------------------------------------------ violations
@router.get("/runs/{run_id}/violations")
def list_violations(run_id: str, actor: Optional[str] = None,
vulnerability: Optional[str] = None,
legality: Optional[str] = None,
environment: Optional[str] = None,
interaction: Optional[str] = None,
behavior: Optional[str] = None,
road_geometry: Optional[str] = None,
traffic_control: Optional[str] = None,
visibility: Optional[str] = None,
lighting: Optional[str] = None,
weather: Optional[str] = None,
consequence_class: Optional[str] = None,
primary_layer: Optional[str] = None,
text: Optional[str] = None) -> Dict:
doc = pipeline.load_run(run_id)
if doc is None:
raise HTTPException(404, f"unknown run {run_id}")
query = parse_query(text) if text else ROTRQuery()
explicit = {"actor": actor, "vulnerability": vulnerability,
"legality": legality, "environment": environment,
"interaction": interaction, "behavior": behavior,
"road_geometry": road_geometry,
"traffic_control": traffic_control, "visibility": visibility,
"lighting": lighting, "weather": weather,
"consequence_class": consequence_class,
"primary_layer": primary_layer}
for axis, val in explicit.items():
if val is not None:
setattr(query, axis, val)
return _query_run(doc, query)
class QueryRequest(BaseModel):
run_id: str
text: Optional[str] = None
filters: Optional[Dict[str, str]] = None
@router.post("/query")
def structured_query(req: QueryRequest) -> Dict:
doc = pipeline.load_run(req.run_id)
if doc is None:
raise HTTPException(404, f"unknown run {req.run_id}")
query = parse_query(req.text) if req.text else ROTRQuery()
for k, v in (req.filters or {}).items():
if not hasattr(query, k):
raise HTTPException(422, f"unknown filter axis {k!r}")
setattr(query, k, v)
return _query_run(doc, query)
def _query_run(doc: Dict, query: ROTRQuery) -> Dict:
env_by_scenario = {s["scenario_id"]: s for s in doc["scenario_summaries"]}
cluster_by_vid = {}
for c in doc["clusters"]:
for vid in c["member_violation_ids"]:
cluster_by_vid[vid] = c["cluster_id"]
results = []
for v in doc["violations"]:
attr = doc["attributions"].get(v["violation_id"], {})
cons = doc["consequences"].get(v["violation_id"], {})
env = env_by_scenario.get(v["scenario_id"], {})
if not matches(query, v.get("taxonomy", {}),
cons.get("consequence_class"),
attr.get("primary_layer"),
{"lighting": env.get("lighting"),
"weather": env.get("weather")}):
continue
results.append({
"violation_id": v["violation_id"],
"scenario_id": v["scenario_id"],
"rule_id": v["rule_id"],
"rule_version": v["rule_version"],
"description": v["description"],
"taxonomy": v.get("taxonomy", {}),
"evidence": v["evidence"],
"confidence": v["confidence"],
"primary_layer": attr.get("primary_layer"),
"consequence_class": cons.get("consequence_class"),
"cluster_id": cluster_by_vid.get(v["violation_id"]),
"environment": {"visibility": env.get("visibility"),
"lighting": env.get("lighting"),
"weather": env.get("weather")},
"provenance": v["provenance"],
})
return {"query": query.model_dump(), "n_results": len(results),
"results": results}
@router.get("/runs/{run_id}/violations/{violation_id}")
def get_violation(run_id: str, violation_id: str) -> Dict:
doc = pipeline.load_run(run_id)
if doc is None:
raise HTTPException(404, f"unknown run {run_id}")
v = next((v for v in doc["violations"]
if v["violation_id"] == violation_id), None)
if v is None:
raise HTTPException(404, f"unknown violation {violation_id}")
return {"violation": v,
"attribution": doc["attributions"].get(violation_id),
"consequence": doc["consequences"].get(violation_id)}
@router.get("/runs/{run_id}/attribution")
def attribution_matrix(run_id: str) -> Dict:
doc = pipeline.load_run(run_id)
if doc is None:
raise HTTPException(404, f"unknown run {run_id}")
rows = []
for v in doc["violations"]:
attr = doc["attributions"].get(v["violation_id"], {})
rows.append({
"violation_id": v["violation_id"],
"scenario_id": v["scenario_id"],
"rule_id": v["rule_id"],
"primary_layer": attr.get("primary_layer"),
"layers": attr.get("layers", {}),
"note": attr.get("note", ""),
})
return {"run_id": run_id, "rows": rows,
"invariant": "downstream failure never auto-implies perception; "
"layers are implicated only by their own positive "
"evidence"}
@router.get("/runs/{run_id}/violations/{violation_id}/consequence")
def get_consequence(run_id: str, violation_id: str) -> Dict:
doc = pipeline.load_consequence(run_id, violation_id)
if doc is None:
raise HTTPException(404, "no consequence replay for "
f"{violation_id} in {run_id}")
# Attach the scenario geometry the UI needs for the BEV replay view.
run = pipeline.load_run(run_id)
if run:
v = next((v for v in run["violations"]
if v["violation_id"] == violation_id), None)
if v:
sc = pipeline.load_scenario(run["bank_id"], v["scenario_id"])
if sc:
doc["scenario_geometry"] = {
"actual_context": sc.actual_context.model_dump(),
"actors": [{
"actor_id": a.actor_id, "class_name": a.class_name,
"dims": a.dims,
"states": [{"t": s.t, "x": s.x, "y": s.y,
"yaw": s.yaw} for s in a.states],
} for a in sc.actors],
}
return doc
# ------------------------------------------------------------ metrics + regression
@router.get("/runs/{run_id}/metrics")
def get_metrics(run_id: str) -> Dict:
doc = pipeline.load_run(run_id)
if doc is None:
raise HTTPException(404, f"unknown run {run_id}")
return doc["metrics"]
@router.get("/runs/{run_id}/clusters")
def get_clusters(run_id: str) -> List[Dict]:
doc = pipeline.load_run(run_id)
if doc is None:
raise HTTPException(404, f"unknown run {run_id}")
return doc["clusters"]
class RegressionRequest(BaseModel):
baseline_run_id: str
candidate_run_id: str
seed: int = 11
@router.post("/regression")
def run_regression(req: RegressionRequest) -> Dict:
base = pipeline.load_run(req.baseline_run_id)
cand = pipeline.load_run(req.candidate_run_id)
if base is None or cand is None:
raise HTTPException(404, "unknown baseline or candidate run")
rid = f"reg-{req.baseline_run_id}-vs-{req.candidate_run_id}"
result = evaluate_candidate(rid, base, cand, seed=req.seed)
store.write_json(result.model_dump(), "regressions", f"{rid}.json")
return result.model_dump()
@router.get("/regressions")
def list_regressions() -> List[Dict]:
out = []
for name in store.list_dir("regressions"):
doc = store.read_json("regressions", name)
if doc:
out.append(doc)
return out
# ------------------------------------------------------------ HITL + flywheel
@router.get("/runs/{run_id}/hitl")
def hitl_queue(run_id: str) -> List[Dict]:
if pipeline.load_run(run_id) is None:
raise HTTPException(404, f"unknown run {run_id}")
return flywheel_mod.get_queue(run_id)
class HITLActionRequest(BaseModel):
run_id: str
review_id: str
action: str # VALIDATE | REJECT
actor: str
notes: str = ""
@router.post("/hitl/action")
def hitl_action(req: HITLActionRequest) -> Dict:
try:
return flywheel_mod.act(req.run_id, req.review_id, req.action,
req.actor, req.notes)
except KeyError as e:
raise HTTPException(404, str(e))
except ValueError as e:
raise HTTPException(422, str(e))
@router.get("/flywheel/suite")
def regression_suite() -> Dict:
return flywheel_mod.get_suite()
@router.get("/flywheel/candidates")
def training_candidates() -> List[Dict]:
return flywheel_mod.list_candidates()
class PromoteRequest(BaseModel):
candidate_id: str
actor: str
@router.post("/flywheel/promote")
def promote(req: PromoteRequest) -> Dict:
try:
return flywheel_mod.promote_to_training(req.candidate_id, req.actor)
except ContaminationError as e:
raise HTTPException(409, str(e))
except KeyError as e:
raise HTTPException(404, str(e))
class OverrideRequest(BaseModel):
candidate_id: str
actor: str
reason: str
@router.post("/flywheel/override")
def override(req: OverrideRequest) -> Dict:
try:
return flywheel_mod.governance_override(req.candidate_id, req.actor,
req.reason)
except KeyError as e:
raise HTTPException(404, str(e))
except ValueError as e:
raise HTTPException(422, str(e))
# ------------------------------------------------------------ policy + docs
@router.get("/stopship/policy")
def stopship_policy() -> Dict:
return {"policy": STOPSHIP_POLICY, "policy_version": policy_version(),
"note": "deterministic conjunction; explicitly NOT LLM-driven"}
@router.get("/runs/{run_id}/gate")
def run_gate(run_id: str) -> Dict:
doc = pipeline.load_run(run_id)
if doc is None:
raise HTTPException(404, f"unknown run {run_id}")
return doc["gate"]
@router.get("/rules")
def list_rules() -> Dict:
return {"ruleset_version": RULESET_VERSION,
"config": CONFIG,
"config_note": "thresholds are ILLUSTRATIVE (synthetic substrate)",
"rules": [{"rule_id": rid, "description": desc}
for rid, desc, _fn in RULES]}
@router.get("/docs/architecture")
def architecture_doc() -> Dict:
path = Path(__file__).resolve().parents[2] / "docs" / "architecture" / \
"rotr-architecture.md"
if not path.exists():
raise HTTPException(404, "architecture doc missing")
return {"path": str(path), "markdown": path.read_text()}