AdithyaSK HF Staff commited on
Commit
86cc49e
·
verified ·
1 Parent(s): 0290002

Upload folder using huggingface_hub

Browse files
openenv/berth_openenv/environment.py CHANGED
@@ -24,7 +24,7 @@ from typing import Annotated, Any
24
  from fastmcp import FastMCP
25
  from openenv.core.env_server.mcp_environment import MCPEnvironment
26
  from openenv.core.env_server.mcp_types import CallToolAction
27
- from openenv.core.env_server.types import Action, Observation, State
28
  from pydantic import BaseModel, Field
29
 
30
  from berth_core import PROMPT_VERSION, Task, TaskPack, evaluate, load_pack, parse_plan, plan_to_list, rules, situation
@@ -121,6 +121,18 @@ class BerthPlanningEnvironment(MCPEnvironment):
121
 
122
  SUPPORTS_CONCURRENT_SESSIONS = True
123
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  def __init__(self, pack: TaskPack | None = None):
125
  self.pack = pack or load_pack()
126
  self._episode: Episode | None = None
 
24
  from fastmcp import FastMCP
25
  from openenv.core.env_server.mcp_environment import MCPEnvironment
26
  from openenv.core.env_server.mcp_types import CallToolAction
27
+ from openenv.core.env_server.types import Action, EnvironmentMetadata, Observation, State
28
  from pydantic import BaseModel, Field
29
 
30
  from berth_core import PROMPT_VERSION, Task, TaskPack, evaluate, load_pack, parse_plan, plan_to_list, rules, situation
 
121
 
122
  SUPPORTS_CONCURRENT_SESSIONS = True
123
 
124
+ def get_metadata(self) -> EnvironmentMetadata:
125
+ """What OpenEnv's /metadata reports (the web UI shows it too)."""
126
+ return EnvironmentMetadata(
127
+ name="PortSimEnv v1",
128
+ description="Re-plan a broken week of container-ship dockings at a Port of Barcelona quay, built from the "
129
+ "port's real 2024 calls. Tools: get_situation, check_plan (10 per episode), submit_plan "
130
+ "(graded once, deterministically, against the CP-SAT optimum).",
131
+ version="1.0.0",
132
+ author="FineEnvs",
133
+ documentation_url="https://github.com/adithya-s-k/FineEnvs/tree/main/07-simulation-environments/portsim-v1",
134
+ )
135
+
136
  def __init__(self, pack: TaskPack | None = None):
137
  self.pack = pack or load_pack()
138
  self._episode: Episode | None = None
openenv/berth_openenv/gradio_ui.py CHANGED
@@ -21,8 +21,8 @@ DEFAULT_SPLIT = "eval"
21
  DEFAULT_TASK = "dock-24B-w07x1-busy-0"
22
 
23
 
