Spaces:
Running
Running
Turn PyBot into an in-browser web demo of the best bot (3185a7a, 26.55%)
Browse filesPyodide 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 +2 -2
- .gitignore +1 -0
- Dockerfile +0 -12
- README.md +27 -46
- app.py +0 -120
- bot_runner.py +0 -195
- build/build.mjs +11 -0
- build/build_bundle.py +30 -0
- build_progress.py +0 -127
- {site/fonts → fonts}/LetterGothicCode-B.ttf +0 -0
- {site/fonts → fonts}/LetterGothicCode-R.ttf +0 -0
- {site/fonts → fonts}/routed-gothic.ttf +0 -0
- index.html +232 -0
- bot/bot-64eb2c844738.zip → pybot-3185a7a9bc50.zip +2 -2
- pybot_web.py +121 -0
- requirements.txt +0 -2
- site/fonts/routed-gothic-narrow.ttf +0 -0
- site/index.html +0 -0
- site/progress.js +0 -79
- tree.json +1 -0
- worker.js +74 -0
.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 |
-
|
| 37 |
-
|
|
|
|
| 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:
|
| 7 |
-
|
| 8 |
pinned: false
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
-
PyBot: a conversational bot without LLMs, written by a swarm of LLM agents.
|
| 12 |
-
|
|
|
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
##
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 37 |
```
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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 |
+
};
|