Spaces:
Running
Running
File size: 5,910 Bytes
0290002 a1e3907 0290002 a1e3907 0290002 a1e3907 0290002 a1e3907 0290002 a1e3907 0290002 a1e3907 0290002 a1e3907 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | """The 3D viewer (`/viewer/`) and the read-only JSON it uses (`/api/...`), on the env server's port, or alone in the
eval explorer (`explorer.py`).
/api/config the viewer's mode: "env" (play here) or "explorer" (no sessions; play links out)
/api/tasks task summaries
/api/tasks/{id} a public task (what the agent sees, structured)
/api/tasks/{id}/reference naive and optimal plans - for people; no tool exposes these to the agent
/api/runs rollout runs found under BERTH_RUNS_DIR
/api/runs/{run} one run's episode index
/api/runs/{run}/episode one rollout (?model=&task_id=)
/api/episodes live episodes on this server (newest first; env mode only)
/api/episodes/{episode_id} one live episode: its task id, every plan checked so far, the grade
BERTH_VIEWER_MODE=explorer no live episodes, no Play page: playing links to BERTH_PLAY_URL (the env Space)
"""
from __future__ import annotations
import json
import os
import re
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from berth_core import TaskPack
WEB = Path(__file__).resolve().parent / "web"
PLAY_URL = "https://huggingface.co/spaces/FineEnvs/PortSimEnv" # the environment Space: Try Environment, the editor
ENV_URL = "https://fineenvs-portsimenv.hf.space" # its server, for an agent's OpenEnv client
DEFAULT_RUNS = Path(__file__).resolve().parents[4] / "results" / "rollouts"
_SAFE = re.compile(r"^[A-Za-z0-9._@:+-]+$")
def model_slug(model: str) -> str:
return re.sub(r"[^A-Za-z0-9._-]+", "__", model)
def runs_dir() -> Path:
return Path(os.environ.get("BERTH_RUNS_DIR", DEFAULT_RUNS))
def _summary(t) -> dict:
return {"task_id": t.task_id, "split": t.split, "quay": t.quay, "terminal": t.terminal, "week": t.week,
"difficulty": t.difficulty, "ships": len(t.ships), "disruptions": [e["type"] for e in t.disruptions]}
def viewer_config(mode: str | None = None) -> dict:
mode = (mode or os.environ.get("BERTH_VIEWER_MODE") or "env").strip().lower()
return {"mode": "explorer" if mode == "explorer" else "env",
"play_url": os.environ.get("BERTH_PLAY_URL") or PLAY_URL,
"env_url": os.environ.get("BERTH_ENV_URL") or ENV_URL}
def mount_viewer(app: FastAPI, pack: TaskPack, mode: str | None = None) -> None:
"""mode: "env" (default; live episodes, Play) or "explorer" (read-only); None reads BERTH_VIEWER_MODE."""
config = viewer_config(mode)
@app.get("/api/config", tags=["Viewer"])
def viewer_mode():
return config
@app.get("/api/tasks", tags=["Viewer"])
def tasks():
return [_summary(t) for t in pack.tasks]
@app.get("/api/tasks/{task_id}", tags=["Viewer"])
def task(task_id: str):
try:
return pack.public(pack.get(task_id))
except KeyError:
raise HTTPException(404, "unknown task") from None
@app.get("/api/tasks/{task_id}/reference", tags=["Viewer"])
def reference(task_id: str):
try:
return pack.get(task_id).reference
except KeyError:
raise HTTPException(404, "unknown task") from None
@app.get("/api/runs", tags=["Viewer"])
def runs():
root = runs_dir()
out = []
for d in sorted(root.iterdir() if root.is_dir() else [], reverse=True):
idx = d / "index.json"
if not idx.is_file():
continue
# only episodes on tasks this server serves (runs on other packs are hidden, not broken links)
eps = [e for e in json.loads(idx.read_text()).get("episodes", []) if e.get("task_id") in pack._by_id]
if not eps:
continue
out.append({"run": d.name, "models": sorted({e["model"] for e in eps}), "episodes": len(eps),
"mean_reward": round(sum(e["reward"] for e in eps) / len(eps), 4)})
return out
@app.get("/api/runs/{run}", tags=["Viewer"])
def run(run: str):
if not _SAFE.match(run) or not (runs_dir() / run / "index.json").is_file():
raise HTTPException(404, "unknown run")
body = json.loads((runs_dir() / run / "index.json").read_text())
body["episodes"] = [e for e in body.get("episodes", []) if e.get("task_id") in pack._by_id]
return body
@app.get("/api/runs/{run}/episode", tags=["Viewer"])
def episode(run: str, model: str, task_id: str):
if not (_SAFE.match(run) and _SAFE.match(task_id)):
raise HTTPException(404, "unknown episode")
path = runs_dir() / run / model_slug(model) / f"{task_id}.json"
if not path.is_file():
raise HTTPException(404, "unknown episode")
return json.loads(path.read_text())
if config["mode"] == "env": # live episodes need the env's sessions (and its OpenEnv imports)
_mount_live(app)
@app.get("/viewer", include_in_schema=False)
def viewer_slash():
return RedirectResponse("/viewer/")
app.mount("/viewer", StaticFiles(directory=str(WEB), html=True), name="viewer")
def _mount_live(app: FastAPI) -> None:
try:
from .environment import STORE
except ImportError:
from environment import STORE
@app.get("/api/episodes", tags=["Viewer"])
def live_episodes():
return [{k: v for k, v in ep.summary().items() if k != "steps"} | {"checks": len(ep.steps)}
for ep in STORE.recent()]
@app.get("/api/episodes/{episode_id}", tags=["Viewer"])
def live_episode(episode_id: str):
ep = STORE.get(episode_id)
if ep is None:
raise HTTPException(404, "unknown or expired episode")
return ep.summary()
|