lewtun HF Staff commited on
Commit
b56ec7a
·
verified ·
1 Parent(s): f6eeecf

Turn PyBot into an in-browser web demo of the best bot (3185a7a, 26.55%)

Browse files

Pyodide runs SwarmChat-v8 commit 3185a7a9bc50 (the leaderboard's best) in a Web Worker; no server. Keeps the chat, trace and file treemap; drops the story text, progress chart, FastAPI server and old bot zip. Switches the Space to sdk: static.

.gitattributes CHANGED
@@ -33,5 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
- site/fonts/LetterGothicCode-B.ttf filter=lfs diff=lfs merge=lfs -text
37
- site/fonts/LetterGothicCode-R.ttf filter=lfs diff=lfs merge=lfs -text
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ fonts/LetterGothicCode-B.ttf filter=lfs diff=lfs merge=lfs -text
37
+ fonts/LetterGothicCode-R.ttf filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -2,3 +2,4 @@
2
  __pycache__/
3
  *.py[cod]
4
  scratch/
 
 
2
  __pycache__/
3
  *.py[cod]
4
  scratch/
5
+ node_modules/
Dockerfile DELETED
@@ -1,12 +0,0 @@
1
- FROM python:3.12-slim
2
- RUN useradd -m -u 1000 user
3
- USER user
4
- WORKDIR /home/user/app
5
- ENV PATH="/home/user/.local/bin:$PATH" PYTHONUNBUFFERED=1
6
- COPY --chown=user requirements.txt ./
7
- RUN pip install --no-cache-dir --user -r requirements.txt
8
- COPY --chown=user app.py bot_runner.py ./
9
- COPY --chown=user bot ./bot
10
- COPY --chown=user site ./site
11
- EXPOSE 7860
12
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -3,60 +3,41 @@ title: PyBot
3
  emoji: 🐍
4
  colorFrom: gray
5
  colorTo: gray
6
- sdk: docker
7
- app_port: 7860
8
  pinned: false
 
 
9
  ---
10
 
11
- PyBot: a conversational bot without LLMs, written by a swarm of LLM agents. One page: the story and a live chat that shows
12
- which of PyBot's modules and files produced each reply. Pinned to commit `64eb2c844738` of `HuggingFaceSwarms/SwarmChat-v8`.
 
13
 
14
- `app.py` serves `site/` and `POST /api/chat` / `POST /api/trace`; `bot/bot-<sha12>.zip` holds the bot's source (`bot/**.py` only);
15
- `bot_runner.py` runs it in a fresh worker per request. Built by `build_pybot.py` in the swarmchat-release workspace.
 
 
16
 
17
- ## Run locally
18
 
19
- Use Python 3.12 (required by the trace endpoint) on macOS or Linux:
 
 
 
 
 
 
 
20
 
21
- ```sh
22
- uv venv --python 3.12 .venv
23
- source .venv/bin/activate
24
- uv pip install -r requirements.txt
25
- uvicorn app:app --host 127.0.0.1 --port 7860
26
- ```
27
-
28
- Open http://127.0.0.1:7860. The health endpoint is `/health`.
29
- Keep local logs and temporary files in `scratch/`, which is ignored by Git.
30
-
31
- ### Progress chart
32
-
33
- Generate the chart snapshot from the leaderboard's v3 cache before starting the app:
34
 
35
  ```sh
36
- python build_progress.py /path/to/swarmchat-leaderboard-v3.json --source-repo scratch/v8-history.git
 
 
 
37
  ```
38
 
39
- The generated data stays in `scratch/progress.json` and is never committed. In the
40
- hosted Space, the private `HuggingFaceSwarms/pybot-chart` bucket is mounted read-only
41
- at `/home/user/app/scratch`, supplying `progress.json` across rebuilds and restarts.
42
- To publish a regenerated snapshot, activate the local environment and run:
43
-
44
- ```sh
45
- hf buckets cp scratch/progress.json hf://buckets/HuggingFaceSwarms/pybot-chart/progress.json
46
- ```
47
-
48
- The bucket mount is configured in the Space settings, separately from Git. Preserve
49
- it when changing volumes, and recreate it when duplicating the Space. No Hub token
50
- is needed by the app. The chart shows an unavailable message if the snapshot is absent.
51
- It uses the leaderboard's length-penalised percentage metric and latest successful
52
- 1,000-prompt run per revision, with all three SmolLM baselines. The contiguous outage
53
- plateau from November 7, 2026, 12:12:47 through 13:15:11 UTC (commit times) is omitted;
54
- other score fluctuations and the original revision positions are preserved.
55
-
56
- The right axis overlays physical lines in `bot/**/*.py` at each retained evaluated
57
- commit, including blank lines and comments but excluding tests. Counts come from
58
- the source repository's first-parent Git diffs, including deletions and merges.
59
- Fetch the source history into `scratch/v8-history.git` before building the snapshot;
60
- unavailable commits get a null code-size value, rather than an estimate. The first
61
- evaluated seed (`541b8def5107`) predates the current main history and is no longer
62
- available from the Hub; its win-rate point remains, but its code-size point is absent.
 
3
  emoji: 🐍
4
  colorFrom: gray
5
  colorTo: gray
6
+ sdk: static
7
+ app_file: index.html
8
  pinned: false
9
+ datasets:
10
+ - HuggingFaceSwarms/SwarmChat-v8
11
  ---
12
 
13
+ PyBot: a conversational bot without LLMs, written by a swarm of LLM agents. This Space is a chat demo that runs
14
+ PyBot entirely in your browser: [Pyodide](https://pyodide.org) (CPython compiled to WebAssembly) runs the bot's Python
15
+ in a Web Worker, so there is no server. Each reply shows which of PyBot's modules and files produced it.
16
 
17
+ The bot is commit `3185a7a9bc50` of `HuggingFaceSwarms/SwarmChat-v8`, the best run on the
18
+ [SwarmChat leaderboard](https://huggingface.co/spaces/HuggingFaceSwarms/swarmchat-leaderboard) (26.55% length-penalised
19
+ win rate on SmolTalkBench). Replaying 40 of that run's recorded conversations in the browser bundle gives byte-identical
20
+ replies.
21
 
22
+ ## Files
23
 
24
+ - `index.html`: the page (chat, the reply's trace, and a treemap of PyBot's files; Plotly from jsDelivr).
25
+ - `worker.js`: loads Pyodide 314.0.7 from jsDelivr and the bot bundle, then answers turns. Like transformers.js does
26
+ for model files, it keeps the bundle in Cache Storage, so a second visit doesn't download it again.
27
+ - `pybot_web.py`: the glue, ported from the previous server version of this Space (`bot_runner.py`, `app.py`): fixed
28
+ random seeds as in the eval server, the trace via `sys.monitoring`, and the file that holds a reply's text.
29
+ - `pybot-<sha12>.zip`: `bot/**.pyc` compiled for Pyodide's CPython 3.14 (loading skips compiling 33 MB of source),
30
+ plus `norm.json` (each file's text, letters and digits only) for finding the file that holds a reply.
31
+ - `tree.json`: PyBot's files and line counts for the treemap.
32
 
33
+ ## Rebuild for another commit
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  ```sh
36
+ hf download HuggingFaceSwarms/SwarmChat-v8 --repo-type dataset --revision <sha> --include "bot/**" --local-dir src
37
+ (cd src && zip -qr ../bot-src.zip bot -i '*.py')
38
+ npm install pyodide@314.0.7 # the same version as worker.js: bytecode only loads in the Python that compiled it
39
+ node build/build.mjs bot-src.zip build/build_bundle.py pybot-<sha12>.zip
40
  ```
41
 
42
+ Then regenerate `tree.json` (`[path, line count]` for every `bot/**.py`, plus `repo` and `sha`) and update `BUNDLE`
43
+ in `worker.js`. To test locally, serve this folder (e.g. `python -m http.server`) and open it.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py DELETED
@@ -1,120 +0,0 @@
1
- """PyBot release Space: the story page (site/) and the PyBot demo's chat endpoint.
2
-
3
- The bot is one pinned commit's bot/**.py, shipped in bot/bot-<sha12>.zip, run by bot_runner (from SwarmChat-Demo):
4
- one loader process, and a fresh forked worker per request that replays the conversation's user lines, so visitors
5
- never share bot memory. Run only trusted bot code here and add no secrets: the worker is not a security sandbox.
6
- """
7
- import re
8
- import sys
9
- import tempfile
10
- import threading
11
- import zipfile
12
- from pathlib import Path
13
- from typing import Literal
14
-
15
- from fastapi import FastAPI, HTTPException
16
- from fastapi.staticfiles import StaticFiles
17
- from fastapi.responses import FileResponse
18
- from pydantic import BaseModel, Field
19
-
20
- from bot_runner import Bot, user_lines
21
-
22
- HERE = Path(__file__).parent
23
- TREE = tempfile.TemporaryDirectory(prefix="swarmchat-source-")
24
- zipfile.ZipFile(next(HERE.glob("bot/bot-*.zip"))).extractall(TREE.name)
25
- app = FastAPI(title="PyBot")
26
- _bot, _bot_lock = None, threading.Lock()
27
- _slots = threading.BoundedSemaphore(4) # conversations answered at once; cpu-basic has 2 vCPUs
28
- _norm = lambda s: re.sub(r"[^a-z0-9]", "", s.lower())
29
- # Every bot file, letters and digits only, to find which one holds a reply's text verbatim.
30
- SOURCES = {p.relative_to(TREE.name).as_posix(): _norm(p.read_text(errors="replace")) for p in Path(TREE.name, "bot").rglob("*.py")}
31
-
32
-
33
- def bot():
34
- global _bot
35
- with _bot_lock:
36
- if _bot is None or not _bot.alive():
37
- _bot = Bot(Path(TREE.name), seed=0, timeout=30, workers=2, log=sys.stderr)
38
- return _bot
39
-
40
-
41
- threading.Thread(target=bot, daemon=True).start() # load at boot, so the first visitor does not wait
42
-
43
-
44
- class Message(BaseModel):
45
- role: Literal["user", "assistant"]
46
- content: str = Field(min_length=1, max_length=2000)
47
-
48
-
49
- class ChatRequest(BaseModel):
50
- messages: list[Message] = Field(min_length=1, max_length=60)
51
-
52
-
53
- @app.post("/api/chat")
54
- def chat(req: ChatRequest):
55
- lines = user_lines([m.model_dump() for m in req.messages])
56
- if not lines or req.messages[-1].role != "user":
57
- raise HTTPException(400, "A user message is required.")
58
- if sum(map(len, lines)) > 20000:
59
- raise HTTPException(400, "This conversation is too long. Reload the page to start again.")
60
- if not _slots.acquire(timeout=15):
61
- raise HTTPException(503, "The bot is busy. Please try again in a moment.")
62
- try:
63
- result = bot().ask(lines)
64
- finally:
65
- _slots.release()
66
- if result.get("status") != "ok" or len(result.get("replies", [])) != len(lines):
67
- raise HTTPException(502, "The bot failed on that line. Try another one.")
68
- return {"reply": result["replies"][-1]}
69
-
70
-
71
- def answer_source(reply: str, used: list[str]) -> str | None:
72
- """The one file that holds the reply verbatim (its first 40+ letters/digits), preferring files the winner called;
73
- None when the reply is composed, too short, or found in several files."""
74
- text = _norm(reply)
75
- for n in (60, 40):
76
- key = text[:n]
77
- if len(key) < 20: # too short to point at one place
78
- continue
79
- hits = [f for f, body in SOURCES.items() if key in body]
80
- hits = hits if len(hits) == 1 else [f for f in hits if f in used]
81
- if len(hits) == 1:
82
- return hits[0]
83
- return None
84
-
85
-
86
- @app.post("/api/trace")
87
- def trace(req: ChatRequest):
88
- """Like /api/chat, plus how the last reply was made: the module that won, the ones that also proposed a reply,
89
- the bot files the winner called, and the file holding the reply's text."""
90
- lines = user_lines([m.model_dump() for m in req.messages])
91
- if not lines or req.messages[-1].role != "user":
92
- raise HTTPException(400, "A user message is required.")
93
- if sum(map(len, lines)) > 20000:
94
- raise HTTPException(400, "This conversation is too long. Reload the page to start again.")
95
- if not _slots.acquire(timeout=15):
96
- raise HTTPException(503, "The bot is busy. Please try again in a moment.")
97
- try:
98
- result = bot().ask(lines, trace=True)
99
- finally:
100
- _slots.release()
101
- if result.get("status") != "ok" or len(result.get("replies", [])) != len(lines) or not result.get("trace"):
102
- raise HTTPException(502, "The bot failed on that line. Try another one.")
103
- reply, info = result["replies"][-1], result["trace"]
104
- return {"reply": reply, **info, "source": answer_source(reply, info["files"])}
105
-
106
-
107
- @app.get("/health")
108
- def health():
109
- return {"ok": True}
110
-
111
-
112
- @app.get("/api/progress")
113
- def progress():
114
- snapshot = HERE / "scratch" / "progress.json"
115
- if not snapshot.is_file():
116
- raise HTTPException(503, "The progress chart data is unavailable.")
117
- return FileResponse(snapshot, media_type="application/json")
118
-
119
-
120
- app.mount("/", StaticFiles(directory=HERE / "site", html=True), name="site")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bot_runner.py DELETED
@@ -1,195 +0,0 @@
1
- # From Space HuggingFaceSwarms/SwarmChat-Demo (bot_runner.py, itself adapted from flock's run4 eval server),
2
- # plus a trace mode: which modules proposed a reply, which won, and the bot files the winner called.
3
- """Serve a checkout of a swarm's chatbot as SwarmChat, an OpenAI-compatible endpoint for evals.
4
-
5
- python experiments/run4/eval/swarmchat_server.py <chatbot checkout> --port 8000
6
-
7
- Loading the bot takes seconds (hundreds of modules), answering takes milliseconds. So one
8
- process loads the bot and forks a worker per conversation, and every conversation starts from
9
- the same just-loaded state. The same lines get the same replies whatever the order or the
10
- concurrency of the requests, in this run or the next: the hash seed, the global `random` state
11
- (restored in each worker) and the seeds of the bot's own random.Random() instances are fixed.
12
- Replies that read the clock or seed from an object id (a few modules use id(memory)) can still
13
- change: ids move with address randomisation, which HF Jobs do not let us turn off.
14
-
15
- POST /v1/chat/completions feeds the request's user messages to the bot in order (system and
16
- assistant messages are ignored: the bot only reads user lines) and returns its last reply.
17
- There is no sandbox: the bot runs as the server's user, with an empty environment, so run it
18
- somewhere disposable such as an HF Job.
19
- """
20
-
21
- import json
22
- import os
23
- import signal
24
- import select
25
- import socket
26
- import subprocess
27
- import sys
28
- import tempfile
29
- from pathlib import Path
30
-
31
- LOADER = r'''
32
- import io, json, os, random, resource, runpy, signal, socket, sys, time
33
- root, seed, timeout, path, workers = sys.argv[1], int(sys.argv[2]), float(sys.argv[3]), sys.argv[4], int(sys.argv[5])
34
- sys.path.insert(0, root)
35
- status = os.fdopen(os.dup(1), "w")
36
- os.dup2(2, 1) # stray prints from the bot go to the log
37
- listener = socket.socket(socket.AF_UNIX)
38
- listener.bind(path)
39
- listener.listen(256)
40
- # A worker writes a byte here when it takes a conversation; the loader forks one per byte.
41
- spawn_r, spawn_w = os.pipe()
42
- os.write(spawn_w, bytes(workers))
43
-
44
-
45
- def converse():
46
- """A worker: take one connection, hold that conversation, send the result and exit."""
47
- conn = listener.accept()[0]
48
- os.write(spawn_w, b"\0")
49
- random.setstate(loaded) # random reseeds itself from the OS in every forked child
50
- started = time.monotonic()
51
- request = json.loads(conn.makefile("rb").readline())
52
- lines, trace = (request, False) if isinstance(request, list) else (request["lines"], bool(request.get("trace")))
53
- limit = int(timeout) + 1
54
- signal.alarm(limit)
55
- resource.setrlimit(resource.RLIMIT_CPU, (limit, limit + 1))
56
- if trace:
57
- result, out = "ok", {}
58
- try:
59
- out = traced(lines)
60
- except BaseException as exc:
61
- result = f"error {type(exc).__name__}"
62
- conn.sendall((json.dumps({"replies": out.get("replies", []), "trace": out.get("trace"), "status": result,
63
- "seconds": round(time.monotonic() - started, 3)}) + "\n").encode())
64
- os._exit(0)
65
- sys.stdin = io.StringIO("".join(line + "\n" for line in lines))
66
- sys.stdout = buffer = io.StringIO()
67
- result = "ok"
68
- try:
69
- runpy.run_module("bot", run_name="__main__")
70
- except BaseException as exc: # the fixed __main__ catches reply errors; this is the rest
71
- result = f"error {type(exc).__name__}"
72
- conn.sendall((json.dumps({"replies": buffer.getvalue().splitlines(), "status": result,
73
- "seconds": round(time.monotonic() - started, 3)}) + "\n").encode())
74
- os._exit(0)
75
-
76
-
77
- def traced(lines):
78
- """Replies like the fixed __main__ (one per line, whitespace-normalised, "Hello!" on errors), plus, for the last
79
- line, the bot's own decision (bot.runtime.turn: candidates and winner) and the bot files each proposing handler called.
80
- sys.monitoring records a function once and then disables that event, so tracing costs about nothing."""
81
- import bot.runtime as rt
82
- def say(text, fn):
83
- try:
84
- return " ".join(str(fn(text)).split()) or "Hello!"
85
- except Exception: # noqa: BLE001 - the fixed __main__'s rule
86
- return "Hello!"
87
- memory = {}
88
- replies = [say(line, lambda t: rt.reply(t, memory)) for line in lines[:-1]]
89
- mon, tool, bot_dir, seen, files = sys.monitoring, 5, os.path.join(root, "bot") + os.sep, set(), {}
90
- mon.use_tool_id(tool, "pybot-trace")
91
- def start(code, offset):
92
- if code.co_filename.startswith(bot_dir):
93
- seen.add("bot/" + code.co_filename[len(bot_dir):])
94
- return mon.DISABLE
95
- mon.register_callback(tool, mon.events.PY_START, start)
96
- def watch(name, respond):
97
- def run(text, mem):
98
- seen.clear(); mon.restart_events(); mon.set_events(tool, mon.events.PY_START)
99
- try:
100
- return respond(text, mem)
101
- finally:
102
- mon.set_events(tool, 0); files[name] = sorted(seen)
103
- return run
104
- rt._HANDLERS = [(name, priority, watch(name, respond)) for name, priority, respond in rt._HANDLERS]
105
- decision = {}
106
- def last(text):
107
- decision.update(rt.turn(text, memory))
108
- return decision["reply"]
109
- replies.append(say(lines[-1], last))
110
- module = decision.get("module")
111
- return {"replies": replies, "trace": {"module": module,
112
- "candidates": [c["module"] for c in decision.get("candidates", [])],
113
- "files": files.get(module, []) if module else []}}
114
-
115
-
116
- # Modules make their own random.Random(), which seeds itself from the OS: derive those seeds
117
- # from the run's seed and a counter instead, so every run starts in the same state.
118
- os_seeded, created = random.Random.seed, iter(range(1 << 62))
119
- def derived_seed(self, a=None, version=2):
120
- os_seeded(self, f"{seed}:{next(created)}" if a is None else a, version)
121
- random.Random.seed = derived_seed
122
- random.seed = random._inst.seed
123
- random.seed(seed)
124
- started = time.monotonic()
125
- import bot.runtime
126
- loaded = random.getstate()
127
- print(json.dumps({"load_s": round(time.monotonic() - started, 2),
128
- "handlers": len(bot.runtime._HANDLERS)}), file=status, flush=True)
129
- # Keep `workers` forks waiting for a connection. The workers read the requests, never the
130
- # loader, and the kernel reaps them.
131
- signal.signal(signal.SIGCHLD, signal.SIG_IGN)
132
- while True:
133
- os.read(spawn_r, 1)
134
- if os.fork() == 0:
135
- converse()
136
- '''
137
-
138
-
139
- class Bot:
140
- """The loader process; ask() holds one conversation in a fresh worker."""
141
-
142
- def __init__(self, tree: Path, *, seed: int, timeout: float, workers: int, log):
143
- self.timeout = timeout
144
- self.tmp = tempfile.TemporaryDirectory(prefix="swarmchat-")
145
- self.path = os.path.join(self.tmp.name, "bot.sock")
146
- # No -I: PYTHONHASHSEED must apply. The environment holds nothing else.
147
- self.proc = subprocess.Popen(
148
- [sys.executable, "-s", "-S", "-P", "-c", LOADER, str(tree), str(seed), str(timeout),
149
- self.path, str(workers)],
150
- cwd=self.tmp.name, env={"PYTHONHASHSEED": str(seed)}, stdout=subprocess.PIPE,
151
- stderr=log, text=True, start_new_session=True)
152
- if not select.select([self.proc.stdout], [], [], 90)[0]:
153
- self.close()
154
- raise RuntimeError("The bot did not load within 90 seconds.")
155
- line = self.proc.stdout.readline()
156
- if not line:
157
- self.close()
158
- raise RuntimeError("This commit could not load its bot; try another commit.")
159
- self.ready = json.loads(line)
160
-
161
- def ask(self, lines: list[str], trace: bool = False) -> dict:
162
- with socket.socket(socket.AF_UNIX) as conn:
163
- conn.settimeout(self.timeout + 5)
164
- conn.connect(self.path)
165
- conn.sendall((json.dumps({"lines": lines, "trace": True} if trace else lines) + "\n").encode())
166
- try:
167
- raw = conn.makefile("rb").readline()
168
- except TimeoutError:
169
- return {"replies": [], "status": "timeout"}
170
- # The worker died without replying, e.g. on its CPU or wall-clock limit.
171
- return json.loads(raw) if raw else {"replies": [], "status": "worker exited"}
172
-
173
- def alive(self) -> bool:
174
- return self.proc.poll() is None
175
-
176
- def close(self):
177
- if self.alive():
178
- os.killpg(self.proc.pid, signal.SIGKILL)
179
- self.proc.wait()
180
- self.proc.stdout.close()
181
- self.tmp.cleanup()
182
-
183
-
184
- def user_lines(messages) -> list[str]:
185
- """User message texts as single bot input lines (the bot reads one line per turn)."""
186
- lines = []
187
- for m in messages:
188
- if m.get("role") != "user":
189
- continue
190
- content = m.get("content") or ""
191
- if isinstance(content, list):
192
- content = " ".join(p.get("text", "") for p in content if p.get("type") == "text")
193
- lines.append(" ".join(str(content).split()))
194
- return lines
195
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/build.mjs ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Build the browser bundle inside the same Pyodide the page uses: node build.mjs <bot source zip> <build_bundle.py> <out zip>
2
+ import { loadPyodide } from "pyodide";
3
+ import { readFileSync, writeFileSync } from "node:fs";
4
+
5
+ const [src, script, out] = process.argv.slice(2);
6
+ const py = await loadPyodide();
7
+ py.unpackArchive(new Uint8Array(readFileSync(src)), "zip", { extractDir: "/in" });
8
+ py.FS.mkdirTree("/out");
9
+ py.runPython(readFileSync(script, "utf8"));
10
+ writeFileSync(out, py.FS.readFile("/out/pybot.zip"));
11
+ console.log("python", py.runPython("import sys; sys.version"), "->", out);
build/build_bundle.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run inside Pyodide (build.mjs): turn the bot's source zip into the browser bundle.
2
+
3
+ In: /in/bot/**.py (the bot at one commit)
4
+ Out: /out/pybot.zip with
5
+ - bot/**.pyc sourceless bytecode for this Pyodide's CPython (imports skip compiling 33 MB of source),
6
+ co_filename kept as /pybot/bot/<path>.py so the trace still names the right files;
7
+ - norm.json every file's text, letters and digits only, for finding the file that holds a reply.
8
+ """
9
+ import json
10
+ import os
11
+ import py_compile
12
+ import re
13
+ import zipfile
14
+
15
+ SRC, OUT = "/in", "/out"
16
+ norm = {}
17
+ with zipfile.ZipFile(f"{OUT}/pybot.zip", "w", zipfile.ZIP_DEFLATED, compresslevel=9) as bundle:
18
+ for dirpath, _, names in sorted(os.walk(f"{SRC}/bot")):
19
+ for name in sorted(names):
20
+ if not name.endswith(".py"):
21
+ continue
22
+ path = os.path.join(dirpath, name)
23
+ rel = os.path.relpath(path, SRC)
24
+ pyc = py_compile.compile(path, cfile=f"/tmp/{rel}c", dfile=f"/pybot/{rel}", doraise=True,
25
+ invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH)
26
+ bundle.write(pyc, rel + "c")
27
+ with open(path, encoding="utf-8", errors="replace") as handle:
28
+ norm[rel] = re.sub(r"[^a-z0-9]", "", handle.read().lower())
29
+ bundle.writestr("norm.json", json.dumps(norm, separators=(",", ":")))
30
+ print(json.dumps({"files": len(norm), "bytes": os.path.getsize(f"{OUT}/pybot.zip")}))
build_progress.py DELETED
@@ -1,127 +0,0 @@
1
- """Build the local chart snapshot from the leaderboard's cached summaries."""
2
-
3
- import argparse
4
- import json
5
- import math
6
- import subprocess
7
- from pathlib import Path
8
-
9
- # The abrupt outage plateau, bounded by the last/first affected commit timestamps.
10
- # Explicit bounds retain genuine regressions elsewhere; never filter on score.
11
- OUTAGE_START = "20261107T121247Z"
12
- OUTAGE_END = "20261107T131511Z"
13
- MODEL = "HuggingFaceSwarms/SwarmChat-v8"
14
-
15
-
16
- def count_source_tree(repository, commit):
17
- """Count a source tree directly for evaluated commits outside the main history."""
18
- tree = subprocess.check_output(
19
- ["git", "-C", str(repository), "ls-tree", "-r", commit, "--", "bot"], text=True)
20
- objects = [line.split()[2] for line in tree.splitlines() if line.endswith(".py")]
21
- total = 0
22
- with subprocess.Popen(["git", "-C", str(repository), "cat-file", "--batch"],
23
- stdin=subprocess.PIPE, stdout=subprocess.PIPE) as process:
24
- for oid in objects:
25
- process.stdin.write((oid + "\n").encode())
26
- process.stdin.flush()
27
- size = int(process.stdout.readline().split()[2])
28
- blob = process.stdout.read(size)
29
- process.stdout.read(1)
30
- total += blob.count(b"\n") + bool(blob and not blob.endswith(b"\n"))
31
- process.stdin.close()
32
- if process.wait() != 0:
33
- raise RuntimeError(f"Could not read source tree for {commit}")
34
- return total
35
-
36
-
37
- def source_line_counts(repository):
38
- """Physical lines in bot/**/*.py, including blanks/comments, excluding tests.
39
-
40
- Accumulate first-parent diffs, including deletions and merge changes. Disabling
41
- rename detection makes moves across the bot/ boundary count correctly.
42
- """
43
- command = ["git", "-C", str(repository), "log", "--first-parent", "--reverse",
44
- "--format=commit:%H", "--numstat", "--no-renames",
45
- "--diff-merges=first-parent", "HEAD"]
46
- counts, commit, total = {}, None, 0
47
- with subprocess.Popen(command, stdout=subprocess.PIPE, text=True) as process:
48
- for line in process.stdout:
49
- line = line.rstrip("\n")
50
- if line.startswith("commit:"):
51
- if commit is not None:
52
- counts[commit[:12]] = total
53
- commit = line.removeprefix("commit:")
54
- elif "\t" in line:
55
- added, removed, path = line.split("\t", 2)
56
- if path.startswith("bot/") and path.endswith(".py"):
57
- if added == "-" or removed == "-":
58
- raise ValueError(f"Cannot count binary Python file: {path}")
59
- total += int(added) - int(removed)
60
- if process.wait() != 0:
61
- raise RuntimeError("Could not read source history")
62
- if commit is not None:
63
- counts[commit[:12]] = total
64
- return counts
65
-
66
-
67
- def build_progress(cache):
68
- latest = {}
69
- for path, summary in cache["summaries"].items():
70
- meta = summary.get("metadata", {})
71
- metric = summary.get("primary_metric", {})
72
- score = metric.get("value")
73
- if (meta.get("task") != "smoltalk_bench_gold"
74
- or meta.get("status") != "success" or meta.get("task_args")
75
- or meta.get("total_samples") != 1000
76
- or metric.get("name") != "length_penalised_win_rate"
77
- or not isinstance(score, (int, float)) or not math.isfinite(score)):
78
- continue
79
- key = (meta["model_id"], meta["revision"])
80
- order = (meta["completed_at"], path)
81
- if key not in latest or order > latest[key][0]:
82
- latest[key] = (order, summary)
83
- revisions = sorted((revision, summary) for (model, revision), (_, summary)
84
- in latest.items() if model == MODEL)
85
- points, excluded = [], 0
86
- for index, (revision, summary) in enumerate(revisions, 1):
87
- if OUTAGE_START <= revision.split("-")[0] <= OUTAGE_END:
88
- excluded += 1
89
- continue
90
- points.append({"revision": index, "commit": revision.split("-")[1],
91
- "score": summary["primary_metric"]["value"]})
92
- baselines = sorted([
93
- {"name": model.split("/")[-1], "score": summary["primary_metric"]["value"]}
94
- for (model, revision), (_, summary) in latest.items()
95
- if model.startswith("HuggingFaceTB/SmolLM-") and revision == "main"
96
- ], key=lambda baseline: baseline["score"])
97
- if not points or len(baselines) != 3:
98
- raise ValueError("Expected v8 evaluations and all three SmolLM baselines")
99
- return {"points": points, "baselines": baselines, "excluded": excluded,
100
- "fetched_at": cache["fetched_at"]}
101
-
102
-
103
- if __name__ == "__main__":
104
- parser = argparse.ArgumentParser(description=__doc__)
105
- parser.add_argument("cache", type=Path)
106
- parser.add_argument("--source-repo", type=Path, required=True,
107
- help="Git repository containing the evaluated source history")
108
- args = parser.parse_args()
109
- result = build_progress(json.loads(args.cache.read_text()))
110
- counts = source_line_counts(args.source_repo)
111
- missing = []
112
- for point in result["points"]:
113
- if point["commit"] not in counts:
114
- exists = subprocess.run(
115
- ["git", "-C", str(args.source_repo), "cat-file", "-e", point["commit"]],
116
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
117
- counts[point["commit"]] = count_source_tree(args.source_repo, point["commit"]) if exists else None
118
- if counts[point["commit"]] is None:
119
- missing.append(point["commit"])
120
- point["lines_of_code"] = counts[point["commit"]]
121
- result["missing_source_commits"] = missing
122
- if missing:
123
- print(f"No source available for {len(missing)} commits; leaving code-size gaps: {', '.join(missing)}")
124
- output = Path(__file__).parent / "scratch" / "progress.json"
125
- output.parent.mkdir(exist_ok=True)
126
- output.write_text(json.dumps(result, separators=(",", ":")) + "\n")
127
- print(f"Wrote {len(result['points'])} points; excluded {result['excluded']} outage evaluations")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
{site/fonts → fonts}/LetterGothicCode-B.ttf RENAMED
File without changes
{site/fonts → fonts}/LetterGothicCode-R.ttf RENAMED
File without changes
{site/fonts → fonts}/routed-gothic.ttf RENAMED
File without changes
index.html ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>PyBot</title>
7
+ <style>
8
+ @font-face { font-family:"Routed Gothic"; src:url("fonts/routed-gothic.ttf") format("truetype"); }
9
+ @font-face { font-family:"Letter Gothic"; src:url("fonts/LetterGothicCode-R.ttf") format("truetype"); }
10
+ @font-face { font-family:"Letter Gothic"; font-weight:700; src:url("fonts/LetterGothicCode-B.ttf") format("truetype"); }
11
+ /* the swarms landing page's palette and type; one accent, for the module whose reply won */
12
+ :root { --ink:#000; --paper:#fff; --mute:#5c5c5c; --hair:#e9e9e9; --faint:#d6d6d6; --off:#f4f4f4; --accent:#eb6834;
13
+ --lead:"Routed Gothic",Helvetica,Arial,sans-serif; --mono:"Letter Gothic",ui-monospace,Menlo,monospace; color-scheme:light; }
14
+ * { box-sizing:border-box; }
15
+ html, body { height:100%; }
16
+ body { margin:0; background:var(--paper); color:var(--ink); font:18px/1.55 var(--lead); }
17
+ .page { min-height:100%; max-width:1400px; margin:0 auto; padding:clamp(16px,3vw,32px) clamp(16px,3vw,32px) 20px;
18
+ display:flex; flex-direction:column; gap:16px; }
19
+ header { display:flex; align-items:baseline; justify-content:space-between; gap:12px 24px; flex-wrap:wrap; }
20
+ h1 { font:400 clamp(34px,5vw,52px)/1 var(--lead); letter-spacing:.02em; margin:0; }
21
+ #status { flex:1 1 320px; max-width:520px; font:13px/1.4 var(--mono); color:var(--mute); }
22
+ #status .bar { height:4px; background:var(--off); border:1px solid var(--ink); margin-top:6px; overflow:hidden; }
23
+ #status .bar i { display:block; height:100%; width:0; background:var(--ink); transition:width .2s; }
24
+ #status .bar.busy i { width:30%; animation:slide 1.2s ease-in-out infinite; }
25
+ #status.done .bar { display:none; }
26
+ #status.failed { color:var(--accent); }
27
+ @keyframes slide { from { transform:translateX(-100%); } to { transform:translateX(340%); } }
28
+ .grid { flex:1; display:grid; grid-template-columns:minmax(0,5fr) minmax(0,6fr); gap:16px; min-height:0; }
29
+ .app { border:1px solid var(--ink); display:flex; flex-direction:column; min-height:0; }
30
+ .chat { height:calc(100dvh - 170px); min-height:420px; }
31
+ .trace { height:calc(100dvh - 170px); min-height:420px; }
32
+ #log { flex:1; overflow-y:auto; display:flex; flex-direction:column; gap:10px; padding:14px; }
33
+ .msg { max-width:85%; padding:7px 12px; white-space:pre-wrap; overflow-wrap:anywhere; font-size:17px; }
34
+ .me { align-self:flex-end; background:var(--ink); color:var(--paper); }
35
+ .bot { align-self:flex-start; background:var(--off); border:1px solid var(--hair); }
36
+ .bot.typing { color:var(--mute); }
37
+ .hint { color:var(--mute); text-align:center; margin:auto 0 0; font-size:16px; }
38
+ .chips { display:flex; flex-wrap:wrap; justify-content:center; gap:8px; margin:10px 0 auto; }
39
+ .chips button { font:15px var(--lead); text-transform:none; letter-spacing:0; color:var(--ink); background:var(--paper);
40
+ border:1px solid var(--ink); padding:5px 12px; }
41
+ .chips button:hover { background:var(--off); }
42
+ form { display:flex; gap:8px; padding:10px 14px; border-top:1px solid var(--ink); }
43
+ input { flex:1; min-width:0; font:17px var(--lead); color:var(--ink); background:var(--paper); border:1px solid var(--ink); padding:8px 12px; }
44
+ button { font:700 14px var(--mono); text-transform:uppercase; letter-spacing:.04em; color:var(--paper); background:var(--ink); border:1px solid var(--ink); padding:0 16px; cursor:pointer; }
45
+ button.ghost { color:var(--ink); background:var(--paper); width:42px; padding:0; font:400 20px var(--lead); }
46
+ button:disabled, input:disabled { opacity:.45; cursor:default; }
47
+ input:focus-visible, button:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
48
+ #why { font:14px/1.5 var(--mono); color:var(--mute); min-height:44px; padding:10px 14px; border-bottom:1px solid var(--hair); }
49
+ #why b { color:var(--ink); }
50
+ #map { flex:1; min-height:0; width:100%; }
51
+ .legend { display:flex; flex-wrap:wrap; gap:6px 18px; font:13px var(--mono); color:var(--mute); padding:8px 14px 12px; border-top:1px solid var(--hair); }
52
+ .legend i { display:inline-block; width:16px; height:12px; vertical-align:-1px; margin-right:6px; border:1px solid var(--ink); }
53
+ .sw-win { background:var(--accent); border-width:2px !important; } .sw-src { background:var(--ink); } .sw-used { background:#8a8a8a; }
54
+ .sw-prop { background:var(--paper); border-width:2px !important; } .sw-off { background:var(--off); border-color:var(--faint) !important; }
55
+ .foot { font:13px var(--mono); color:var(--mute); margin:0; } .foot a { color:inherit; }
56
+ @media (max-width:900px) {
57
+ .grid { grid-template-columns:minmax(0,1fr); }
58
+ .chat { height:min(70dvh,560px); min-height:360px; }
59
+ .trace { height:auto; min-height:0; }
60
+ #map { flex:none; height:420px; }
61
+ }
62
+ @media (max-width:640px) { body { font-size:17px; } .msg { max-width:92%; } #map { height:360px; } }
63
+ @media (prefers-reduced-motion:reduce) { #status .bar.busy i { animation:none; width:100%; opacity:.3; } }
64
+ </style>
65
+ </head>
66
+ <body>
67
+ <div class="page">
68
+ <header>
69
+ <h1>PyBot</h1>
70
+ <div id="status" role="status" aria-live="polite"><span id="status-text">Starting…</span><div class="bar busy"><i></i></div></div>
71
+ </header>
72
+
73
+ <div class="grid">
74
+ <section class="app chat" aria-label="Talk to PyBot">
75
+ <div id="log" aria-live="polite"></div>
76
+ <form id="f" autocomplete="off">
77
+ <button type="button" id="reset" class="ghost" title="Start a new conversation" aria-label="New chat">↻</button>
78
+ <input id="q" maxlength="500" placeholder="Say something to PyBot" aria-label="Message">
79
+ <button id="send">Send</button>
80
+ </form>
81
+ </section>
82
+
83
+ <section class="app trace" aria-label="How PyBot made the last reply">
84
+ <div id="why">Say something to PyBot, then see which of its files made the reply.</div>
85
+ <div id="map" role="img" aria-label="PyBot's files; the ones used for the last reply are highlighted"></div>
86
+ <div class="legend">
87
+ <span><i class="sw-win"></i>module whose reply won</span>
88
+ <span><i class="sw-src"></i>file holding the reply's text</span>
89
+ <span><i class="sw-used"></i>files the winner called</span>
90
+ <span><i class="sw-prop"></i>modules that also proposed a reply</span>
91
+ <span><i class="sw-off"></i>not used</span>
92
+ </div>
93
+ </section>
94
+ </div>
95
+
96
+ <p class="foot">PyBot at commit <a id="commit" target="_blank" rel="noopener"></a>, running in your browser with <a href="https://pyodide.org" target="_blank" rel="noopener">Pyodide</a>. Click a file to open it on the Hub.</p>
97
+ </div>
98
+
99
+ <script src="https://cdn.jsdelivr.net/npm/plotly.js-dist-min@2.35.2/plotly.min.js"></script>
100
+ <script>
101
+ const INK = "#000000", PAPER = "#ffffff", OFF = "#f4f4f4", FAINT = "#d6d6d6", USED = "#8a8a8a", WIN = "#eb6834", MONO = "'Letter Gothic', ui-monospace, Menlo, monospace";
102
+ const EXAMPLES = ["Hi, I'm Sam", "Tell me a joke", "How do I make a cup of tea?", "What is 17 times 23?", "I feel sad today", "Can you recommend a book?"];
103
+ const $ = (id) => document.getElementById(id);
104
+ const log = $("log"), q = $("q"), send = $("send"), why = $("why"), statusBox = $("status"), statusText = $("status-text"), bar = statusBox.querySelector(".bar");
105
+
106
+ // ---- the bot, in a Web Worker ----
107
+ const worker = new Worker("worker.js", {type: "module"});
108
+ // The worker script itself failed (e.g. the Pyodide CDN is blocked): report it like a load error.
109
+ worker.onerror = (e) => { e.preventDefault(); worker.onmessage({data: {type: "error", message: e.message || "the Python runtime could not be downloaded"}}); };
110
+ let botReady = false, nextId = 0, turn = 0, pending = null; // pending: {id, bubble, mine}
111
+ const mb = (n) => (n / 1e6).toFixed(1);
112
+ function setStatus(text, fraction) {
113
+ statusText.textContent = text;
114
+ bar.classList.toggle("busy", fraction == null);
115
+ bar.querySelector("i").style.width = fraction == null ? "" : `${Math.round(fraction * 100)}%`;
116
+ }
117
+ worker.onmessage = ({data: m}) => {
118
+ if (m.type === "progress") {
119
+ if (m.stage === "runtime") setStatus("Downloading Python (Pyodide) and PyBot…");
120
+ else if (m.stage === "cached") setStatus("Downloading Python (Pyodide)… PyBot is cached in your browser");
121
+ else if (m.stage === "download") setStatus(`Downloading PyBot · ${mb(m.loaded)}${m.total ? ` / ${mb(m.total)}` : ""} MB`, m.total ? m.loaded / m.total : null);
122
+ else if (m.stage === "import") setStatus(`Loading PyBot's ${TREE ? TREE.files.length : ""} Python files…`);
123
+ } else if (m.type === "ready") {
124
+ botReady = true; statusBox.classList.add("done");
125
+ setStatus(`Ready · ${m.modules} modules loaded in ${m.load_s.toFixed(1)} s · Python ${m.python} in your browser, no server`);
126
+ if (!pending) { q.disabled = send.disabled = false; q.placeholder = "Say something to PyBot"; }
127
+ } else if (m.type === "reply") {
128
+ if (!pending || m.id !== pending.id) return;
129
+ const {bubble, mine} = pending; pending = null;
130
+ if (mine === turn) {
131
+ bubble.textContent = m.reply; bubble.classList.remove("typing");
132
+ last = m; why.innerHTML = explain(m); draw();
133
+ }
134
+ done();
135
+ } else if (m.type === "error") {
136
+ if (m.id == null) { // loading failed
137
+ statusBox.classList.add("failed"); setStatus(`PyBot could not start: ${m.message}`, 0);
138
+ if (pending) { pending.bubble.textContent = "PyBot could not start. Reload the page to try again."; pending.bubble.classList.remove("typing"); pending = null; }
139
+ q.disabled = send.disabled = true; return;
140
+ }
141
+ if (pending && m.id === pending.id) { pending.bubble.textContent = "PyBot could not answer that. Try another line."; pending.bubble.classList.remove("typing"); pending = null; }
142
+ done();
143
+ }
144
+ };
145
+ function done() { q.disabled = send.disabled = false; q.focus({preventScroll:true}); log.scrollTop = log.scrollHeight; }
146
+
147
+ // ---- the file map ----
148
+ let TREE = null, dirs, ids, labels, parents, values, last = null; // last: the last reply's trace
149
+ const url = (p) => `https://huggingface.co/datasets/${TREE.repo}/blob/${TREE.sha}/${p}`;
150
+ function roleOf(id) {
151
+ if (!last || dirs.has(id)) return null;
152
+ const mod = (m) => `bot/modules/${m}.py`;
153
+ if (last.module && id === mod(last.module)) return "won";
154
+ if (id === last.source) return "text";
155
+ if (last.files.includes(id)) return "used";
156
+ if (last.candidates.some((m) => mod(m) === id)) return "proposed";
157
+ return "off";
158
+ }
159
+ function draw() {
160
+ if (!TREE || !window.Plotly) return;
161
+ const roles = ids.map(roleOf);
162
+ const colors = ids.map((id, i) => dirs.has(id) ? PAPER : !last ? OFF : {won: WIN, text: INK, used: USED, proposed: PAPER, off: OFF}[roles[i]]);
163
+ const widths = roles.map((r) => (r === "won" || r === "proposed" ? 3 : 1));
164
+ const lines = ids.map((id, i) => (dirs.has(id) ? INK : roles[i] === "won" || roles[i] === "proposed" || roles[i] === "text" ? INK : roles[i] === "used" ? PAPER : FAINT));
165
+ const hover = roles.map((r) => ({won: "won: its reply was used", text: "holds the reply's text", used: "called by the winner",
166
+ proposed: "also proposed a reply", off: "not used for this reply"}[r] || ""));
167
+ Plotly.react("map", [{type:"treemap", ids, labels, parents, values, branchvalues:"total", maxdepth:3,
168
+ marker:{colors, line:{color:lines, width:widths}}, customdata:hover,
169
+ pathbar:{visible:true, thickness:22, textfont:{family:MONO, size:12, color:INK}},
170
+ texttemplate:"%{label}", textfont:{family:MONO, size:12},
171
+ hovertemplate:"<b>%{id}</b><br>%{value:,} lines<br>%{customdata}<extra></extra>"}],
172
+ {margin:{l:0, r:0, t:26, b:0}, paper_bgcolor:PAPER, font:{family:MONO, color:INK},
173
+ hoverlabel:{bgcolor:PAPER, bordercolor:INK, font:{family:MONO, size:13, color:INK}}},
174
+ {responsive:true, displaylogo:false, displayModeBar:false});
175
+ }
176
+ fetch("tree.json").then((r) => r.json()).then((tree) => {
177
+ TREE = tree;
178
+ const commit = $("commit"); commit.textContent = TREE.sha.slice(0, 7); commit.href = `https://huggingface.co/datasets/${TREE.repo}/tree/${TREE.sha}/bot`;
179
+ dirs = new Map();
180
+ for (const [p, n] of TREE.files) { const parts = p.split("/"); for (let i = 1; i < parts.length; i++) { const d = parts.slice(0, i).join("/"); dirs.set(d, (dirs.get(d) || 0) + n); } }
181
+ ids = [...dirs.keys(), ...TREE.files.map(([p]) => p)];
182
+ labels = ids.map((id) => (dirs.has(id) ? id.split("/").pop() + "/" : id.split("/").pop()));
183
+ parents = ids.map((id) => (id.includes("/") ? id.slice(0, id.lastIndexOf("/")) : ""));
184
+ values = [...dirs.values(), ...TREE.files.map(([, n]) => n)];
185
+ draw();
186
+ $("map").on("plotly_treemapclick", (ev) => { const id = ev.points[0].id; if (!dirs.has(id)) { window.open(url(id), "_blank", "noopener"); return false; } });
187
+ });
188
+
189
+ // ---- the chat ----
190
+ const add = (cls, text) => { const p = document.createElement("div"); p.className = "msg " + cls; p.textContent = text; log.appendChild(p); log.scrollTop = log.scrollHeight; return p; };
191
+ function explain(t) {
192
+ const ms = ` · ${t.ms < 1000 ? `${t.ms} ms` : `${(t.ms / 1000).toFixed(1)} s`}`;
193
+ if (!t.module) return "No module had a reply, so PyBot fell back to its default line." + ms;
194
+ const others = t.candidates.filter((m) => m !== t.module);
195
+ return `<b>${t.module}</b> won${others.length ? ` over ${others.map((m) => `<b>${m}</b>`).join(", ")}` : " (the only module with a reply)"}`
196
+ + ` · it called <b>${t.files.length}</b> files` + (t.source ? ` · the reply's text is in <b>${t.source.replace("bot/", "")}</b>` : "") + ms;
197
+ }
198
+ function welcome(text) {
199
+ log.innerHTML = "";
200
+ const hint = document.createElement("p"); hint.className = "hint"; hint.textContent = text; log.appendChild(hint);
201
+ const chips = document.createElement("div"); chips.className = "chips";
202
+ for (const ex of EXAMPLES) {
203
+ const b = document.createElement("button"); b.type = "button"; b.textContent = ex;
204
+ b.addEventListener("click", () => { if (!pending) { q.value = ex; $("f").requestSubmit(); } });
205
+ chips.appendChild(b);
206
+ }
207
+ log.appendChild(chips);
208
+ }
209
+ function ask(text) {
210
+ log.querySelectorAll(".hint, .chips").forEach((el) => el.remove());
211
+ add("me", text); q.value = ""; q.disabled = send.disabled = true;
212
+ const bubble = add("bot typing", botReady ? "…" : "… (PyBot is still loading)");
213
+ pending = {id: ++nextId, bubble, mine: turn};
214
+ worker.postMessage({type: "turn", id: pending.id, text});
215
+ }
216
+ $("f").addEventListener("submit", (e) => {
217
+ e.preventDefault();
218
+ const text = q.value.trim(); if (!text || pending || statusBox.classList.contains("failed")) return;
219
+ ask(text);
220
+ });
221
+ $("reset").addEventListener("click", () => {
222
+ turn++; pending = null; last = null; draw();
223
+ why.textContent = "New conversation. PyBot has forgotten everything you said.";
224
+ worker.postMessage({type: "reset"});
225
+ welcome("New conversation. Say something, or try one of these:");
226
+ q.value = ""; q.disabled = send.disabled = false; q.focus({preventScroll:true});
227
+ });
228
+ welcome("Say something to PyBot, or try one of these:");
229
+ q.focus({preventScroll:true});
230
+ </script>
231
+ </body>
232
+ </html>
bot/bot-64eb2c844738.zip → pybot-3185a7a9bc50.zip RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:6e0650e67c8a4ab9935aac7f0f2f866c15daafed1e835d85ee4744d434a4a91d
3
- size 9528378
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6c8eb9648a6b10baac097c349a05f2bd6546076125cb4a47ce4873835a03f958
3
+ size 18004369
pybot_web.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PyBot in the browser: load the bot once, then answer turns and explain each reply.
2
+
3
+ Ported from the PyBot Space's server (bot_runner.py's LOADER and traced(), app.py's answer_source()):
4
+ - Random seeds are fixed the way the eval server fixes them, so replies match the benchmark run as
5
+ closely as a browser allows. The server forks a fresh worker per request; here `reset()` restores the
6
+ just-loaded random state and starts a new memory instead.
7
+ - For each reply: the module that won, the ones that also proposed a reply, the bot files the winner
8
+ called (sys.monitoring, one PY_START event per function), and the file holding the reply's text.
9
+ """
10
+ import json
11
+ import os
12
+ import random
13
+ import re
14
+ import sys
15
+ import time
16
+
17
+ ROOT = "/pybot"
18
+ SEED = 0
19
+ _norm = lambda s: re.sub(r"[^a-z0-9]", "", s.lower())
20
+
21
+ _rt = None
22
+ _loaded_state = None
23
+ _memory = {}
24
+ _files = {}
25
+ _sources = None
26
+
27
+
28
+ def load():
29
+ """Import the bot (seconds); returns {"load_s", "modules", "handlers"}."""
30
+ global _rt, _loaded_state
31
+ sys.path.insert(0, ROOT)
32
+ # Modules make their own random.Random(), which seeds itself from the OS: derive those seeds
33
+ # from the run's seed and a counter instead, so every load starts in the same state.
34
+ os_seeded, created = random.Random.seed, iter(range(1 << 62))
35
+
36
+ def derived_seed(self, a=None, version=2):
37
+ os_seeded(self, f"{SEED}:{next(created)}" if a is None else a, version)
38
+
39
+ random.Random.seed = derived_seed
40
+ random.seed = random._inst.seed
41
+ random.seed(SEED)
42
+ started = time.monotonic()
43
+ import bot.runtime as rt
44
+
45
+ _rt = rt
46
+ _loaded_state = random.getstate()
47
+ _watch_handlers()
48
+ return {"load_s": round(time.monotonic() - started, 2), "modules": len(rt._MODULES), "handlers": len(rt._HANDLERS)}
49
+
50
+
51
+ def _watch_handlers():
52
+ """Wrap every respond() so a turn records the bot files each handler called."""
53
+ mon, tool = sys.monitoring, 5
54
+ bot_dir, seen = os.path.join(ROOT, "bot") + os.sep, set()
55
+ mon.use_tool_id(tool, "pybot-trace")
56
+
57
+ def start(code, offset):
58
+ if code.co_filename.startswith(bot_dir):
59
+ seen.add("bot/" + code.co_filename[len(bot_dir):])
60
+ return mon.DISABLE
61
+
62
+ mon.register_callback(tool, mon.events.PY_START, start)
63
+
64
+ def watch(name, respond):
65
+ def run(text, mem):
66
+ seen.clear()
67
+ mon.restart_events()
68
+ mon.set_events(tool, mon.events.PY_START)
69
+ try:
70
+ return respond(text, mem)
71
+ finally:
72
+ mon.set_events(tool, 0)
73
+ _files[name] = sorted(seen)
74
+ return run
75
+
76
+ _rt._HANDLERS = [(name, priority, watch(name, respond)) for name, priority, respond in _rt._HANDLERS]
77
+
78
+
79
+ def reset():
80
+ """A new conversation: empty memory and the just-loaded random state."""
81
+ global _memory
82
+ _memory = {}
83
+ random.setstate(_loaded_state)
84
+
85
+
86
+ def turn(text):
87
+ """One reply as JSON: {"reply", "module", "candidates", "files", "source", "ms"}."""
88
+ text = " ".join(str(text).split())
89
+ _files.clear()
90
+ started = time.monotonic()
91
+ decision = {}
92
+ try:
93
+ decision = _rt.turn(text, _memory)
94
+ reply = " ".join(str(decision["reply"]).split()) or "Hello!"
95
+ except Exception: # noqa: BLE001 - the fixed __main__'s rule
96
+ reply = "Hello!"
97
+ ms = round((time.monotonic() - started) * 1000)
98
+ module = decision.get("module")
99
+ files = _files.get(module, []) if module else []
100
+ return json.dumps({"reply": reply, "module": module,
101
+ "candidates": [c["module"] for c in decision.get("candidates", [])],
102
+ "files": files, "source": answer_source(reply, files), "ms": ms})
103
+
104
+
105
+ def answer_source(reply, used):
106
+ """The one file that holds the reply verbatim (its first 40+ letters/digits), preferring files the winner called;
107
+ None when the reply is composed, too short, or found in several files."""
108
+ global _sources
109
+ if _sources is None: # every bot file, letters and digits only (built by build_bundle.py)
110
+ with open(os.path.join(ROOT, "norm.json"), encoding="utf-8") as handle:
111
+ _sources = json.load(handle)
112
+ text = _norm(reply)
113
+ for n in (60, 40):
114
+ key = text[:n]
115
+ if len(key) < 20: # too short to point at one place
116
+ continue
117
+ hits = [f for f, body in _sources.items() if key in body]
118
+ hits = hits if len(hits) == 1 else [f for f in hits if f in used]
119
+ if len(hits) == 1:
120
+ return hits[0]
121
+ return None
requirements.txt DELETED
@@ -1,2 +0,0 @@
1
- fastapi==0.115.6
2
- uvicorn==0.32.1
 
 
 
site/fonts/routed-gothic-narrow.ttf DELETED
Binary file (57.5 kB)
 
site/index.html DELETED
The diff for this file is too large to render. See raw diff
 
site/progress.js DELETED
@@ -1,79 +0,0 @@
1
- async function drawProgress() {
2
- const status = document.getElementById("progress-status");
3
- const chart = document.getElementById("progress-chart");
4
- const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
5
- const animateOnScroll = !reducedMotion.matches && "IntersectionObserver" in window;
6
- if (animateOnScroll) chart.classList.add("awaiting-reveal");
7
- try {
8
- const response = await fetch("api/progress", {cache: "no-store"});
9
- if (!response.ok) throw new Error("Progress data unavailable");
10
- const data = await response.json();
11
- const points = data.points;
12
- const extent = [points[0].revision, points[points.length - 1].revision];
13
- const traces = [{
14
- type: "scatter", mode: "lines", name: "PyBot",
15
- x: points.map(p => p.revision), y: points.map(p => p.score),
16
- customdata: points.map(p => p.commit),
17
- line: {color: "#eb6834", width: 2},
18
- hovertemplate: "PyBot · revision %{x:,}<br>Win rate: %{y:.2f}%<br>Commit %{customdata}<extra></extra>"
19
- }, {
20
- type: "scatter", mode: "lines", name: "Lines of code", yaxis: "y2",
21
- x: points.map(p => p.revision), y: points.map(p => p.lines_of_code),
22
- customdata: points.map(p => p.commit),
23
- line: {color: "#287a9b", width: 2},
24
- hovertemplate: "Revision %{x:,}<br>Lines of code: %{y:,}<br>Python source in bot/ (including blanks and comments)<br>Commit %{customdata}<extra></extra>"
25
- }, ...data.baselines.map((baseline, index) => ({
26
- type: "scatter", mode: "lines",
27
- name: baseline.name,
28
- x: extent, y: [baseline.score, baseline.score],
29
- line: {color: ["#999999", "#555555", "#000000"][index], width: 1.5,
30
- dash: ["dot", "dash", "dashdot"][index]},
31
- hovertemplate: `${baseline.name}<br>Win rate: %{y:.2f}%<extra></extra>`
32
- }))];
33
- await Plotly.newPlot("progress-chart", traces, {
34
- margin: {l: 60, r: 72, t: 30, b: 60},
35
- paper_bgcolor: "#ffffff", plot_bgcolor: "#ffffff",
36
- font: {family: "'Letter Gothic', monospace", size: 12, color: "#000000"},
37
- xaxis: {title: {text: "PyBot commit", standoff: 12},
38
- range: extent, gridcolor: "#eeeeee", zeroline: false, automargin: true},
39
- yaxis: {title: {text: "Win rate (%)", standoff: 10}, rangemode: "tozero",
40
- color: "#eb6834",
41
- gridcolor: "#eeeeee", zerolinecolor: "#dddddd", automargin: true},
42
- yaxis2: {title: {text: "Lines of code", standoff: 10}, overlaying: "y", side: "right",
43
- rangemode: "tozero", color: "#287a9b", tickformat: "~s",
44
- showgrid: false, zeroline: false, automargin: true},
45
- showlegend: false,
46
- annotations: data.baselines.map(baseline => ({
47
- text: baseline.name, x: 1, xref: "paper", y: baseline.score, yref: "y",
48
- xanchor: "right", yanchor: "bottom", yshift: 4, showarrow: false,
49
- font: {size: 11, color: "#000000"}, bgcolor: "#ffffff", borderpad: 2
50
- })),
51
- hoverlabel: {bgcolor: "#ffffff", bordercolor: "#000000"}
52
- }, {responsive: true, displayModeBar: false, displaylogo: false});
53
- status.hidden = true;
54
- if (animateOnScroll) {
55
- const observer = new IntersectionObserver(entries => {
56
- if (!entries.some(entry => entry.isIntersecting && entry.intersectionRatio >= 0.3)) return;
57
- observer.disconnect();
58
- // Reveal the rendered SVG traces, keeping both axes and all data fixed.
59
- // This avoids redrawing thousands of points on every animation frame.
60
- chart.classList.remove("awaiting-reveal");
61
- if (reducedMotion.matches) return;
62
- const animations = [...chart.querySelectorAll(".scatterlayer .trace")].map(trace =>
63
- trace.animate([
64
- {clipPath: "inset(-10px 100% -10px 0)"},
65
- {clipPath: "inset(-10px 0% -10px 0)"}
66
- ], {duration: 1800, easing: "cubic-bezier(0.22, 1, 0.36, 1)"}));
67
- const finish = () => animations.forEach(animation => animation.finish());
68
- reducedMotion.addEventListener("change", finish, {once: true});
69
- Promise.all(animations.map(animation => animation.finished)).finally(() =>
70
- reducedMotion.removeEventListener("change", finish));
71
- }, {threshold: 0.3});
72
- observer.observe(chart);
73
- }
74
- } catch (error) {
75
- document.getElementById("progress-chart").hidden = true;
76
- status.textContent = "The progress chart is currently unavailable.";
77
- }
78
- }
79
- drawProgress();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tree.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"repo":"HuggingFaceSwarms/SwarmChat-v8","sha":"3185a7a9bc50491205b19f64839f8c70962c4369","files":[["bot/__init__.py",1],["bot/__main__.py",19],["bot/compose/__init__.py",523],["bot/compose/a_place_nobody_can_see.py",439],["bot/compose/badnews.py",1272],["bot/compose/breakup.py",74],["bot/compose/checking.py",966],["bot/compose/childwords.py",163],["bot/compose/circumstance.py",156],["bot/compose/doubt.py",138],["bot/compose/draft.py",718],["bot/compose/facts.py",96],["bot/compose/fallback.py",15517],["bot/compose/fallback_chain.py",5117],["bot/compose/fallback_data.py",8200],["bot/compose/fallback_pools.py",691],["bot/compose/fallback_words.py",514],["bot/compose/find_a_place.py",352],["bot/compose/finding_out.py",806],["bot/compose/findout.py",526],["bot/compose/fit.py",3892],["bot/compose/gap.py",388],["bot/compose/goodnews.py",1548],["bot/compose/handoff.py",149],["bot/compose/harm.py",334],["bot/compose/how_it_went.py",325],["bot/compose/invitation.py",134],["bot/compose/judgement.py",41],["bot/compose/lead.py",249],["bot/compose/leaving.py",776],["bot/compose/limit.py",248],["bot/compose/liveask.py",93],["bot/compose/loss.py",4955],["bot/compose/missing_half.py",296],["bot/compose/moving.py",119],["bot/compose/no_eyes.py",479],["bot/compose/nottheirbest.py",117],["bot/compose/occasionplan.py",460],["bot/compose/onething.py",462],["bot/compose/only_they_know.py",563],["bot/compose/onscreen.py",504],["bot/compose/own_facts.py",198],["bot/compose/place_facts.py",138],["bot/compose/plans.py",212],["bot/compose/pleased.py",103],["bot/compose/postcode.py",392],["bot/compose/prices.py",262],["bot/compose/quantity.py",1242],["bot/compose/reaction.py",291],["bot/compose/reading.py",1778],["bot/compose/recall.py",112],["bot/compose/receiving_end.py",207],["bot/compose/remark.py",1952],["bot/compose/repair.py",418],["bot/compose/rotten_luck.py",313],["bot/compose/routes.py",538],["bot/compose/rule.py",224],["bot/compose/ruledout.py",680],["bot/compose/safetyask.py",193],["bot/compose/saidby.py",203],["bot/compose/setting_off.py",241],["bot/compose/still.py",290],["bot/compose/talkto.py",170],["bot/compose/taste.py",436],["bot/compose/the_rule_for_it.py",322],["bot/compose/the_turn_after.py",157],["bot/compose/their_day.py",436],["bot/compose/theirperson.py",225],["bot/compose/trouble.py",1203],["bot/compose/unseen.py",208],["bot/compose/urgent.py",646],["bot/compose/verdict.py",5788],["bot/compose/waited_for.py",236],["bot/compose/who.py",329],["bot/compose/who_to_see.py",307],["bot/compose/whole.py",57],["bot/compose/why.py",2072],["bot/compose/worn.py",226],["bot/knowledge/__init__.py",157],["bot/knowledge/affording.py",96],["bot/knowledge/animals.py",900],["bot/knowledge/appointments.py",285],["bot/knowledge/babyfood.py",2136],["bot/knowledge/bake_faults.py",221],["bot/knowledge/bikes.py",1096],["bot/knowledge/body.py",4450],["bot/knowledge/body_energy.py",79],["bot/knowledge/business.py",494],["bot/knowledge/cakes.py",298],["bot/knowledge/caring.py",255],["bot/knowledge/cars.py",4231],["bot/knowledge/chilli.py",267],["bot/knowledge/cleaning.py",4848],["bot/knowledge/clothing.py",4485],["bot/knowledge/comeback.py",188],["bot/knowledge/complaints.py",1066],["bot/knowledge/conditions.py",203],["bot/knowledge/consumer.py",5081],["bot/knowledge/cook_kit.py",493],["bot/knowledge/cooking.py",12280],["bot/knowledge/cooking_entries.py",11145],["bot/knowledge/cooking_gaps.py",2356],["bot/knowledge/cooking_more.py",2197],["bot/knowledge/cooking_tables.py",3171],["bot/knowledge/costs.py",1345],["bot/knowledge/creative.py",96],["bot/knowledge/crisis_replies.py",2054],["bot/knowledge/decorating.py",228],["bot/knowledge/devices.py",319],["bot/knowledge/differences.py",304],["bot/knowledge/dish_verdicts.py",437],["bot/knowledge/dishes.py",5960],["bot/knowledge/drinks.py",240],["bot/knowledge/eithers.py",474],["bot/knowledge/entertainment.py",9526],["bot/knowledge/explains.py",2943],["bot/knowledge/family.py",13174],["bot/knowledge/family_more.py",1057],["bot/knowledge/fitness.py",1669],["bot/knowledge/fitweek.py",240],["bot/knowledge/freshness.py",1127],["bot/knowledge/friends.py",1099],["bot/knowledge/fruit.py",413],["bot/knowledge/fun.py",713],["bot/knowledge/garden.py",6685],["bot/knowledge/garden_tools.py",429],["bot/knowledge/geography.py",1710],["bot/knowledge/habits.py",848],["bot/knowledge/health.py",10567],["bot/knowledge/history.py",2030],["bot/knowledge/home.py",8069],["bot/knowledge/home_craft.py",10052],["bot/knowledge/house_odds.py",438],["bot/knowledge/how_it_works.py",151],["bot/knowledge/idioms.py",465],["bot/knowledge/ingredient_swaps.py",63],["bot/knowledge/joint_time.py",131],["bot/knowledge/joints.py",556],["bot/knowledge/keeping.py",1892],["bot/knowledge/keeping_cooked.py",115],["bot/knowledge/kit_lists.py",69],["bot/knowledge/kitchen.py",146],["bot/knowledge/kitchen_odds.py",529],["bot/knowledge/kitchen_prep.py",43],["bot/knowledge/language.py",3486],["bot/knowledge/left_on.py",266],["bot/knowledge/lifeadmin.py",4448],["bot/knowledge/lookup.py",6012],["bot/knowledge/machine_door.py",90],["bot/knowledge/materials.py",666],["bot/knowledge/maths.py",5766],["bot/knowledge/meals.py",2602],["bot/knowledge/measures.py",636],["bot/knowledge/meat_in_a_card.py",72],["bot/knowledge/microwave_methods.py",80],["bot/knowledge/money.py",10232],["bot/knowledge/money_words.py",362],["bot/knowledge/music.py",398],["bot/knowledge/nature.py",855],["bot/knowledge/no_kit.py",335],["bot/knowledge/notable.py",765],["bot/knowledge/nursing.py",54],["bot/knowledge/nutrition.py",2691],["bot/knowledge/occasions.py",202],["bot/knowledge/odours.py",283],["bot/knowledge/opinions.py",1508],["bot/knowledge/panic_talk.py",211],["bot/knowledge/parking.py",45],["bot/knowledge/pasta.py",303],["bot/knowledge/pensions.py",114],["bot/knowledge/pets.py",11217],["bot/knowledge/pharmacy.py",414],["bot/knowledge/photography.py",186],["bot/knowledge/picking.py",422],["bot/knowledge/picks.py",164],["bot/knowledge/plans.py",2612],["bot/knowledge/power_heat.py",43],["bot/knowledge/pregnancy.py",1302],["bot/knowledge/prices.py",186],["bot/knowledge/quantity.py",37],["bot/knowledge/rice.py",350],["bot/knowledge/routines.py",552],["bot/knowledge/rubbish.py",750],["bot/knowledge/sauces.py",196],["bot/knowledge/schoolmaths.py",217],["bot/knowledge/science.py",11486],["bot/knowledge/seasoning.py",277],["bot/knowledge/shopping.py",4141],["bot/knowledge/sleep.py",1046],["bot/knowledge/spice_heat.py",246],["bot/knowledge/sport.py",3981],["bot/knowledge/still_ok.py",913],["bot/knowledge/study.py",4795],["bot/knowledge/swaps.py",1042],["bot/knowledge/takes.py",329],["bot/knowledge/technology.py",7770],["bot/knowledge/travel.py",11322],["bot/knowledge/units.py",3420],["bot/knowledge/vegetables.py",961],["bot/knowledge/warm_food.py",161],["bot/knowledge/whenbuy.py",403],["bot/knowledge/wording.py",2806],["bot/knowledge/work.py",8132],["bot/memory/__init__.py",1309],["bot/memory/a_year_of_birth.py",122],["bot/memory/aboutthem.py",121],["bot/memory/ack.py",6079],["bot/memory/again.py",866],["bot/memory/already.py",906],["bot/memory/amounts.py",260],["bot/memory/another.py",480],["bot/memory/asleep.py",263],["bot/memory/baking.py",107],["bot/memory/carried_on.py",289],["bot/memory/carry.py",783],["bot/memory/cause.py",672],["bot/memory/change.py",153],["bot/memory/choice.py",663],["bot/memory/company.py",340],["bot/memory/condition.py",364],["bot/memory/corrected.py",1921],["bot/memory/counts.py",882],["bot/memory/deadline.py",103],["bot/memory/decided.py",697],["bot/memory/detail.py",1182],["bot/memory/doubt.py",155],["bot/memory/enough.py",666],["bot/memory/facts.py",10397],["bot/memory/favourite.py",519],["bot/memory/filler.py",36],["bot/memory/guest.py",218],["bot/memory/healing.py",331],["bot/memory/history.py",494],["bot/memory/hope.py",144],["bot/memory/hours.py",150],["bot/memory/household.py",191],["bot/memory/kidcount.py",131],["bot/memory/limit.py",860],["bot/memory/limits.py",288],["bot/memory/lineback.py",445],["bot/memory/matters.py",308],["bot/memory/meant.py",687],["bot/memory/more.py",220],["bot/memory/named.py",2610],["bot/memory/narrow.py",2652],["bot/memory/newborn.py",90],["bot/memory/not_a_trip.py",127],["bot/memory/not_this.py",235],["bot/memory/obstacle.py",644],["bot/memory/onset.py",378],["bot/memory/outfit.py",346],["bot/memory/passing_the_time.py",210],["bot/memory/pending.py",172],["bot/memory/people.py",249],["bot/memory/pile.py",124],["bot/memory/places.py",249],["bot/memory/plan.py",468],["bot/memory/plans.py",450],["bot/memory/prefer.py",190],["bot/memory/price.py",600],["bot/memory/purse.py",173],["bot/memory/quotes.py",71],["bot/memory/rates.py",289],["bot/memory/recall.py",590],["bot/memory/reject.py",681],["bot/memory/relations.py",1766],["bot/memory/remedy.py",184],["bot/memory/reminded.py",155],["bot/memory/rename.py",108],["bot/memory/repair.py",938],["bot/memory/repeat.py",4737],["bot/memory/resize.py",379],["bot/memory/returned.py",215],["bot/memory/returns.py",286],["bot/memory/saving.py",1899],["bot/memory/setback.py",318],["bot/memory/size.py",232],["bot/memory/spent.py",685],["bot/memory/spoken.py",137],["bot/memory/stands.py",235],["bot/memory/store_lines.py",1035],["bot/memory/store_replies.py",12266],["bot/memory/stuck.py",447],["bot/memory/surname.py",321],["bot/memory/swap.py",766],["bot/memory/taking.py",409],["bot/memory/taste.py",507],["bot/memory/the_years.py",213],["bot/memory/thread.py",1786],["bot/memory/times.py",223],["bot/memory/told.py",1552],["bot/memory/too_late.py",174],["bot/memory/topics.py",7810],["bot/memory/tried.py",1665],["bot/memory/trips.py",1518],["bot/memory/warmth.py",479],["bot/memory/when.py",1694],["bot/memory/whereabout.py",140],["bot/memory/whose_body.py",76],["bot/memory/workplace.py",198],["bot/memory/years_in.py",309],["bot/modules/__init__.py",1],["bot/modules/a_count_said.py",40],["bot/modules/a_day_somebody_else_keeps.py",121],["bot/modules/aboutachild.py",39],["bot/modules/already.py",26],["bot/modules/answers.py",11236],["bot/modules/arbiter.py",4029],["bot/modules/attribute_said_again.py",603],["bot/modules/bulky.py",153],["bot/modules/carers_turn.py",422],["bot/modules/chat.py",16948],["bot/modules/done_ask.py",240],["bot/modules/doubt.py",24],["bot/modules/favourites.py",161],["bot/modules/figure_asked_again.py",40],["bot/modules/greeting.py",901],["bot/modules/grief.py",818],["bot/modules/household.py",47],["bot/modules/how_long_a_stay.py",205],["bot/modules/intent.py",38],["bot/modules/invitation.py",48],["bot/modules/judge_ask.py",131],["bot/modules/keeping.py",137],["bot/modules/kid_tally.py",28],["bot/modules/live_source.py",294],["bot/modules/live_times.py",241],["bot/modules/loss.py",140],["bot/modules/memory_store.py",5955],["bot/modules/memory_tables.py",469],["bot/modules/my_work.py",26],["bot/modules/name_taken_back.py",107],["bot/modules/night.py",29],["bot/modules/nudge.py",56],["bot/modules/recap.py",150],["bot/modules/receiving_end.py",23],["bot/modules/record_due.py",171],["bot/modules/ruled_out.py",42],["bot/modules/safety.py",10139],["bot/modules/safety_cards.py",2126],["bot/modules/safety_phrases.py",1169],["bot/modules/safety_tables.py",7231],["bot/modules/said_back.py",28],["bot/modules/saving_followup.py",288],["bot/modules/steps.py",23],["bot/modules/taking_asked_again.py",45],["bot/modules/talkto.py",140],["bot/modules/their_life.py",285],["bot/modules/their_pastime.py",358],["bot/modules/theirperson.py",31],["bot/modules/thing_i_did_with.py",194],["bot/modules/trip_date_said_again.py",88],["bot/modules/understand.py",16],["bot/modules/urgent.py",30],["bot/modules/where_it_answers.py",47],["bot/modules/who_to_see.py",40],["bot/modules/words.py",197],["bot/modules/years_in.py",46],["bot/runtime.py",120],["bot/understanding/__init__.py",1761],["bot/understanding/body.py",566],["bot/understanding/breeds.py",171],["bot/understanding/kinds.py",6582],["bot/understanding/lexicon.py",1949],["bot/understanding/normalize.py",6657],["bot/understanding/references.py",419],["bot/understanding/senses.py",178],["bot/understanding/topics.py",3744]]}
worker.js ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // PyBot in a Web Worker: Pyodide (CPython compiled to WebAssembly) runs the bot's Python, so the page stays responsive.
2
+ // Messages in: {type: "turn", id, text} | {type: "reset"}
3
+ // Messages out: {type: "progress", stage, loaded?, total?} | {type: "ready", ...} | {type: "reply", id, ...} | {type: "error", id?, message}
4
+ // A module worker: new Worker("worker.js", {type: "module"}).
5
+ import { loadPyodide } from "https://cdn.jsdelivr.net/pyodide/v314.0.7/full/pyodide.mjs";
6
+
7
+ const PYODIDE = "https://cdn.jsdelivr.net/pyodide/v314.0.7/full/";
8
+ const BUNDLE = "pybot-3185a7a9bc50.zip"; // bot/**.pyc compiled for this Pyodide's Python, plus norm.json
9
+ const CACHE = "pybot-bundles-v1";
10
+
11
+ const post = (msg) => self.postMessage(msg);
12
+
13
+ // Like transformers.js caches model files: the bundle is downloaded once, then read from Cache Storage.
14
+ async function fetchBundle() {
15
+ const url = new URL(BUNDLE, self.location.href).href;
16
+ let cache = null;
17
+ try { cache = await caches.open(CACHE); } catch { /* no Cache Storage (e.g. private window): download every time */ }
18
+ const hit = cache && await cache.match(url).catch(() => null);
19
+ if (hit) {
20
+ post({type: "progress", stage: "cached"});
21
+ return new Uint8Array(await hit.arrayBuffer());
22
+ }
23
+ const res = await fetch(url);
24
+ if (!res.ok) throw new Error(`Could not download PyBot (HTTP ${res.status}).`);
25
+ const total = Number(res.headers.get("content-length")) || 0;
26
+ const reader = res.body.getReader(), chunks = [];
27
+ let loaded = 0;
28
+ for (;;) {
29
+ const {done, value} = await reader.read();
30
+ if (done) break;
31
+ chunks.push(value); loaded += value.length;
32
+ post({type: "progress", stage: "download", loaded, total});
33
+ }
34
+ const bytes = new Uint8Array(loaded);
35
+ let at = 0;
36
+ for (const c of chunks) { bytes.set(c, at); at += c.length; }
37
+ if (cache) {
38
+ // Keep one bundle: drop older commits' bundles, then store this one.
39
+ for (const req of await cache.keys()) if (req.url !== url) await cache.delete(req);
40
+ await cache.put(url, new Response(bytes, {headers: {"content-type": "application/zip"}})).catch(() => {});
41
+ }
42
+ return bytes;
43
+ }
44
+
45
+ let web = null;
46
+ const ready = (async () => {
47
+ post({type: "progress", stage: "runtime"});
48
+ const [py, bundle, glue] = await Promise.all([
49
+ loadPyodide({indexURL: PYODIDE, env: {PYTHONHASHSEED: "0"}}), // the eval server's hash seed, so replies match
50
+ fetchBundle(),
51
+ fetch("pybot_web.py").then((r) => { if (!r.ok) throw new Error("Could not download pybot_web.py."); return r.text(); }),
52
+ ]);
53
+ post({type: "progress", stage: "import"});
54
+ py.unpackArchive(bundle, "zip", {extractDir: "/pybot"});
55
+ py.FS.writeFile("/pybot_web.py", glue);
56
+ py.runPython("import sys; sys.path.insert(0, '/')");
57
+ web = py.pyimport("pybot_web");
58
+ const info = web.load().toJs({dict_converter: Object.fromEntries});
59
+ web.reset();
60
+ post({type: "ready", ...info, python: py.runPython("import sys; sys.version.split()[0]"), pyodide: py.version});
61
+ })().catch((err) => post({type: "error", message: String(err && err.message || err)}));
62
+
63
+ self.onmessage = async ({data}) => {
64
+ await ready;
65
+ if (!web) return;
66
+ if (data.type === "reset") { web.reset(); return; }
67
+ if (data.type === "turn") {
68
+ try {
69
+ post({type: "reply", id: data.id, ...JSON.parse(web.turn(data.text))});
70
+ } catch (err) {
71
+ post({type: "error", id: data.id, message: String(err && err.message || err)});
72
+ }
73
+ }
74
+ };