visproj commited on
Commit
27405c0
·
verified ·
1 Parent(s): d4a309a

fix: end banner starts a new layout; outcome colour

Browse files
Files changed (3) hide show
  1. lantern/bench.py +104 -65
  2. web/app.js +7 -2
  3. web/index.html +1 -1
lantern/bench.py CHANGED
@@ -1,65 +1,104 @@
1
- """Benchmark series: play one level across seeds, optionally carrying the journal forward."""
2
-
3
- from __future__ import annotations
4
-
5
- import time
6
- import uuid
7
-
8
- from .agent import EpisodeConfig, run_episode
9
- from .env import TASKS, DungeonEnv
10
- from .journal import merge_lessons
11
- from .llm import ChatClient
12
- from .store import EpisodeStore
13
-
14
- SEED_BASE = 1000 # benchmark seeds stay clear of the low seeds visitors try in the Space
15
-
16
-
17
- def bench_seeds(n: int) -> list[int]:
18
- return [SEED_BASE + i for i in range(n)]
19
-
20
-
21
- def play_series(task_id: str, seeds: list[int], use_lessons: bool, tag: str,
22
- client: ChatClient, store: EpisodeStore, source: str = "bench") -> list[dict]:
23
- """Play `seeds` in order on one level and save every episode; returns per-episode summaries.
24
-
25
- Each series starts with an empty journal; with `use_lessons`, lessons written after
26
- each episode are merged in before the next seed.
27
- """
28
- config = EpisodeConfig(use_lessons=use_lessons, reflect=use_lessons)
29
- lessons: list[str] = []
30
- summaries = []
31
- for seed in seeds:
32
- env = DungeonEnv(TASKS[task_id], seed=seed)
33
- try:
34
- events = list(run_episode(env, client, lessons, config))
35
- except Exception as exc: # one bad episode must not sink the series
36
- events = [{"type": "error", "message": f"{type(exc).__name__}: {exc}"}]
37
- finally:
38
- env.close()
39
- end = next((e for e in events if e["type"] == "end"), {})
40
- lessons_in = len(lessons)
41
- if use_lessons and end.get("new_lessons"):
42
- lessons = merge_lessons(lessons, end["new_lessons"], 12)
43
- record = {"id": uuid.uuid4().hex[:12], "task": task_id, "seed": seed, "source": source,
44
- "bench": tag, "model": client.model, "use_lessons": use_lessons,
45
- "finished_at": time.time(), "outcome": end.get("outcome", "error"),
46
- "steps": end.get("steps", 0), "lessons_in": lessons_in, "events": events}
47
- store.save(record, wait=True)
48
- summaries.append({k: record[k] for k in ("id", "task", "seed", "use_lessons", "outcome", "steps")}
49
- | {"usage": end.get("usage", {})})
50
- return summaries
51
-
52
-
53
- def summary_table(results: list[dict], task_ids: list[str], conditions: list[str]) -> str:
54
- """Plain-text results table: wins, average steps, and tokens per level and condition."""
55
- lines = [f"{'level':14}{'journal':>8}{'won':>8}{'avg steps':>11}{'tokens':>12}"]
56
- for t in task_ids:
57
- for cond in conditions:
58
- rows = [r for r in results if r["task"] == t and r["use_lessons"] == (cond == "on")]
59
- if not rows:
60
- continue
61
- won = sum(r["outcome"] == "success" for r in rows)
62
- steps = sum(r["steps"] for r in rows) / len(rows)
63
- tok = sum(r["usage"].get("prompt_tokens", 0) + r["usage"].get("completion_tokens", 0) for r in rows)
64
- lines.append(f"{t:14}{cond:>8}{won:>4}/{len(rows):<3}{steps:>11.1f}{tok:>12,}")
65
- return "\n".join(lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark series: play one level across seeds, optionally carrying the journal forward."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ import time
7
+ import uuid
8
+
9
+ from .agent import EpisodeConfig, run_episode
10
+ from .env import TASKS, DungeonEnv
11
+ from .journal import merge_lessons
12
+ from .llm import ChatClient
13
+ from .store import EpisodeStore
14
+
15
+ SEED_BASE = 1000 # benchmark seeds stay clear of the low seeds visitors try in the Space
16
+
17
+
18
+ def bench_seeds(n: int) -> list[int]:
19
+ return [SEED_BASE + i for i in range(n)]
20
+
21
+
22
+ def play_series(task_id: str, seeds: list[int], use_lessons: bool, tag: str,
23
+ client: ChatClient, store: EpisodeStore, source: str = "bench",
24
+ stop: threading.Event | None = None) -> list[dict]:
25
+ """Play `seeds` in order on one level and save every episode; returns per-episode summaries.
26
+
27
+ Each series starts with an empty journal; with `use_lessons`, lessons written after
28
+ each episode are merged in before the next seed. Setting `stop` (e.g. when a spending
29
+ cap is hit) ends the current episode as "stopped" and skips the remaining seeds.
30
+ """
31
+ config = EpisodeConfig(use_lessons=use_lessons, reflect=use_lessons)
32
+ lessons: list[str] = []
33
+ summaries = []
34
+ for seed in seeds:
35
+ if stop is not None and stop.is_set():
36
+ break
37
+ env = DungeonEnv(TASKS[task_id], seed=seed)
38
+ try:
39
+ events = list(run_episode(env, client, lessons, config, stop))
40
+ except Exception as exc: # one bad episode must not sink the series
41
+ events = [{"type": "error", "message": f"{type(exc).__name__}: {exc}"}]
42
+ finally:
43
+ env.close()
44
+ end = next((e for e in events if e["type"] == "end"), {})
45
+ lessons_in = len(lessons)
46
+ if use_lessons and end.get("new_lessons"):
47
+ lessons = merge_lessons(lessons, end["new_lessons"], 12)
48
+ record = {"id": uuid.uuid4().hex[:12], "task": task_id, "seed": seed, "source": source,
49
+ "bench": tag, "model": client.model, "use_lessons": use_lessons,
50
+ "finished_at": time.time(), "outcome": end.get("outcome", "error"),
51
+ "steps": end.get("steps", 0), "lessons_in": lessons_in, "events": events}
52
+ store.save(record, wait=True)
53
+ summaries.append({k: record[k] for k in ("id", "task", "seed", "use_lessons", "outcome", "steps")}
54
+ | {"usage": end.get("usage", {})})
55
+ return summaries
56
+
57
+
58
+ class BudgetedClient:
59
+ """Wraps a ChatClient and stops all parallel series once shared token spend hits a cap.
60
+
61
+ Each series records only its own running total under `key` in a shared mapping
62
+ (a Modal Dict in production), so concurrent writers never overwrite each other; the
63
+ cap is checked against the sum over `all_keys` after every call.
64
+ """
65
+
66
+ def __init__(self, client: ChatClient, shared, key: str, all_keys: list[str],
67
+ cap_tokens: int, stop: threading.Event):
68
+ self.client, self.shared, self.key, self.all_keys = client, shared, key, all_keys
69
+ self.cap_tokens, self.stop = cap_tokens, stop
70
+ self.used = 0
71
+
72
+ @property
73
+ def model(self) -> str:
74
+ return self.client.model
75
+
76
+ def total(self) -> int:
77
+ return sum(self.shared.get(k, 0) for k in self.all_keys)
78
+
79
+ def chat(self, *args, **kwargs):
80
+ if self.total() >= self.cap_tokens:
81
+ self.stop.set()
82
+ reply = self.client.chat(*args, **kwargs)
83
+ self.used += reply.usage.get("prompt_tokens", 0) + reply.usage.get("completion_tokens", 0)
84
+ self.shared[self.key] = self.used
85
+ if self.total() >= self.cap_tokens:
86
+ self.stop.set()
87
+ return reply
88
+
89
+
90
+ def summary_table(results: list[dict], task_ids: list[str], conditions: list[str]) -> str:
91
+ """Plain-text results table: wins, average steps, and tokens per level and condition."""
92
+ lines = [f"{'level':14}{'journal':>8}{'won':>8}{'avg steps':>11}{'tokens':>12}"]
93
+ for t in task_ids:
94
+ for cond in conditions:
95
+ # Episodes cut off by the spending cap are neither wins nor losses.
96
+ rows = [r for r in results if r["task"] == t and r["use_lessons"] == (cond == "on")
97
+ and r["outcome"] != "stopped"]
98
+ if not rows:
99
+ continue
100
+ won = sum(r["outcome"] == "success" for r in rows)
101
+ steps = sum(r["steps"] for r in rows) / len(rows)
102
+ tok = sum(r["usage"].get("prompt_tokens", 0) + r["usage"].get("completion_tokens", 0) for r in rows)
103
+ lines.append(f"{t:14}{cond:>8}{won:>4}/{len(rows):<3}{steps:>11.1f}{tok:>12,}")
104
+ return "\n".join(lines)
web/app.js CHANGED
@@ -272,7 +272,8 @@ function renderEnding() {
272
  if (!end || !atEnd || state.replayTimer) { box.hidden = true; return; }
273
  const [cls, label] = OUTCOMES[end.outcome] || ["", end.outcome];
274
  $("ending-title").textContent = label;
275
- $("ending-title").className = cls;
 
276
  $("ending-detail").textContent = `${end.steps} steps, ${end.usage.calls} Beam calls.`;
277
  box.hidden = false;
278
  }
@@ -436,7 +437,11 @@ $("controls").addEventListener("submit", startRun);
436
  $("stop").addEventListener("click", stopRun);
437
  $("follow").addEventListener("click", () => select(state.turns.length - 1, true));
438
  $("again").addEventListener("click", () => { if (state.episodeId) openReplay(state.episodeId); });
439
- $("new-run").addEventListener("click", () => { window.scrollTo({ top: 0 }); $("task").focus(); });
 
 
 
 
440
  $("task").addEventListener("change", () => { if (!state.live && !startEvent()) showGoal(); });
441
  $("timeline").addEventListener("keydown", (e) => {
442
  if (e.key === "ArrowLeft") { stopReplay(); select(state.selected - 1, false); e.preventDefault(); }
 
272
  if (!end || !atEnd || state.replayTimer) { box.hidden = true; return; }
273
  const [cls, label] = OUTCOMES[end.outcome] || ["", end.outcome];
274
  $("ending-title").textContent = label;
275
+ $("ending-title").className = `ending-title ${cls}`;
276
+ $("new-run").disabled = !!state.liveBlocked || !!state.live;
277
  $("ending-detail").textContent = `${end.steps} steps, ${end.usage.calls} Beam calls.`;
278
  box.hidden = false;
279
  }
 
437
  $("stop").addEventListener("click", stopRun);
438
  $("follow").addEventListener("click", () => select(state.turns.length - 1, true));
439
  $("again").addEventListener("click", () => { if (state.episodeId) openReplay(state.episodeId); });
440
+ // Start right away on the same level with the next layout, so it's a fresh dungeon.
441
+ $("new-run").addEventListener("click", () => {
442
+ $("seed").value = String((Number($("seed").value) || 0) + 1);
443
+ $("controls").requestSubmit();
444
+ });
445
  $("task").addEventListener("change", () => { if (!state.live && !startEvent()) showGoal(); });
446
  $("timeline").addEventListener("keydown", (e) => {
447
  if (e.key === "ArrowLeft") { stopReplay(); select(state.selected - 1, false); e.preventDefault(); }
web/index.html CHANGED
@@ -46,7 +46,7 @@
46
  <p class="ending-detail" id="ending-detail"></p>
47
  <div class="ending-actions">
48
  <button class="secondary" id="again" type="button">Watch again</button>
49
- <button class="primary" id="new-run" type="button">New run</button>
50
  </div>
51
  </div>
52
  <p class="empty" id="empty">No runs yet. Pick a level and press <em>Watch Beam explore!</em></p>
 
46
  <p class="ending-detail" id="ending-detail"></p>
47
  <div class="ending-actions">
48
  <button class="secondary" id="again" type="button">Watch again</button>
49
+ <button class="primary" id="new-run" type="button">Try a new layout</button>
50
  </div>
51
  </div>
52
  <p class="empty" id="empty">No runs yet. Pick a level and press <em>Watch Beam explore!</em></p>