wl-benchmark 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wl_benchmark/__init__.py +0 -0
- wl_benchmark/__main__.py +3 -0
- wl_benchmark/cli.py +250 -0
- wl_benchmark/client.py +166 -0
- wl_benchmark/publisher.py +113 -0
- wl_benchmark/reporter.py +61 -0
- wl_benchmark/review_pdf.py +297 -0
- wl_benchmark/runner.py +87 -0
- wl_benchmark/site.py +275 -0
- wl_benchmark/tasks/__init__.py +46 -0
- wl_benchmark/tasks/base.py +70 -0
- wl_benchmark/tasks/essay.py +226 -0
- wl_benchmark/tasks/quant.py +259 -0
- wl_benchmark/tasks/scheduling.py +475 -0
- wl_benchmark/tasks/svg.py +255 -0
- wl_benchmark/tasks_data/essay/01-storytelling/rubric.md +69 -0
- wl_benchmark/tasks_data/essay/01-storytelling/rubric.png +0 -0
- wl_benchmark/tasks_data/essay/01-storytelling/spec.json +47 -0
- wl_benchmark/tasks_data/essay/01-storytelling/task.md +12 -0
- wl_benchmark/tasks_data/essay/02-argument/rubric.md +69 -0
- wl_benchmark/tasks_data/essay/02-argument/rubric.png +0 -0
- wl_benchmark/tasks_data/essay/02-argument/spec.json +64 -0
- wl_benchmark/tasks_data/essay/02-argument/task.md +10 -0
- wl_benchmark/tasks_data/essay/03-proposal/rubric.md +70 -0
- wl_benchmark/tasks_data/essay/03-proposal/rubric.png +0 -0
- wl_benchmark/tasks_data/essay/03-proposal/spec.json +78 -0
- wl_benchmark/tasks_data/essay/03-proposal/task.md +11 -0
- wl_benchmark/tasks_data/quant/fe-mining-01.json +1143 -0
- wl_benchmark/tasks_data/scheduling/term-plan-2627t1.json +743 -0
- wl_benchmark/tasks_data/svg/stage1-riding.json +24 -0
- wl_benchmark/tasks_data/svg/stage2-relation.json +13 -0
- wl_benchmark/tasks_data/svg/stage3-architecture.json +16 -0
- wl_benchmark-0.4.0.dist-info/METADATA +244 -0
- wl_benchmark-0.4.0.dist-info/RECORD +37 -0
- wl_benchmark-0.4.0.dist-info/WHEEL +5 -0
- wl_benchmark-0.4.0.dist-info/entry_points.txt +2 -0
- wl_benchmark-0.4.0.dist-info/top_level.txt +1 -0
wl_benchmark/__init__.py
ADDED
|
File without changes
|
wl_benchmark/__main__.py
ADDED
wl_benchmark/cli.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""CLI entry: wl-bench {run|list-tasks|report|doctor}
|
|
2
|
+
|
|
3
|
+
`run` / `doctor` interactively ask for the target under test
|
|
4
|
+
(endpoint / key / model) every time — the tool stores no provider presets.
|
|
5
|
+
Use --endpoint/--key/--model flags to skip the interactive prompts.
|
|
6
|
+
|
|
7
|
+
Run as: ./wl-bench <cmd> | python3 -m wl_benchmark <cmd>
|
|
8
|
+
| wl-bench <cmd> (after pip install)
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import getpass
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import urllib.request
|
|
18
|
+
|
|
19
|
+
from .publisher import (cleanup_run, load_site_config, prompt_site_config,
|
|
20
|
+
publish_run)
|
|
21
|
+
from .reporter import write_report
|
|
22
|
+
from .review_pdf import build_review_pdf
|
|
23
|
+
from .runner import run_all
|
|
24
|
+
from .tasks import TASK_TYPES, build_tasks
|
|
25
|
+
|
|
26
|
+
PKG_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
DEFAULT_TASKS_ROOT = os.path.join(PKG_DIR, "tasks_data")
|
|
28
|
+
DEFAULT_CONFIG = os.path.join("config", "bench.json") # optional run params
|
|
29
|
+
VERSION = "0.4.0"
|
|
30
|
+
BRAND = "WL-Benchmark"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ------------------------------------------------------- interactive input
|
|
34
|
+
def _list_models(base_url: str, api_key: str) -> list:
|
|
35
|
+
try:
|
|
36
|
+
req = urllib.request.Request(
|
|
37
|
+
base_url.rstrip("/") + "/models",
|
|
38
|
+
headers={"Authorization": f"Bearer {api_key}"})
|
|
39
|
+
with urllib.request.urlopen(req, timeout=15) as r:
|
|
40
|
+
return [m.get("id") for m in json.load(r).get("data", []) if m.get("id")]
|
|
41
|
+
except Exception:
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _prompt_provider(args) -> dict:
|
|
46
|
+
"""Interactively collect endpoint / key / model.
|
|
47
|
+
--endpoint/--key/--model flags skip the matching prompt."""
|
|
48
|
+
print(f"{BRAND} — target under test (nothing is stored)")
|
|
49
|
+
base_url = (args.endpoint or "").strip()
|
|
50
|
+
while not base_url.startswith(("http://", "https://")):
|
|
51
|
+
base_url = input("Endpoint (OpenAI-compatible, e.g. "
|
|
52
|
+
"https://api.example.com/v1): ").strip()
|
|
53
|
+
|
|
54
|
+
api_key = (args.key or "").strip()
|
|
55
|
+
while not api_key:
|
|
56
|
+
api_key = getpass.getpass("API key (input hidden): ").strip()
|
|
57
|
+
|
|
58
|
+
model = (args.model or "").strip()
|
|
59
|
+
if not model:
|
|
60
|
+
models = _list_models(base_url, api_key)
|
|
61
|
+
if models:
|
|
62
|
+
print(f"The endpoint offers {len(models)} models:")
|
|
63
|
+
for i, m in enumerate(models, 1):
|
|
64
|
+
print(f" {i:2d}. {m}")
|
|
65
|
+
sel = input("Pick a number, or type a model name: ").strip()
|
|
66
|
+
if sel.isdigit() and 1 <= int(sel) <= len(models):
|
|
67
|
+
model = models[int(sel) - 1]
|
|
68
|
+
elif sel:
|
|
69
|
+
model = sel
|
|
70
|
+
while not model:
|
|
71
|
+
model = input("Model: ").strip()
|
|
72
|
+
|
|
73
|
+
return {"name": model, "base_url": base_url, "api_key": api_key,
|
|
74
|
+
"model": model}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _load_run_cfg(path: str) -> dict:
|
|
78
|
+
"""Optional run-parameter overrides; sensible defaults otherwise."""
|
|
79
|
+
cfg = {"tasks_data_root": DEFAULT_TASKS_ROOT,
|
|
80
|
+
"rubric_modality": "auto",
|
|
81
|
+
"max_tokens": 2048, "essay_max_tokens": 4096,
|
|
82
|
+
"svg_max_tokens": 16384, "scheduling_max_tokens": 4096,
|
|
83
|
+
"scheduling_max_turns": 16, "quant_max_turns": 16,
|
|
84
|
+
"quant_max_tokens": 8192,
|
|
85
|
+
"temperature": 0.2, "timeout": 300}
|
|
86
|
+
if path and os.path.exists(path):
|
|
87
|
+
with open(path, encoding="utf-8") as f:
|
|
88
|
+
raw = json.load(f)
|
|
89
|
+
cfg["tasks_data_root"] = raw.get("tasks_data_root",
|
|
90
|
+
cfg["tasks_data_root"])
|
|
91
|
+
cfg.update(raw.get("run", {}))
|
|
92
|
+
return cfg
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cmd_run(args) -> None:
|
|
96
|
+
provider = _prompt_provider(args)
|
|
97
|
+
run_cfg = _load_run_cfg(args.config)
|
|
98
|
+
out_dir = run_all(provider, run_cfg,
|
|
99
|
+
only_types=args.tasks.split(",") if args.tasks else None,
|
|
100
|
+
out_root=args.out)
|
|
101
|
+
summary = write_report(out_dir)
|
|
102
|
+
print(f"[cli] summary -> {summary}")
|
|
103
|
+
_maybe_publish(out_dir, keep=args.keep, skip=args.no_upload)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _maybe_publish(run_dir: str, keep: bool = False, skip: bool = False) -> None:
|
|
107
|
+
"""Default flow: upload the run to the benchmark site, then wipe the
|
|
108
|
+
local data. Falls back to keeping everything if upload is skipped or
|
|
109
|
+
fails (a later `wlb publish <run-dir>` can retry)."""
|
|
110
|
+
run_id = os.path.basename(run_dir.rstrip("/"))
|
|
111
|
+
if skip:
|
|
112
|
+
print(f"[cli] upload skipped (--no-upload); local data kept at {run_dir}")
|
|
113
|
+
return
|
|
114
|
+
cfg = load_site_config()
|
|
115
|
+
if cfg is None and sys.stdin.isatty():
|
|
116
|
+
cfg = prompt_site_config()
|
|
117
|
+
if cfg is None:
|
|
118
|
+
print(f"[cli] site upload not configured — local data kept at {run_dir}")
|
|
119
|
+
print(" (set WL_BENCH_URL / WL_BENCH_TOKEN, or delete the")
|
|
120
|
+
print(" run dir manually)")
|
|
121
|
+
return
|
|
122
|
+
try:
|
|
123
|
+
url = publish_run(run_dir, cfg)
|
|
124
|
+
except Exception as e: # noqa: BLE001
|
|
125
|
+
print(f"[cli] UPLOAD FAILED — local data kept at {run_dir}")
|
|
126
|
+
print(f" {e}")
|
|
127
|
+
print(" retry later with: wlb publish " + run_dir)
|
|
128
|
+
return
|
|
129
|
+
print(f"[cli] published -> {url}")
|
|
130
|
+
print(f"[cli] share link: {url}")
|
|
131
|
+
if keep:
|
|
132
|
+
print(f"[cli] local data kept (--keep): {run_dir}")
|
|
133
|
+
elif cleanup_run(run_dir):
|
|
134
|
+
print(f"[cli] local run data deleted: {run_dir}")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def cmd_list_tasks(args) -> None:
|
|
138
|
+
run_cfg = _load_run_cfg(args.config)
|
|
139
|
+
tasks = build_tasks(run_cfg.get("tasks_data_root", DEFAULT_TASKS_ROOT),
|
|
140
|
+
run_cfg, artifacts_root="/tmp/wl-bench-list")
|
|
141
|
+
print(f"{len(tasks)} task(s):")
|
|
142
|
+
for t in tasks:
|
|
143
|
+
extra = ""
|
|
144
|
+
if t.task_type == "essay":
|
|
145
|
+
extra = f" rubric={os.path.basename(t.spec.get('rubric') or '-')}"
|
|
146
|
+
elif t.task_type == "svg":
|
|
147
|
+
extra = f" stage={t.spec.get('stage')}"
|
|
148
|
+
print(f" [{t.task_type:10s}] {t.task_id}{extra}")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_publish(args) -> None:
|
|
152
|
+
cfg = load_site_config()
|
|
153
|
+
if cfg is None:
|
|
154
|
+
cfg = prompt_site_config()
|
|
155
|
+
if cfg is None:
|
|
156
|
+
print("[publish] no Cloudflare configuration — nothing uploaded")
|
|
157
|
+
return
|
|
158
|
+
url = publish_run(args.run_dir, cfg)
|
|
159
|
+
print(f"[publish] {url}")
|
|
160
|
+
if args.keep:
|
|
161
|
+
print(f"[publish] local data kept: {args.run_dir}")
|
|
162
|
+
elif cleanup_run(args.run_dir):
|
|
163
|
+
print(f"[publish] local run data deleted: {args.run_dir}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def cmd_report(args) -> None:
|
|
167
|
+
write_report(args.run_dir)
|
|
168
|
+
pdf = build_review_pdf(args.run_dir)
|
|
169
|
+
print(f"summary -> {os.path.relpath(os.path.join(args.run_dir, 'summary.md'))}")
|
|
170
|
+
print(f"review -> {pdf}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def cmd_doctor(args) -> None:
|
|
174
|
+
"""Connectivity check: interactive too; runs no tasks, stores nothing."""
|
|
175
|
+
provider = _prompt_provider(args)
|
|
176
|
+
models = _list_models(provider["base_url"], provider["api_key"])
|
|
177
|
+
print(f"\nendpoint : {provider['base_url']}")
|
|
178
|
+
print(f"model : {provider['model']}")
|
|
179
|
+
if models:
|
|
180
|
+
found = provider["model"] in models
|
|
181
|
+
print(f"reachable: YES, {len(models)} models; "
|
|
182
|
+
f"model {'found' if found else 'NOT in list (may still work)'}")
|
|
183
|
+
else:
|
|
184
|
+
print("reachable: /models unavailable (may still work for chat)")
|
|
185
|
+
|
|
186
|
+
run_cfg = _load_run_cfg(args.config)
|
|
187
|
+
tasks = build_tasks(run_cfg.get("tasks_data_root", DEFAULT_TASKS_ROOT),
|
|
188
|
+
run_cfg, artifacts_root="/tmp/wl-bench-doctor")
|
|
189
|
+
print(f"tasks : {len(tasks)} loaded "
|
|
190
|
+
f"({', '.join(sorted({t.task_type for t in tasks}))})")
|
|
191
|
+
print("review : essay/svg are graded by humans "
|
|
192
|
+
"(artifacts under results/<run>/artifacts/)")
|
|
193
|
+
print(f"\nverdict: {'READY' if models else 'READY (unverified)'} "
|
|
194
|
+
f"— run `wlb run` to start")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def main(argv=None) -> None:
|
|
198
|
+
p = argparse.ArgumentParser(
|
|
199
|
+
prog="wlb",
|
|
200
|
+
description=f"{BRAND} — one provider, one model, "
|
|
201
|
+
"interactive target input")
|
|
202
|
+
p.add_argument("--config", default=DEFAULT_CONFIG,
|
|
203
|
+
help="optional run-parameter JSON "
|
|
204
|
+
"(default config/bench.json; may not exist)")
|
|
205
|
+
p.add_argument("-V", "--version", action="version",
|
|
206
|
+
version=f"{BRAND} {VERSION} (wl-benchmark)")
|
|
207
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
208
|
+
|
|
209
|
+
r = sub.add_parser("run",
|
|
210
|
+
help="ask for endpoint/key/model, then run all tasks")
|
|
211
|
+
r.add_argument("--endpoint", help="skip prompt: OpenAI-compatible base_url")
|
|
212
|
+
r.add_argument("--key", help="skip prompt: API key")
|
|
213
|
+
r.add_argument("--model", help="skip prompt: model name")
|
|
214
|
+
r.add_argument("--tasks", help=f"comma list of {TASK_TYPES}")
|
|
215
|
+
r.add_argument("--out", default="results")
|
|
216
|
+
r.add_argument("--no-upload", action="store_true",
|
|
217
|
+
help="do not upload to the benchmark site")
|
|
218
|
+
r.add_argument("--keep", action="store_true",
|
|
219
|
+
help="keep local run data even after a successful upload")
|
|
220
|
+
r.set_defaults(fn=cmd_run)
|
|
221
|
+
|
|
222
|
+
l = sub.add_parser("list-tasks", help="list discovered tasks")
|
|
223
|
+
l.set_defaults(fn=cmd_list_tasks)
|
|
224
|
+
|
|
225
|
+
pb = sub.add_parser("publish",
|
|
226
|
+
help="upload a run dir to the benchmark site "
|
|
227
|
+
"(then delete it locally)")
|
|
228
|
+
pb.add_argument("run_dir")
|
|
229
|
+
pb.add_argument("--keep", action="store_true",
|
|
230
|
+
help="keep the local run dir after upload")
|
|
231
|
+
pb.set_defaults(fn=cmd_publish)
|
|
232
|
+
|
|
233
|
+
rp = sub.add_parser("report",
|
|
234
|
+
help="rebuild summary.md + review.pdf for a run dir")
|
|
235
|
+
rp.add_argument("run_dir")
|
|
236
|
+
rp.set_defaults(fn=cmd_report)
|
|
237
|
+
|
|
238
|
+
d = sub.add_parser("doctor",
|
|
239
|
+
help="connectivity check (interactive, runs nothing)")
|
|
240
|
+
d.add_argument("--endpoint")
|
|
241
|
+
d.add_argument("--key")
|
|
242
|
+
d.add_argument("--model")
|
|
243
|
+
d.set_defaults(fn=cmd_doctor)
|
|
244
|
+
|
|
245
|
+
args = p.parse_args(argv)
|
|
246
|
+
args.fn(args)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
main()
|
wl_benchmark/client.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""OpenAI-compatible chat client (stdlib only, no external deps).
|
|
2
|
+
|
|
3
|
+
Supports: tool calling, multimodal content (text / image_url / file),
|
|
4
|
+
retry with backoff, latency & usage recording.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.request
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ChatResult:
|
|
19
|
+
content: Optional[str] = None
|
|
20
|
+
tool_calls: List[Dict[str, Any]] = field(default_factory=list)
|
|
21
|
+
finish_reason: Optional[str] = None
|
|
22
|
+
usage: Dict[str, Any] = field(default_factory=dict)
|
|
23
|
+
latency: float = 0.0
|
|
24
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
error: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def ok(self) -> bool:
|
|
29
|
+
return self.error is None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ChatClient:
|
|
33
|
+
"""Minimal OpenAI-compatible /v1/chat/completions client.
|
|
34
|
+
|
|
35
|
+
proxy: "direct" (default; bypass system/env proxies — typical endpoints
|
|
36
|
+
here are reachable directly)
|
|
37
|
+
| "system" (follow system/env proxies) | "http://host:port"
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
RETRYABLE = (429, 500, 502, 503, 504)
|
|
41
|
+
|
|
42
|
+
def __init__(self, base_url: str, api_key: str, timeout: int = 180,
|
|
43
|
+
max_retries: int = 3, proxy: str = "direct"):
|
|
44
|
+
self.base_url = base_url.rstrip("/")
|
|
45
|
+
self.api_key = api_key
|
|
46
|
+
self.timeout = timeout
|
|
47
|
+
self.max_retries = max_retries
|
|
48
|
+
self.proxy = proxy
|
|
49
|
+
if proxy == "direct":
|
|
50
|
+
# bypass macOS system proxy / env-var proxies (urllib reads them by default)
|
|
51
|
+
self._opener = urllib.request.build_opener(
|
|
52
|
+
urllib.request.ProxyHandler({}))
|
|
53
|
+
elif proxy == "system":
|
|
54
|
+
self._opener = urllib.request.build_opener()
|
|
55
|
+
else:
|
|
56
|
+
self._opener = urllib.request.build_opener(
|
|
57
|
+
urllib.request.ProxyHandler({"http": proxy, "https": proxy}))
|
|
58
|
+
|
|
59
|
+
# -------------------------------------------------------------- helpers
|
|
60
|
+
@staticmethod
|
|
61
|
+
def image_part(path_or_dataurl: str) -> Dict[str, Any]:
|
|
62
|
+
"""Build an image_url content part from a local file or data URL."""
|
|
63
|
+
if path_or_dataurl.startswith(("http://", "https://", "data:")):
|
|
64
|
+
url = path_or_dataurl
|
|
65
|
+
else:
|
|
66
|
+
with open(path_or_dataurl, "rb") as f:
|
|
67
|
+
b64 = base64.b64encode(f.read()).decode()
|
|
68
|
+
mime = "image/png" if path_or_dataurl.endswith(".png") else "image/jpeg"
|
|
69
|
+
url = f"data:{mime};base64,{b64}"
|
|
70
|
+
return {"type": "image_url", "image_url": {"url": url}}
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def file_part(path: str) -> Dict[str, Any]:
|
|
74
|
+
"""Build a file content part (pdf etc.) as base64 data URL."""
|
|
75
|
+
import mimetypes
|
|
76
|
+
mime = mimetypes.guess_type(path)[0] or "application/octet-stream"
|
|
77
|
+
with open(path, "rb") as f:
|
|
78
|
+
b64 = base64.b64encode(f.read()).decode()
|
|
79
|
+
return {"type": "file", "file": {
|
|
80
|
+
"filename": path.split("/")[-1],
|
|
81
|
+
"file_data": f"data:{mime};base64,{b64}",
|
|
82
|
+
}}
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------ api
|
|
85
|
+
def chat(self, model: str, messages: List[Dict[str, Any]],
|
|
86
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
87
|
+
tool_choice: str = "auto",
|
|
88
|
+
max_tokens: Optional[int] = None,
|
|
89
|
+
temperature: Optional[float] = None,
|
|
90
|
+
response_format: Optional[Dict[str, Any]] = None) -> ChatResult:
|
|
91
|
+
body: Dict[str, Any] = {"model": model, "messages": messages}
|
|
92
|
+
if tools:
|
|
93
|
+
body["tools"] = tools
|
|
94
|
+
body["tool_choice"] = tool_choice
|
|
95
|
+
if max_tokens:
|
|
96
|
+
body["max_tokens"] = max_tokens
|
|
97
|
+
if temperature is not None:
|
|
98
|
+
body["temperature"] = temperature
|
|
99
|
+
if response_format:
|
|
100
|
+
body["response_format"] = response_format
|
|
101
|
+
|
|
102
|
+
payload = json.dumps(body).encode()
|
|
103
|
+
last_err: Optional[str] = None
|
|
104
|
+
|
|
105
|
+
for attempt in range(self.max_retries):
|
|
106
|
+
t0 = time.time()
|
|
107
|
+
req = urllib.request.Request(
|
|
108
|
+
f"{self.base_url}/chat/completions", data=payload,
|
|
109
|
+
headers={"Authorization": f"Bearer {self.api_key}",
|
|
110
|
+
"Content-Type": "application/json"})
|
|
111
|
+
try:
|
|
112
|
+
with self._opener.open(req, timeout=self.timeout) as r:
|
|
113
|
+
data = json.loads(r.read().decode())
|
|
114
|
+
return self._parse(data, time.time() - t0)
|
|
115
|
+
except urllib.error.HTTPError as e:
|
|
116
|
+
detail = ""
|
|
117
|
+
try:
|
|
118
|
+
detail = e.read().decode()[:300]
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
last_err = f"HTTP {e.code}: {detail}"
|
|
122
|
+
if e.code not in self.RETRYABLE:
|
|
123
|
+
break
|
|
124
|
+
except Exception as e: # noqa: BLE001
|
|
125
|
+
last_err = f"{type(e).__name__}: {e}"
|
|
126
|
+
time.sleep(2 * (attempt + 1))
|
|
127
|
+
|
|
128
|
+
return ChatResult(error=last_err, latency=time.time() - t0)
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _parse(data: Dict[str, Any], latency: float) -> ChatResult:
|
|
132
|
+
if "choices" not in data:
|
|
133
|
+
return ChatResult(error=json.dumps(data, ensure_ascii=False)[:500],
|
|
134
|
+
latency=latency, raw=data)
|
|
135
|
+
choice = data["choices"][0]
|
|
136
|
+
msg = choice.get("message", {})
|
|
137
|
+
return ChatResult(
|
|
138
|
+
content=msg.get("content"),
|
|
139
|
+
tool_calls=msg.get("tool_calls") or [],
|
|
140
|
+
finish_reason=choice.get("finish_reason"),
|
|
141
|
+
usage=data.get("usage", {}),
|
|
142
|
+
latency=latency,
|
|
143
|
+
raw=data,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def parse_judge_json(text: str) -> Optional[Dict[str, Any]]:
|
|
148
|
+
"""Extract a JSON object from a judge reply (tolerates code fences)."""
|
|
149
|
+
if not text:
|
|
150
|
+
return None
|
|
151
|
+
text = text.strip()
|
|
152
|
+
if "```" in text:
|
|
153
|
+
for seg in text.split("```"):
|
|
154
|
+
seg = seg.strip()
|
|
155
|
+
if seg.startswith("json"):
|
|
156
|
+
seg = seg[4:].strip()
|
|
157
|
+
if seg.startswith("{"):
|
|
158
|
+
text = seg
|
|
159
|
+
break
|
|
160
|
+
start, end = text.find("{"), text.rfind("}")
|
|
161
|
+
if start == -1 or end <= start:
|
|
162
|
+
return None
|
|
163
|
+
try:
|
|
164
|
+
return json.loads(text[start:end + 1])
|
|
165
|
+
except json.JSONDecodeError:
|
|
166
|
+
return None
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Publisher: upload a finished run to the WL-Benchmark platform
|
|
2
|
+
(a Cloudflare Worker, see site/), then wipe the local run directory.
|
|
3
|
+
|
|
4
|
+
Default flow of `wlb run`:
|
|
5
|
+
1. build one self-contained page for the new run (images inlined);
|
|
6
|
+
2. POST it to <site>/api/runs (Bearer upload token);
|
|
7
|
+
3. print the share link <site>/r/<run-id>;
|
|
8
|
+
4. delete the local run directory — the platform is the single
|
|
9
|
+
source of truth.
|
|
10
|
+
|
|
11
|
+
Configuration (config/site.json, gitignored; env vars win):
|
|
12
|
+
WL_BENCH_URL e.g. https://benchmark.wulei.org
|
|
13
|
+
WL_BENCH_TOKEN the platform's UPLOAD_TOKEN
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import shutil
|
|
21
|
+
import urllib.request
|
|
22
|
+
|
|
23
|
+
from .site import build_run_page, manifest_entry
|
|
24
|
+
|
|
25
|
+
SITE_CONFIG = os.path.join("config", "site.json")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --------------------------------------------------------------- config
|
|
29
|
+
def load_site_config() -> dict | None:
|
|
30
|
+
cfg = {}
|
|
31
|
+
if os.path.exists(SITE_CONFIG):
|
|
32
|
+
try:
|
|
33
|
+
with open(SITE_CONFIG, encoding="utf-8") as f:
|
|
34
|
+
cfg = json.load(f)
|
|
35
|
+
except json.JSONDecodeError:
|
|
36
|
+
cfg = {}
|
|
37
|
+
cfg["url"] = os.environ.get("WL_BENCH_URL", cfg.get("url", "")).rstrip("/")
|
|
38
|
+
cfg["token"] = os.environ.get("WL_BENCH_TOKEN", cfg.get("token", ""))
|
|
39
|
+
if cfg["url"] and cfg["url"].startswith(("http://", "https://")):
|
|
40
|
+
return cfg
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def prompt_site_config() -> dict | None:
|
|
45
|
+
"""Interactive first-time setup; returns None if the user declines."""
|
|
46
|
+
print("\n[publish] the benchmark platform is not configured yet.")
|
|
47
|
+
print(" every run is uploaded to your own site and the local")
|
|
48
|
+
print(" data is deleted afterwards.")
|
|
49
|
+
ans = input("Configure it now? [Y/n] ").strip().lower()
|
|
50
|
+
if ans in ("n", "no"):
|
|
51
|
+
return None
|
|
52
|
+
url = ""
|
|
53
|
+
while not url.startswith(("http://", "https://")):
|
|
54
|
+
url = input("Platform URL (e.g. https://benchmark.wulei.org): "
|
|
55
|
+
).strip().rstrip("/")
|
|
56
|
+
token = input("Upload token (UPLOAD_TOKEN secret of the worker): ").strip()
|
|
57
|
+
if not token:
|
|
58
|
+
print("[publish] empty token — upload will likely be rejected (401)")
|
|
59
|
+
cfg = {"url": url, "token": token}
|
|
60
|
+
os.makedirs(os.path.dirname(SITE_CONFIG), exist_ok=True)
|
|
61
|
+
with open(SITE_CONFIG, "w", encoding="utf-8") as f:
|
|
62
|
+
json.dump(cfg, f, indent=1)
|
|
63
|
+
print(f"[publish] saved -> {SITE_CONFIG} (gitignored)")
|
|
64
|
+
return cfg
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# -------------------------------------------------------------- publish
|
|
68
|
+
def publish_run(run_dir: str, cfg: dict) -> str:
|
|
69
|
+
"""Upload one run; returns the share link."""
|
|
70
|
+
run_id = os.path.basename(run_dir.rstrip("/"))
|
|
71
|
+
with open(os.path.join(run_dir, "results.json"), encoding="utf-8") as f:
|
|
72
|
+
results = json.load(f)
|
|
73
|
+
|
|
74
|
+
page = build_run_page(results, run_id, run_dir)
|
|
75
|
+
payload = json.dumps({
|
|
76
|
+
"id": run_id,
|
|
77
|
+
"meta": manifest_entry(results, run_id),
|
|
78
|
+
"html": page,
|
|
79
|
+
}, ensure_ascii=False).encode()
|
|
80
|
+
|
|
81
|
+
req = urllib.request.Request(
|
|
82
|
+
cfg["url"] + "/api/runs", data=payload, method="POST",
|
|
83
|
+
headers={"Content-Type": "application/json; charset=utf-8",
|
|
84
|
+
"Authorization": f"Bearer {cfg['token']}",
|
|
85
|
+
# a custom UA: Cloudflare's Browser Integrity Check (error
|
|
86
|
+
# 1010) blocks the default Python-urllib signature
|
|
87
|
+
"User-Agent": "wl-benchmark/0.4"})
|
|
88
|
+
try:
|
|
89
|
+
with urllib.request.urlopen(req, timeout=120) as r:
|
|
90
|
+
resp = json.load(r)
|
|
91
|
+
except urllib.error.HTTPError as e:
|
|
92
|
+
detail = e.read().decode(errors="replace")[:200]
|
|
93
|
+
raise RuntimeError(f"upload rejected: HTTP {e.code} — {detail}") from e
|
|
94
|
+
if not resp.get("ok"):
|
|
95
|
+
raise RuntimeError(f"platform refused the run: {resp}")
|
|
96
|
+
return f"{cfg['url']}/r/{run_id}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# --------------------------------------------------------------- cleanup
|
|
100
|
+
def cleanup_run(run_dir: str) -> bool:
|
|
101
|
+
"""Delete a run directory (results/<YYYYMMDD-HHMMSS>) after a
|
|
102
|
+
successful upload. Refuses anything that does not look like a run."""
|
|
103
|
+
name = os.path.basename(run_dir.rstrip("/"))
|
|
104
|
+
parent = os.path.basename(os.path.dirname(run_dir.rstrip("/")))
|
|
105
|
+
if parent != "results" or not re.fullmatch(r"\d{8}-\d{6}", name):
|
|
106
|
+
print(f"[cleanup] refusing to delete {run_dir!r} — not a run dir")
|
|
107
|
+
return False
|
|
108
|
+
shutil.rmtree(run_dir, ignore_errors=True)
|
|
109
|
+
try:
|
|
110
|
+
os.rmdir(os.path.dirname(run_dir)) # drop empty results root
|
|
111
|
+
except OSError:
|
|
112
|
+
pass
|
|
113
|
+
return True
|
wl_benchmark/reporter.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Markdown reporter: aggregate results.json into a readable summary.
|
|
2
|
+
|
|
3
|
+
essay / svg use human review: the score column shows "pending human review"
|
|
4
|
+
and lists artifact paths for the reviewer.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def summarize(results_path: str) -> str:
|
|
14
|
+
with open(results_path, encoding="utf-8") as f:
|
|
15
|
+
results = json.load(f)
|
|
16
|
+
|
|
17
|
+
lines = ["# WL-Benchmark Run Summary", ""]
|
|
18
|
+
|
|
19
|
+
by_model = defaultdict(list)
|
|
20
|
+
for r in results:
|
|
21
|
+
by_model[(r["provider"], r["model"])].append(r)
|
|
22
|
+
|
|
23
|
+
for (prov, model), rs in sorted(by_model.items()):
|
|
24
|
+
lines += [f"## {prov} / {model}", "",
|
|
25
|
+
"| task | type | score | latency | artifacts | error |",
|
|
26
|
+
"|---|---|---|---|---|---|"]
|
|
27
|
+
for r in rs:
|
|
28
|
+
if r["error"]:
|
|
29
|
+
score = "ERR"
|
|
30
|
+
elif r["score"] is None:
|
|
31
|
+
score = "pending human review"
|
|
32
|
+
else:
|
|
33
|
+
score = f"{r['score']:.2f}"
|
|
34
|
+
lat = f"{r['latency']}s" if r.get("latency") else "-"
|
|
35
|
+
arts = "; ".join(os.path.relpath(a) for a in r.get("artifacts", []))
|
|
36
|
+
arts = arts.replace("|", "\\|") or "-"
|
|
37
|
+
err = (r["error"] or "")[:60].replace("|", "\\|")
|
|
38
|
+
lines.append(f"| {r['task_id']} | {r['task_type']} | {score} "
|
|
39
|
+
f"| {lat} | {arts} | {err} |")
|
|
40
|
+
|
|
41
|
+
scored = [r["score"] for r in rs
|
|
42
|
+
if not r["error"] and r["score"] is not None]
|
|
43
|
+
manual = [r for r in rs if not r["error"] and r["score"] is None]
|
|
44
|
+
if scored:
|
|
45
|
+
avg = sum(scored) / len(scored)
|
|
46
|
+
lines += ["", f"**Auto-scored average: {avg:.3f} "
|
|
47
|
+
f"({len(scored)} auto + {len(manual)} pending human "
|
|
48
|
+
f"review)**"]
|
|
49
|
+
elif manual:
|
|
50
|
+
lines += ["", f"**All {len(manual)} tasks pending human review**"]
|
|
51
|
+
lines.append("")
|
|
52
|
+
|
|
53
|
+
return "\n".join(lines)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def write_report(run_dir: str) -> str:
|
|
57
|
+
src = os.path.join(run_dir, "results.json")
|
|
58
|
+
dst = os.path.join(run_dir, "summary.md")
|
|
59
|
+
with open(dst, "w", encoding="utf-8") as f:
|
|
60
|
+
f.write(summarize(src))
|
|
61
|
+
return dst
|