24
- def _iframe(task_id: str, split: str) -> str:
25
- src = f"/viewer/?embed=1#/play?split={quote(split)}&task={quote(task_id)}&start=1"
26
  return (
27
  # OpenEnv's page caps the container width; the editor wants the full window.
28
  "<style>.gradio-container .main.fillable{max-width:none!important;padding-left:12px!important;"
@@ -44,7 +44,7 @@ def build_ui(pack: TaskPack) -> gr.Blocks:
44
  def _frame(index, split):
45
  split = split if split in ids else split0
46
  i = min(max(int(index or 0), 0), len(ids[split]) - 1)
47
- return _iframe(ids[split][i], split)
48
 
49
  with gr.Blocks(title="PortSimEnv v1") as ui:
50
  with gr.Row():
 
21
  DEFAULT_TASK = "dock-24B-w07x1-busy-0"
22
 
23
 
24
+ def _iframe(task_id: str, split: str, index: int) -> str:
25
+ src = f"/viewer/?embed=1#/play?split={quote(split)}&index={index}&task={quote(task_id)}&start=1"
26
  return (
27
  # OpenEnv's page caps the container width; the editor wants the full window.
28
  "<style>.gradio-container .main.fillable{max-width:none!important;padding-left:12px!important;"
 
44
  def _frame(index, split):
45
  split = split if split in ids else split0
46
  i = min(max(int(index or 0), 0), len(ids[split]) - 1)
47
+ return _iframe(ids[split][i], split, i)
48
 
49
  with gr.Blocks(title="PortSimEnv v1") as ui:
50
  with gr.Row():
openenv/berth_openenv/web/index.html CHANGED
@@ -5,7 +5,7 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
  <title>PortSimEnv v1 · Port of Barcelona</title>
7
  <meta name="description" content="Re-planning a week of container-ship dockings at one quay of the Port of Barcelona after a disruption.">
8
- <link rel="stylesheet" href="./style.css?v=eval-1">
9
  <script type="importmap">
10
  {
11
  "imports": {
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
  <title>PortSimEnv v1 · Port of Barcelona</title>
7
  <meta name="description" content="Re-planning a week of container-ship dockings at one quay of the Port of Barcelona after a disruption.">
8
+ <link rel="stylesheet" href="./style.css?v=eval-2">
9
  <script type="importmap">
10
  {
11
  "imports": {
openenv/berth_openenv/web/overview.js CHANGED
@@ -55,6 +55,22 @@ function board(episodes) {
55
  })).sort((a, b) => b.mean - a.mean);
56
  }
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  function resultHtml(e) {
59
  if (!e.submitted) return `<span class="muted" title="${escapeHtml(e.end_reason || "")}">not submitted</span>`;
60
  if (!e.feasible) return '<i class="dot bad"></i>rule broken';
@@ -113,11 +129,16 @@ export async function overviewPage({ app, setCrumbs, isCurrent, params, config =
113
  const evalSec = `<section>
114
  <div class="sec-head"><h2>Eval</h2><span class="muted small">${RUN} · 50 held-out tasks · one rollout per model and task · 12 turns, 32k output tokens per turn</span></div>
115
  <table class="tbl click" id="ov-board">
116
- <thead><tr><th>Model</th><th class="num">Mean</th>${TIERS.map((t) => `<th class="num opt" title="${escapeHtml(TIER_TIPS[t])}">${cap(t)}</th>`).join("")}<th class="num" title="Episodes that ended with submit_plan">Submitted</th><th class="num" title="Plans that break no rule">Valid</th><th class="num" title="Plans that match the proven optimum (reward 1.0)">Optimal</th></tr></thead>
117
- <tbody>${Array.from({ length: 6 }, () => `<tr class="skel">${"<td><i></i></td>".repeat(9)}</tr>`).join("")}</tbody>
118
  </table>
 
119
  <p class="muted small ov-note" id="ov-board-note">Mean reward per model and tier. Click a model to list its rollouts; each one replays in 3D.</p>
120
  </section>
 
 
 
 
121
  <section class="ov-eps" hidden>
122
  <div class="sec-head"><h2 id="ov-eps-h">Rollouts</h2><span class="muted small" id="ov-eps-note"></span></div>
123
  <table class="tbl click" id="ov-eps"><thead></thead><tbody></tbody></table>
@@ -198,7 +219,7 @@ print(step.reward) # 0..1, graded once</code></pre>
198
 
199
  const tbody = app.querySelector("#ov-board tbody");
200
  const noRollouts = () => {
201
- tbody.innerHTML = `<tr><td colspan="9" class="muted">No eval rollouts on this server.${explorer ? "" : ` They are in the <a href="${EVAL_VIEWER}" target="_blank" rel="noopener">eval Space ↗</a>.`}</td></tr>`;
202
  app.querySelector("#ov-board-note").hidden = true;
203
  if (explorer) return;
204
  for (const a of app.querySelectorAll('a[href^="#/run/"]')) { // the showcase rollout: watch it in the eval Space
@@ -218,7 +239,7 @@ print(step.reward) # 0..1, graded once</code></pre>
218
  if (!isCurrent()) return;
219
  if (!rows.length) return noRollouts();
220
  tbody.innerHTML = rows.map((r) => `<tr data-row="${escapeHtml(r.model)}" title="List ${escapeHtml(r.name)}'s rollouts">
221
- <td>${escapeHtml(r.name)}</td><td class="num"><b>${fmtNum(r.mean, 3)}</b></td>
222
  ${TIERS.map((t) => `<td class="num opt">${r.tiers[t] == null ? "–" : fmtNum(r.tiers[t], 2)}</td>`).join("")}
223
  <td class="num">${r.submitted}/${r.n}</td><td class="num">${r.valid}/${r.n}</td><td class="num">${r.optimal}/${r.n}</td></tr>`).join("");
224
 
@@ -226,6 +247,24 @@ print(step.reward) # 0..1, graded once</code></pre>
226
  for (const e of rows[0].eps) perTier.set(e.difficulty, (perTier.get(e.difficulty) || 0) + 1);
227
  app.querySelector("#ov-board-note").textContent = `Mean reward per model and tier (${TIERS.filter((t) => perTier.has(t)).map((t) => `${t} ${perTier.get(t)}`).join(", ")} tasks). Click a model to list its rollouts; each one replays in 3D.`;
228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  // The selected model's rollouts, under the board (#/?model=<id> keeps the choice in shared links).
230
  const epsSec = app.querySelector(".ov-eps");
231
  let epsTable = null;
 
55
  })).sort((a, b) => b.mean - a.mean);
56
  }
57
 
58
+ // Outcome of one rollout, for the bars and the grid.
59
+ const OUTCOMES = [
60
+ { k: "optimal", label: "optimal plan (1.0)" },
61
+ { k: "valid", label: "valid, not optimal" },
62
+ { k: "broken", label: "broke a rule (≤ 0.2)" },
63
+ { k: "none", label: "no plan submitted (0)" },
64
+ ];
65
+ const outcomeOf = (e) => (!e.submitted ? "none" : !e.feasible ? "broken" : (e.reward || 0) >= 0.999 ? "optimal" : "valid");
66
+
67
+ function outcomeBar(r) {
68
+ const n = r.n || 1;
69
+ const c = { optimal: r.optimal, valid: r.valid - r.optimal, broken: r.submitted - r.valid, none: r.n - r.submitted };
70
+ return `<span class="ov-obar" title="${OUTCOMES.map((o) => `${c[o.k]} ${o.label}`).join(" · ")}">${OUTCOMES.filter((o) => c[o.k] > 0)
71
+ .map((o) => `<i class="oc-${o.k}" style="width:${((100 * c[o.k]) / n).toFixed(2)}%"></i>`).join("")}</span>`;
72
+ }
73
+
74
  function resultHtml(e) {
75
  if (!e.submitted) return `<span class="muted" title="${escapeHtml(e.end_reason || "")}">not submitted</span>`;
76
  if (!e.feasible) return '<i class="dot bad"></i>rule broken';
 
129
  const evalSec = `<section>
130
  <div class="sec-head"><h2>Eval</h2><span class="muted small">${RUN} · 50 held-out tasks · one rollout per model and task · 12 turns, 32k output tokens per turn</span></div>
131
  <table class="tbl click" id="ov-board">
132
+ <thead><tr><th>Model</th><th class="ov-oth" title="The 50 eval weeks by outcome">Outcomes</th><th class="num">Mean</th>${TIERS.map((t) => `<th class="num opt" title="${escapeHtml(TIER_TIPS[t])}">${cap(t)}</th>`).join("")}<th class="num" title="Episodes that ended with submit_plan">Submitted</th><th class="num" title="Plans that break no rule">Valid</th><th class="num" title="Plans that match the proven optimum (reward 1.0)">Optimal</th></tr></thead>
133
+ <tbody>${Array.from({ length: 6 }, () => `<tr class="skel">${"<td><i></i></td>".repeat(10)}</tr>`).join("")}</tbody>
134
  </table>
135
+ <div class="ov-legend">${OUTCOMES.map((o) => `<span><i class="oc-${o.k}"></i>${o.label}</span>`).join("")}</div>
136
  <p class="muted small ov-note" id="ov-board-note">Mean reward per model and tier. Click a model to list its rollouts; each one replays in 3D.</p>
137
  </section>
138
+ <section class="ov-grid-sec" hidden>
139
+ <div class="sec-head"><h2>Every rollout</h2><span class="muted small">one square per model and eval week, coloured by outcome (fainter = lower reward) · click a square to replay it in 3D</span></div>
140
+ <div class="ov-mx" id="ov-mx"></div>
141
+ </section>
142
  <section class="ov-eps" hidden>
143
  <div class="sec-head"><h2 id="ov-eps-h">Rollouts</h2><span class="muted small" id="ov-eps-note"></span></div>
144
  <table class="tbl click" id="ov-eps"><thead></thead><tbody></tbody></table>
 
219
 
220
  const tbody = app.querySelector("#ov-board tbody");
221
  const noRollouts = () => {
222
+ tbody.innerHTML = `<tr><td colspan="10" class="muted">No eval rollouts on this server.${explorer ? "" : ` They are in the <a href="${EVAL_VIEWER}" target="_blank" rel="noopener">eval Space ↗</a>.`}</td></tr>`;
223
  app.querySelector("#ov-board-note").hidden = true;
224
  if (explorer) return;
225
  for (const a of app.querySelectorAll('a[href^="#/run/"]')) { // the showcase rollout: watch it in the eval Space
 
239
  if (!isCurrent()) return;
240
  if (!rows.length) return noRollouts();
241
  tbody.innerHTML = rows.map((r) => `<tr data-row="${escapeHtml(r.model)}" title="List ${escapeHtml(r.name)}'s rollouts">
242
+ <td>${escapeHtml(r.name)}</td><td class="ov-otd">${outcomeBar(r)}</td><td class="num"><b>${fmtNum(r.mean, 3)}</b></td>
243
  ${TIERS.map((t) => `<td class="num opt">${r.tiers[t] == null ? "–" : fmtNum(r.tiers[t], 2)}</td>`).join("")}
244
  <td class="num">${r.submitted}/${r.n}</td><td class="num">${r.valid}/${r.n}</td><td class="num">${r.optimal}/${r.n}</td></tr>`).join("");
245
 
 
247
  for (const e of rows[0].eps) perTier.set(e.difficulty, (perTier.get(e.difficulty) || 0) + 1);
248
  app.querySelector("#ov-board-note").textContent = `Mean reward per model and tier (${TIERS.filter((t) => perTier.has(t)).map((t) => `${t} ${perTier.get(t)}`).join(", ")} tasks). Click a model to list its rollouts; each one replays in 3D.`;
249
 
250
+ // Every rollout at a glance: models down, eval weeks across (grouped by tier), each square a link to its 3D replay.
251
+ {
252
+ const tasks = [...new Map(rows.flatMap((r) => r.eps).map((e) => [e.task_id, e])).values()]
253
+ .sort((a, b) => TIERS.indexOf(a.difficulty) - TIERS.indexOf(b.difficulty) || a.task_id.localeCompare(b.task_id));
254
+ const byKey = new Map(rows.flatMap((r) => r.eps.map((e) => [`${r.model}|${e.task_id}`, e])));
255
+ const groups = TIERS.map((t) => ({ t, tasks: tasks.filter((e) => e.difficulty === t) })).filter((g) => g.tasks.length);
256
+ const cell = (r, task) => {
257
+ const e = byKey.get(`${r.model}|${task.task_id}`);
258
+ if (!e) return '<span class="mx-c mx-missing"></span>';
259
+ const oc = outcomeOf(e);
260
+ const op = oc === "valid" ? (0.35 + 0.65 * Math.max(0, ((e.reward || 0) - 0.2) / 0.8)).toFixed(2) : "1";
261
+ return `<a class="mx-c oc-${oc}" style="opacity:${op}" href="${runHref(r.model, task.task_id)}" title="${escapeHtml(r.name)} · ${escapeHtml(task.task_id)} · ${task.ships} ships · ${oc === "none" ? "no plan" : `reward ${fmtNum(e.reward, 2)}`}"></a>`;
262
+ };
263
+ app.querySelector("#ov-mx").innerHTML = `<div class="mx-row mx-head"><span class="mx-name"></span>${groups.map((g) => `<span class="mx-g" style="--n:${g.tasks.length}" title="${escapeHtml(TIER_TIPS[g.t])}">${cap(g.t)} · ${g.tasks.length}</span>`).join("")}</div>` +
264
+ rows.map((r) => `<div class="mx-row"><span class="mx-name">${escapeHtml(r.name)}</span>${groups.map((g) => `<span class="mx-g" style="--n:${g.tasks.length}">${g.tasks.map((t) => cell(r, t)).join("")}</span>`).join("")}<span class="mx-mean">${fmtNum(r.mean, 2)}</span></div>`).join("");
265
+ app.querySelector(".ov-grid-sec").hidden = false;
266
+ }
267
+
268
  // The selected model's rollouts, under the board (#/?model=<id> keeps the choice in shared links).
269
  const epsSec = app.querySelector(".ov-eps");
270
  let epsTable = null;
openenv/berth_openenv/web/play.js CHANGED
@@ -1297,7 +1297,22 @@ export async function playPage({ app, params, isCurrent, setTeardown, sceneMod }
1297
 
1298
  onConnStatus(session.status);
1299
  bindShell();
1300
- // #/play?task=<id>&start=1 starts that task's episode straight away (used by embeds)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1301
  openPicker().then(() => {
1302
  if (params.get("start") !== "1" || !isCurrent() || st.phase === "episode") return;
1303
  const t = pickerTasks.find((x) => x.task_id === st.selected);
 
1297
 
1298
  onConnStatus(session.status);
1299
  bindShell();
1300
+ // #/play?split=<s>&index=<i>&start=1 starts that task's episode straight away, without the picker (OpenEnv's /web
1301
+ // tab and embeds use it); the one task comes from the Task API, so a 1,050-task split is never listed first.
1302
+ const autoIndex = params.get("index");
1303
+ if (params.get("start") === "1" && st.split && autoIndex !== null && autoIndex !== "") {
1304
+ $("#pa-main").innerHTML = '<div class="pa-empty muted">Starting an episode…</div>';
1305
+ taskApi("task", { split: st.split, index: Number(autoIndex) })
1306
+ .then((d) => {
1307
+ const t = d.task || d;
1308
+ if (!isCurrent() || st.phase === "episode") return;
1309
+ st.selected = t.task_id;
1310
+ return start({ task_id: t.task_id }, t);
1311
+ })
1312
+ .catch(() => isCurrent() && openPicker());
1313
+ return;
1314
+ }
1315
+ // #/play?task=<id>&start=1 (older links): pick it from the split list, then start
1316
  openPicker().then(() => {
1317
  if (params.get("start") !== "1" || !isCurrent() || st.phase === "episode") return;
1318
  const t = pickerTasks.find((x) => x.task_id === st.selected);
openenv/berth_openenv/web/style.css CHANGED
@@ -647,3 +647,25 @@ a.ext { font-size: 12px; white-space: nowrap; }
647
  #ov-eps td { white-space: nowrap; }
648
  .board-sec { overflow-x: auto; }
649
  @media (max-width: 700px) { html.explorer .crumbs { display: none; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
  #ov-eps td { white-space: nowrap; }
648
  .board-sec { overflow-x: auto; }
649
  @media (max-width: 700px) { html.explorer .crumbs { display: none; } }
650
+
651
+ /* eval visuals: outcome bars and the rollout grid */
652
+ .ov-oth { width: 30%; }
653
+ .ov-otd { padding-top: 6px !important; }
654
+ .ov-obar { display: flex; height: 12px; border-radius: 2px; overflow: hidden; background: var(--line2); min-width: 110px; }
655
+ .ov-obar i { display: block; height: 100%; }
656
+ .oc-optimal { background: var(--ok); }
657
+ .oc-valid { background: color-mix(in srgb, var(--ok) 45%, var(--bg)); }
658
+ .oc-broken { background: var(--red); }
659
+ .oc-none { background: var(--faint); opacity: .45; }
660
+ .ov-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 6px; font-size: 12px; color: var(--muted); }
661
+ .ov-legend i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 5px; vertical-align: -1px; }
662
+ .ov-mx { overflow-x: auto; padding-bottom: 4px; }
663
+ .mx-row { display: flex; align-items: center; gap: 10px; min-width: 760px; margin-bottom: 4px; }
664
+ .mx-name { flex: 0 0 120px; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
665
+ .mx-mean { flex: 0 0 34px; text-align: right; font-size: 12px; font-variant-numeric: tabular-nums; font-weight: 600; }
666
+ .mx-g { flex: var(--n) 1 0; display: grid; grid-template-columns: repeat(var(--n), 1fr); gap: 2px; min-width: 0; }
667
+ .mx-head .mx-g { display: block; font-size: 11px; color: var(--muted); border-bottom: 1px solid var(--line); padding-bottom: 2px; white-space: nowrap; overflow: hidden; }
668
+ .mx-c { display: block; aspect-ratio: 1; border-radius: 2px; min-width: 8px; }
669
+ .mx-c:hover { outline: 2px solid var(--fg); outline-offset: 1px; }
670
+ .mx-missing { background: transparent; border: 1px dashed var(--line); }
671
+ @media (max-width: 700px) { .ov-oth, .ov-otd { display: none; } }