bench2agent 0.3.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.
- bench2agent/__init__.py +3 -0
- bench2agent/__main__.py +3 -0
- bench2agent/cli.py +515 -0
- bench2agent/core/__init__.py +1 -0
- bench2agent/core/__main__.py +3 -0
- bench2agent/core/benchmarks.py +562 -0
- bench2agent/core/chat.py +632 -0
- bench2agent/core/cli.py +454 -0
- bench2agent/core/datasets.py +220 -0
- bench2agent/core/export.py +248 -0
- bench2agent/core/figures.py +253 -0
- bench2agent/core/mcp.py +1194 -0
- bench2agent/core/paths.py +30 -0
- bench2agent/core/policy.py +17 -0
- bench2agent/core/server.py +326 -0
- bench2agent/core/translate.py +149 -0
- bench2agent/core/verify.py +87 -0
- bench2agent/data.py +123 -0
- bench2agent/migration.py +58 -0
- bench2agent/providers.py +142 -0
- bench2agent/session.py +97 -0
- bench2agent-0.3.0.dist-info/METADATA +184 -0
- bench2agent-0.3.0.dist-info/RECORD +27 -0
- bench2agent-0.3.0.dist-info/WHEEL +5 -0
- bench2agent-0.3.0.dist-info/entry_points.txt +2 -0
- bench2agent-0.3.0.dist-info/licenses/LICENSE +202 -0
- bench2agent-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
"""Portable full-text benchmark statistics and evidence; standard library only.
|
|
2
|
+
|
|
3
|
+
Build a snapshot AFTER the mention pass and reviewed identities have settled.
|
|
4
|
+
Every parsed paper is retained, including papers with no stated role. Runtime
|
|
5
|
+
queries never substitute the abstract inventory for missing full-text data.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import collections
|
|
10
|
+
import datetime as dt
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import math
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import unicodedata
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from .paths import ROOT
|
|
20
|
+
SNAPSHOT = "benchmark_snapshot.json"
|
|
21
|
+
ROLES = ("evaluates_on", "trains_on")
|
|
22
|
+
# Where a registered benchmark lives is OUR mapping (config/benchmarks.json):
|
|
23
|
+
# a Hub dataset id or a GitHub repo, shown only when it answered the API at
|
|
24
|
+
# the registry's last check, or an author-registered homepage, never checked.
|
|
25
|
+
HOSTS = (("hf", "huggingface", "https://huggingface.co/datasets/"),
|
|
26
|
+
("gh", "github", "https://github.com/"))
|
|
27
|
+
LINK_NOTE = ("locations: our mapping from this benchmark id to where the artifact lives. "
|
|
28
|
+
"check=api: the Hub/GitHub id answered the API when the registry was last checked "
|
|
29
|
+
"(checked_at null means the date is not recorded in this snapshot); check=none: an "
|
|
30
|
+
"author-registered homepage. An empty list is no confirmed location, not none. "
|
|
31
|
+
"introducing_paper: the reviewed introducing paper in this corpus, null when there is "
|
|
32
|
+
"none; it is a paper, not a download location. Neither ranks or filters anything.")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def locations(entry: dict, checked_at: str | None = None) -> list[dict]:
|
|
36
|
+
ok = entry.get("ok") or {}
|
|
37
|
+
out = [{"url": prefix + entry[field], "host": host, "check": "api", "checked_at": checked_at}
|
|
38
|
+
for field, host, prefix in HOSTS if entry.get(field) and ok.get(field)]
|
|
39
|
+
if entry.get("url"):
|
|
40
|
+
out.append({"url": entry["url"], "host": "web", "check": "none", "checked_at": None})
|
|
41
|
+
return out
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def introducing_paper(entry: dict) -> dict | None:
|
|
45
|
+
claims = entry.get("claims") or []
|
|
46
|
+
if not claims:
|
|
47
|
+
return None
|
|
48
|
+
c = next((c for c in claims if c["edition"] == entry.get("first_claim")), claims[0])
|
|
49
|
+
return {"paper_id": c["arxiv_base"], "url": "https://arxiv.org/abs/" + c["arxiv_base"],
|
|
50
|
+
"edition": c["edition"], "title": c["title"]}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def fold(s: str) -> str:
|
|
54
|
+
s = s.lower().translate(str.maketrans({"τ": "tau", "∞": "inf", "²": "2", "³": "3"}))
|
|
55
|
+
s = re.sub(r"^(the|a)\s+", "", s)
|
|
56
|
+
s = re.sub(r"\s+(benchmark|dataset|corpus|suite)s?$", "", s)
|
|
57
|
+
return re.sub(r"[^a-z0-9+]", "", s)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def person(s: str) -> str:
|
|
61
|
+
s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode().lower()
|
|
62
|
+
return re.sub(r"[^a-z ]", "", s).strip()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def read_json(path: Path, default=None):
|
|
66
|
+
return json.loads(path.read_text(encoding="utf-8")) if path.exists() else default
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def rows(path: Path):
|
|
70
|
+
with path.open(encoding="utf-8") as fh:
|
|
71
|
+
for line in fh:
|
|
72
|
+
if line.strip():
|
|
73
|
+
yield json.loads(line)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def catalogue(root: Path) -> list[dict]:
|
|
77
|
+
reg = read_json(root / "config/benchmarks.json", {"benchmarks": []})
|
|
78
|
+
out = {e["id"]: dict(e) for e in reg["benchmarks"]}
|
|
79
|
+
intro = read_json(root / "config/benchmarks_introduced.json", {})
|
|
80
|
+
# Only the reviewed schema is eligible: old title-gated outputs are not a
|
|
81
|
+
# substitute for claim-by-claim review.
|
|
82
|
+
for e in intro.get("benchmarks", []):
|
|
83
|
+
if "developers" in e and e.get("claims"):
|
|
84
|
+
out[e["id"]] = {**out.get(e["id"], {}), **e}
|
|
85
|
+
# Two extraction claims in one introducing paper can name the same
|
|
86
|
+
# product. They are duplicates, not homonyms (e.g. LIBERO's expanded name).
|
|
87
|
+
# Preserve all claim sentences and remap IDs only in the serving snapshot.
|
|
88
|
+
parent = {bid: bid for bid in out}
|
|
89
|
+
|
|
90
|
+
def find(bid):
|
|
91
|
+
while parent[bid] != bid:
|
|
92
|
+
parent[bid] = parent[parent[bid]]
|
|
93
|
+
bid = parent[bid]
|
|
94
|
+
return bid
|
|
95
|
+
|
|
96
|
+
known = {}
|
|
97
|
+
for e in out.values():
|
|
98
|
+
for c in e.get("claims", []):
|
|
99
|
+
key = (fold(e["name"]), e.get("kind"), c["arxiv_base"])
|
|
100
|
+
if key in known:
|
|
101
|
+
parent[find(e["id"])] = find(known[key])
|
|
102
|
+
known[key] = e["id"]
|
|
103
|
+
groups = collections.defaultdict(list)
|
|
104
|
+
for e in out.values():
|
|
105
|
+
groups[find(e["id"])].append(e)
|
|
106
|
+
merged, remap = [], {}
|
|
107
|
+
for group in groups.values():
|
|
108
|
+
group.sort(key=lambda e: (not bool(e.get("registered")), len(e["id"]), e["id"]))
|
|
109
|
+
entry = dict(group[0])
|
|
110
|
+
ids = {e["id"] for e in group}
|
|
111
|
+
for bid in ids:
|
|
112
|
+
remap[bid] = entry["id"]
|
|
113
|
+
if len(group) > 1:
|
|
114
|
+
entry["merged_ids"] = sorted(ids - {entry["id"]})
|
|
115
|
+
claims = {c.get("claim_id", c["arxiv_base"] + c["evidence"]): c
|
|
116
|
+
for e in group for c in e.get("claims", [])}
|
|
117
|
+
entry["claims"] = sorted(claims.values(), key=lambda c: (c["edition"].rsplit("-", 1)[1], c["edition"], c["arxiv_base"]))
|
|
118
|
+
entry["first_claim"] = entry["claims"][0]["edition"]
|
|
119
|
+
entry["developers"] = sorted({a for e in group for a in e.get("developers", [])})
|
|
120
|
+
entry["aliases"] = sorted({a for e in group for a in e.get("aliases", [])})
|
|
121
|
+
entry["homonym_of"] = sorted({bid for e in group for bid in e.get("homonym_of") or []} - ids) or None
|
|
122
|
+
merged.append(entry)
|
|
123
|
+
for e in merged:
|
|
124
|
+
if e.get("homonym_of"):
|
|
125
|
+
e["homonym_of"] = sorted({remap.get(bid, bid) for bid in e["homonym_of"]} - {e["id"]}) or None
|
|
126
|
+
out = {e["id"]: e for e in merged}
|
|
127
|
+
return list(out.values())
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _files(root: Path, edition: str) -> tuple[Path, Path]:
|
|
131
|
+
venue, year = edition.rsplit("-", 1)
|
|
132
|
+
stem = "papers" if edition == "icml-2026" else (
|
|
133
|
+
f"papers_{year}" if venue == "icml" else f"papers_{venue}_{year}")
|
|
134
|
+
resolved = "resolved" if edition == "icml-2026" else f"resolved_{edition}"
|
|
135
|
+
return root / "data/processed" / (stem + ".jsonl"), root / "data/raw/arxiv" / (resolved + ".jsonl")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _cited(title: str, refs: list[str]) -> bool:
|
|
139
|
+
# Matching titles, not names, distinguishes homonyms. No global reference
|
|
140
|
+
# list is used: refs must come from the sentence asserting THIS role.
|
|
141
|
+
key = fold(title)
|
|
142
|
+
return len(key) >= 12 and any(key in fold(r) for r in refs)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def build_snapshot(root: Path = ROOT, out: Path | None = None) -> dict:
|
|
146
|
+
files = sorted((root / "data/interim/mentions").glob("mentions_*.jsonl"))
|
|
147
|
+
if not files:
|
|
148
|
+
raise FileNotFoundError("No full-text mention outputs. Finish icml.bench_mentions --all first.")
|
|
149
|
+
dependencies = files + [root / "config/benchmarks.json", root / "config/benchmarks_introduced.json"]
|
|
150
|
+
stamps = {}
|
|
151
|
+
|
|
152
|
+
def remember(paths):
|
|
153
|
+
for p in paths:
|
|
154
|
+
if p.exists():
|
|
155
|
+
stamps[str(p.relative_to(root))] = (p.stat().st_mtime_ns, p.stat().st_size)
|
|
156
|
+
|
|
157
|
+
remember(dependencies)
|
|
158
|
+
metadata = {}
|
|
159
|
+
topics = {}
|
|
160
|
+
editions = {}
|
|
161
|
+
for file in files:
|
|
162
|
+
ed = file.stem.removeprefix("mentions_")
|
|
163
|
+
if not re.fullmatch(r"[a-z]+-\d{4}", ed):
|
|
164
|
+
raise ValueError(f"Invalid edition: {ed}")
|
|
165
|
+
papers_file, resolved = _files(root, ed)
|
|
166
|
+
if not papers_file.exists() or not resolved.exists():
|
|
167
|
+
raise FileNotFoundError(f"Paper metadata and arXiv mapping required for {ed}")
|
|
168
|
+
dependencies += [papers_file, resolved]
|
|
169
|
+
remember([papers_file, resolved])
|
|
170
|
+
papers = {p["event_id"]: p for p in rows(papers_file)}
|
|
171
|
+
tp = root / f"data/processed/topics_{ed}.json"
|
|
172
|
+
if tp.exists():
|
|
173
|
+
dependencies.append(tp)
|
|
174
|
+
remember([tp])
|
|
175
|
+
for t in read_json(tp, {}).get("topics", []):
|
|
176
|
+
for eid in set(t.get("explicit", []) + t.get("via_child", [])):
|
|
177
|
+
topics.setdefault((ed, eid), []).append(t["label"])
|
|
178
|
+
matched = set()
|
|
179
|
+
for r in rows(resolved):
|
|
180
|
+
base = r.get("arxiv_base")
|
|
181
|
+
p = papers.get(r.get("event_id"))
|
|
182
|
+
if base and p:
|
|
183
|
+
matched.add(base)
|
|
184
|
+
metadata[(ed, base)] = {"title": p["title"], "authors": p.get("authors") or [],
|
|
185
|
+
"fields": sorted(set(topics.get((ed, p["event_id"]), []) +
|
|
186
|
+
[s for s in (p.get("area"), p.get("subarea")) if s]))}
|
|
187
|
+
editions[ed] = {"total_papers": len(papers), "arxiv_matched": len(matched)}
|
|
188
|
+
# Read source stamps before the expensive scan; reject a changing source
|
|
189
|
+
# rather than publishing a mixture of two extraction revisions.
|
|
190
|
+
entries = catalogue(root)
|
|
191
|
+
by_key = collections.defaultdict(dict)
|
|
192
|
+
by_fold = collections.defaultdict(dict)
|
|
193
|
+
for e in entries:
|
|
194
|
+
for name in [e["name"], *e.get("aliases", [])]:
|
|
195
|
+
by_fold[fold(name)][e["id"]] = e
|
|
196
|
+
key = e.get("registered") or e.get("homonym_of_registered") or (
|
|
197
|
+
"hom:" + e.get("fold", fold(e["name"])) if e.get("homonym_of") else e["id"])
|
|
198
|
+
by_key[key][e["id"]] = e
|
|
199
|
+
for old_id in e.get("merged_ids", []):
|
|
200
|
+
by_key[old_id][e["id"]] = e
|
|
201
|
+
# Registered products and their homonyms must both be candidates even if
|
|
202
|
+
# only one has an introducing paper inside the observation window.
|
|
203
|
+
for key, candidates in by_key.items():
|
|
204
|
+
for e in list(candidates.values()):
|
|
205
|
+
for other in by_fold[fold(e["name"])].values():
|
|
206
|
+
candidates[other["id"]] = other
|
|
207
|
+
result = []
|
|
208
|
+
for file in files:
|
|
209
|
+
ed = file.stem.removeprefix("mentions_")
|
|
210
|
+
seen = set()
|
|
211
|
+
for r in rows(file):
|
|
212
|
+
base = r["arxiv_base"]
|
|
213
|
+
if base in seen:
|
|
214
|
+
raise ValueError(f"Duplicate paper {ed}/{base}")
|
|
215
|
+
seen.add(base)
|
|
216
|
+
meta = metadata.get((ed, base))
|
|
217
|
+
if meta is None:
|
|
218
|
+
raise ValueError(f"Missing paper metadata: {ed}/{base}")
|
|
219
|
+
uses, unresolved = {}, []
|
|
220
|
+
for key, m in r.get("mentions", {}).items():
|
|
221
|
+
candidates = list(by_key.get(key, {}).values())
|
|
222
|
+
if not candidates:
|
|
223
|
+
# Unreviewed provisional vocabulary is evidence of a
|
|
224
|
+
# name, not an established entity to rank.
|
|
225
|
+
if m.get("roles"):
|
|
226
|
+
unresolved.append({"key": key, "name": m.get("as_written"), "reason": "unreviewed"})
|
|
227
|
+
continue
|
|
228
|
+
for role, evidence in m.get("roles", {}).items():
|
|
229
|
+
if role not in ROLES:
|
|
230
|
+
continue
|
|
231
|
+
selected = candidates
|
|
232
|
+
if len(candidates) > 1:
|
|
233
|
+
refs = m.get("role_cites", [])
|
|
234
|
+
if isinstance(refs, dict):
|
|
235
|
+
refs = refs.get(role, [])
|
|
236
|
+
selected = [e for e in candidates if any(_cited(c["title"], refs)
|
|
237
|
+
for c in e.get("claims", []))]
|
|
238
|
+
if len(selected) != 1:
|
|
239
|
+
unresolved.append({"key": key, "role": role, "reason": "homonym",
|
|
240
|
+
"candidates": [e["id"] for e in candidates]})
|
|
241
|
+
continue
|
|
242
|
+
e = selected[0]
|
|
243
|
+
use = uses.setdefault(e["id"], {"surfaces": {}, "roles": {}})
|
|
244
|
+
use["surfaces"].update(m.get("surfaces") or {m.get("as_written", e["name"]): 1})
|
|
245
|
+
use["roles"].setdefault(role, []).extend(evidence)
|
|
246
|
+
result.append({"paper_id": base, "edition": ed, **meta, "uses": uses,
|
|
247
|
+
"unattributed": unresolved})
|
|
248
|
+
editions[ed]["parsed_papers"] = len(seen)
|
|
249
|
+
editions[ed]["field_labelled_papers"] = sum(bool(p["fields"]) for p in result if p["edition"] == ed)
|
|
250
|
+
for name, stamp in stamps.items():
|
|
251
|
+
p = root / name
|
|
252
|
+
if (p.stat().st_mtime_ns, p.stat().st_size) != stamp:
|
|
253
|
+
raise ValueError(f"Source changed during export: {name}; retry after extraction finishes")
|
|
254
|
+
source_id = hashlib.sha256(json.dumps(stamps, sort_keys=True).encode()).hexdigest()[:16]
|
|
255
|
+
data = {"schema_version": 1, "snapshot_id": source_id,
|
|
256
|
+
"generated_at": dt.datetime.now(dt.timezone.utc).isoformat(),
|
|
257
|
+
"source": "arxiv_html_stated_roles", "editions": editions,
|
|
258
|
+
# the registry's own API-check date, never the export date
|
|
259
|
+
"registry_checked": read_json(root / "config/benchmarks.json", {}).get("checked"),
|
|
260
|
+
"benchmarks": entries, "papers": result}
|
|
261
|
+
target = out or root / "data/processed" / SNAPSHOT
|
|
262
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
temp = target.with_suffix(".tmp")
|
|
264
|
+
temp.write_text(json.dumps(data, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
|
|
265
|
+
os.replace(temp, target)
|
|
266
|
+
return {"path": str(target), "snapshot_id": source_id, "editions": len(editions),
|
|
267
|
+
"parsed_papers": len(result), "benchmarks": len(entries)}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class BenchmarkStore:
|
|
271
|
+
def __init__(self, root: Path = ROOT):
|
|
272
|
+
self.root = root
|
|
273
|
+
self._stamp = None
|
|
274
|
+
self._data = None
|
|
275
|
+
|
|
276
|
+
def data(self) -> dict:
|
|
277
|
+
path = self.root / "data/processed" / SNAPSHOT
|
|
278
|
+
if not path.exists():
|
|
279
|
+
return {"source": "arxiv_html_stated_roles", "available": False,
|
|
280
|
+
"benchmarks": catalogue(self.root), "papers": [], "editions": {}}
|
|
281
|
+
stamp = (path.stat().st_mtime_ns, path.stat().st_size)
|
|
282
|
+
if stamp != self._stamp:
|
|
283
|
+
data = read_json(path)
|
|
284
|
+
if data.get("schema_version") != 1 or data.get("source") != "arxiv_html_stated_roles":
|
|
285
|
+
raise ValueError("Unsupported benchmark snapshot; rebuild with bench2agent benchmarks")
|
|
286
|
+
self._data, self._stamp = {**data, "available": True}, stamp
|
|
287
|
+
return self._data
|
|
288
|
+
|
|
289
|
+
def _base(self, data: dict) -> dict:
|
|
290
|
+
return {k: data.get(k) for k in ("available", "source", "snapshot_id", "generated_at")}
|
|
291
|
+
|
|
292
|
+
def _links(self, data: dict, entry: dict) -> dict:
|
|
293
|
+
"""Keyed by the entry (benchmark id), never by name: a homonym gets its own."""
|
|
294
|
+
return {"locations": locations(entry, data.get("registry_checked")),
|
|
295
|
+
"introducing_paper": introducing_paper(entry)}
|
|
296
|
+
|
|
297
|
+
def _entry(self, data: dict, name: str) -> dict:
|
|
298
|
+
exact = [e for e in data["benchmarks"] if e["id"] == name or name in e.get("merged_ids", [])]
|
|
299
|
+
found = exact or [e for e in data["benchmarks"]
|
|
300
|
+
if fold(name) in {fold(s) for s in [e["name"], *e.get("aliases", [])]}]
|
|
301
|
+
if len(found) != 1:
|
|
302
|
+
raise ValueError("Pass a benchmark ID from benchmark_usage or new_benchmarks. "
|
|
303
|
+
+ json.dumps([{"id": e["id"], "name": e["name"]} for e in found]))
|
|
304
|
+
return found[0]
|
|
305
|
+
|
|
306
|
+
def scope(self, a: dict) -> dict:
|
|
307
|
+
data = self.data()
|
|
308
|
+
fields = collections.Counter(f for p in data["papers"] for f in {s.casefold() for s in p.get("fields", [])})
|
|
309
|
+
query = (a.get("query") or "").casefold()
|
|
310
|
+
return {**self._base(data), "editions": data["editions"],
|
|
311
|
+
"fields": [{"name": f, "parsed_papers": n} for f, n in fields.most_common()
|
|
312
|
+
if query in f.casefold()][:100],
|
|
313
|
+
"catalogue_entries": len(data["benchmarks"]),
|
|
314
|
+
"note": "Fields are exact declared topic/area/subarea labels; their coverage is incomplete. "
|
|
315
|
+
"Usage needs benchmark_snapshot.json; missing data is not zero usage."}
|
|
316
|
+
|
|
317
|
+
def _select(self, data: dict, a: dict, *, latest: bool = False) -> tuple[list, dict]:
|
|
318
|
+
eds = list(data["editions"])
|
|
319
|
+
venue = (a.get("venue") or "").lower()
|
|
320
|
+
if venue:
|
|
321
|
+
eds = [ed for ed in eds if ed.rsplit("-", 1)[0] == venue]
|
|
322
|
+
if a.get("edition"):
|
|
323
|
+
eds = [ed for ed in eds if ed == a["edition"].lower()]
|
|
324
|
+
if a.get("years"):
|
|
325
|
+
eds = [ed for ed in eds if int(ed.rsplit("-", 1)[1]) in a["years"]]
|
|
326
|
+
if latest and not a.get("edition") and not a.get("years"):
|
|
327
|
+
newest = {}
|
|
328
|
+
for ed in eds:
|
|
329
|
+
v = ed.rsplit("-", 1)[0]
|
|
330
|
+
newest[v] = max(newest.get(v, ed), ed)
|
|
331
|
+
eds = list(newest.values())
|
|
332
|
+
if data["available"] and not eds:
|
|
333
|
+
raise ValueError("No analysed editions match the requested scope")
|
|
334
|
+
papers = [p for p in data["papers"] if p["edition"] in eds]
|
|
335
|
+
topic = a.get("topic")
|
|
336
|
+
if topic:
|
|
337
|
+
known = {f.casefold() for p in data["papers"] for f in p.get("fields", [])}
|
|
338
|
+
if topic.casefold() not in known:
|
|
339
|
+
raise ValueError("Unknown field label; call benchmark_scope(query=...) or supply paper_ids")
|
|
340
|
+
papers = [p for p in papers if topic.casefold() in {f.casefold() for f in p.get("fields", [])}]
|
|
341
|
+
if a.get("paper_ids") is not None:
|
|
342
|
+
selected = set(a["paper_ids"])
|
|
343
|
+
papers = [p for p in papers if p["paper_id"] in selected]
|
|
344
|
+
denominators = collections.Counter(p["edition"] for p in papers)
|
|
345
|
+
cov = {ed: {**data["editions"][ed], "in_scope_parsed": denominators[ed]} for ed in sorted(eds)}
|
|
346
|
+
return papers, {"topic": topic, "editions": cov,
|
|
347
|
+
"parsed_papers": len(papers),
|
|
348
|
+
"field_labelled_papers": sum(bool(p.get("fields")) for p in papers),
|
|
349
|
+
"unattributed_papers": sum(bool(p.get("unattributed")) for p in papers),
|
|
350
|
+
"note": "Denominators include every successfully parsed paper in this scope, "
|
|
351
|
+
"including those with no stated benchmark. Latest editions differ by venue. "
|
|
352
|
+
"Pooled counts are paper-edition observations. Field filters cover labelled papers only; "
|
|
353
|
+
"zero in-scope papers is not proof a venue has no papers in that field."}
|
|
354
|
+
|
|
355
|
+
def _require(self, data: dict) -> dict | None:
|
|
356
|
+
if not data["available"]:
|
|
357
|
+
return {**self._base(data), "error": "Full-text benchmark usage snapshot is not installed. "
|
|
358
|
+
"After extraction and identity review, run bench2agent benchmarks on the build machine "
|
|
359
|
+
"and distribute the data bundle. Abstract mentions are not a fallback."}
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
@staticmethod
|
|
363
|
+
def _role(a: dict) -> str:
|
|
364
|
+
role = a.get("role", "evaluates_on")
|
|
365
|
+
if role not in ROLES:
|
|
366
|
+
raise ValueError("role must be evaluates_on or trains_on")
|
|
367
|
+
return role
|
|
368
|
+
|
|
369
|
+
@staticmethod
|
|
370
|
+
def _paper(p: dict, bid: str, role: str) -> dict:
|
|
371
|
+
return {"paper_id": p["paper_id"], "title": p["title"], "edition": p["edition"],
|
|
372
|
+
"url": "https://arxiv.org/abs/" + p["paper_id"],
|
|
373
|
+
"surfaces": p["uses"][bid].get("surfaces", {}),
|
|
374
|
+
"evidence": p["uses"][bid]["roles"][role]}
|
|
375
|
+
|
|
376
|
+
def usage(self, a: dict) -> dict:
|
|
377
|
+
data = self.data()
|
|
378
|
+
if error := self._require(data):
|
|
379
|
+
return error
|
|
380
|
+
papers, coverage = self._select(data, a, latest=a.get("latest", True))
|
|
381
|
+
role = self._role(a)
|
|
382
|
+
counts = collections.defaultdict(list)
|
|
383
|
+
for p in papers:
|
|
384
|
+
for bid, use in p["uses"].items():
|
|
385
|
+
if use["roles"].get(role):
|
|
386
|
+
counts[bid].append(p)
|
|
387
|
+
cat = {e["id"]: e for e in data["benchmarks"]}
|
|
388
|
+
limit = max(1, min(int(a.get("limit", 10)), 100))
|
|
389
|
+
results = [{"id": bid, "name": cat[bid]["name"], "kind": cat[bid].get("kind"),
|
|
390
|
+
"papers": len(ps), "per_1000": round(len(ps) / len(papers) * 1000, 3),
|
|
391
|
+
**self._links(data, cat[bid]),
|
|
392
|
+
"evidence": [self._paper(p, bid, role) for p in ps[:2]]}
|
|
393
|
+
for bid, ps in sorted(counts.items(), key=lambda x: (-len(x[1]), x[0]))[:limit]]
|
|
394
|
+
return {**self._base(data), "role": role, "coverage": coverage, "results": results,
|
|
395
|
+
"total_benchmarks": len(counts), "truncated": len(counts) > limit,
|
|
396
|
+
"note": "Counts measure explicitly stated use, not quality. Missing stated roles do not prove non-use. "
|
|
397
|
+
+ LINK_NOTE}
|
|
398
|
+
|
|
399
|
+
def evidence(self, a: dict) -> dict:
|
|
400
|
+
data = self.data()
|
|
401
|
+
if error := self._require(data):
|
|
402
|
+
return error
|
|
403
|
+
entry = self._entry(data, a["benchmark"])
|
|
404
|
+
papers, coverage = self._select(data, a)
|
|
405
|
+
role = self._role(a)
|
|
406
|
+
matches = [self._paper(p, entry["id"], role) for p in papers
|
|
407
|
+
if p["uses"].get(entry["id"], {}).get("roles", {}).get(role)]
|
|
408
|
+
offset, limit = max(0, int(a.get("offset", 0))), max(1, min(int(a.get("limit", 10)), 100))
|
|
409
|
+
return {**self._base(data), "id": entry["id"], "name": entry["name"], "role": role,
|
|
410
|
+
**self._links(data, entry),
|
|
411
|
+
"coverage": coverage, "total": len(matches), "results": matches[offset:offset + limit],
|
|
412
|
+
"truncated": offset + limit < len(matches), "note": LINK_NOTE}
|
|
413
|
+
|
|
414
|
+
def trend(self, a: dict) -> dict:
|
|
415
|
+
data = self.data()
|
|
416
|
+
if error := self._require(data):
|
|
417
|
+
return error
|
|
418
|
+
entry = self._entry(data, a["benchmark"])
|
|
419
|
+
papers, coverage = self._select(data, a)
|
|
420
|
+
role = self._role(a)
|
|
421
|
+
counts = collections.Counter(p["edition"] for p in papers
|
|
422
|
+
if p["uses"].get(entry["id"], {}).get("roles", {}).get(role))
|
|
423
|
+
results, comparisons = [], []
|
|
424
|
+
for ed, cov in coverage["editions"].items():
|
|
425
|
+
n, k = cov["in_scope_parsed"], counts[ed]
|
|
426
|
+
results.append({"edition": ed, "papers": k, "parsed_papers": n,
|
|
427
|
+
"per_1000": round(k / n * 1000, 3) if n else None})
|
|
428
|
+
venues = collections.defaultdict(list)
|
|
429
|
+
for r in results:
|
|
430
|
+
venues[r["edition"].rsplit("-", 1)[0]].append(r)
|
|
431
|
+
for series in venues.values():
|
|
432
|
+
for old, new in zip(series, series[1:]):
|
|
433
|
+
n1, n2, k1, k2 = old["parsed_papers"], new["parsed_papers"], old["papers"], new["papers"]
|
|
434
|
+
z, state = None, "insufficient_scope"
|
|
435
|
+
if n1 and n2:
|
|
436
|
+
pooled = (k1 + k2) / (n1 + n2)
|
|
437
|
+
se = math.sqrt(pooled * (1 - pooled) * (1 / n1 + 1 / n2))
|
|
438
|
+
z = (k2 / n2 - k1 / n1) / se if se else 0.0
|
|
439
|
+
state = "no_detected_change"
|
|
440
|
+
if k1 == 0 and k2 >= 8:
|
|
441
|
+
state = "appearing"
|
|
442
|
+
elif abs(z) >= 2.576:
|
|
443
|
+
state = "rising" if z > 0 else "falling"
|
|
444
|
+
comparisons.append({"from": old["edition"], "to": new["edition"],
|
|
445
|
+
"z": round(z, 3) if z is not None else None, "state": state})
|
|
446
|
+
return {**self._base(data), "id": entry["id"], "name": entry["name"], "role": role,
|
|
447
|
+
**self._links(data, entry),
|
|
448
|
+
"coverage": coverage, "results": results, "comparisons": comparisons,
|
|
449
|
+
"rule": "Within venue: two-proportion z, abs(z) >= 2.576; 0 to >=8 papers is appearing."}
|
|
450
|
+
|
|
451
|
+
def adoption(self, a: dict) -> dict:
|
|
452
|
+
data = self.data()
|
|
453
|
+
if error := self._require(data):
|
|
454
|
+
return error
|
|
455
|
+
entry = self._entry(data, a["benchmark"])
|
|
456
|
+
if not entry.get("claims"):
|
|
457
|
+
return {"error": "No reviewed introducing paper in this corpus; developer attribution is unavailable."}
|
|
458
|
+
papers, coverage = self._select(data, a)
|
|
459
|
+
return {**self._base(data), **self._adoption(entry, papers), **self._links(data, entry),
|
|
460
|
+
"coverage": coverage}
|
|
461
|
+
|
|
462
|
+
def _adoption(self, entry: dict, papers: list) -> dict:
|
|
463
|
+
claimers = {c["arxiv_base"] for c in entry["claims"]}
|
|
464
|
+
devs = {person(s) for s in entry.get("developers", []) if person(s)}
|
|
465
|
+
first_year = int(entry["first_claim"].rsplit("-", 1)[1])
|
|
466
|
+
totals = {role: {who: 0 for who in ("others", "self", "undetermined")} for role in ROLES}
|
|
467
|
+
before, per_edition = [], {}
|
|
468
|
+
grouped = collections.defaultdict(list)
|
|
469
|
+
for p in sorted(papers, key=lambda p: (p["edition"].rsplit("-", 1)[1], p["edition"])):
|
|
470
|
+
if p["uses"].get(entry["id"]) and p["paper_id"] not in claimers:
|
|
471
|
+
grouped[p["paper_id"]].append(p)
|
|
472
|
+
ambiguous = {p["paper_id"] for p in papers if p["paper_id"] not in claimers and any(
|
|
473
|
+
entry["id"] in u.get("candidates", []) for u in p.get("unattributed", []))}
|
|
474
|
+
for observed in grouped.values():
|
|
475
|
+
p = observed[0]
|
|
476
|
+
if int(p["edition"].rsplit("-", 1)[1]) < first_year:
|
|
477
|
+
before.append(p["paper_id"])
|
|
478
|
+
continue
|
|
479
|
+
users = {person(s) for obs in observed for s in obs.get("authors", []) if person(s)}
|
|
480
|
+
who = "undetermined" if not users or not devs else "self" if users & devs else "others"
|
|
481
|
+
ec = per_edition.setdefault(p["edition"], {r: {w: 0 for w in totals[r]} for r in ROLES})
|
|
482
|
+
for role in ROLES:
|
|
483
|
+
if any(obs["uses"][entry["id"]]["roles"].get(role) for obs in observed):
|
|
484
|
+
totals[role][who] += 1
|
|
485
|
+
ec[role][who] += 1
|
|
486
|
+
return {"id": entry["id"], "name": entry["name"],
|
|
487
|
+
"first_claim": entry["first_claim"], **totals,
|
|
488
|
+
"by_edition": per_edition, "before_claim": len(before),
|
|
489
|
+
"unattributed_using_papers": len(ambiguous),
|
|
490
|
+
"note": "Unique using papers, introducing papers excluded. Same-name attribution requires local "
|
|
491
|
+
"citation evidence; unattributed uses are additional unresolved observations, not non-use. "
|
|
492
|
+
"Duplicate paper observations are merged and assigned to their earliest observed edition. "
|
|
493
|
+
"Author-name matching is conservative; same-year ordering is unknown. "
|
|
494
|
+
"first_claim means first reviewed claim in this corpus, not first public release."}
|
|
495
|
+
|
|
496
|
+
def new(self, a: dict) -> dict:
|
|
497
|
+
data = self.data()
|
|
498
|
+
kind = a.get("kind", "benchmark")
|
|
499
|
+
if kind not in ("benchmark", "dataset"):
|
|
500
|
+
raise ValueError("kind must be benchmark or dataset")
|
|
501
|
+
sort = a.get("sort", "introduced")
|
|
502
|
+
if sort not in ("introduced", "adoption"):
|
|
503
|
+
raise ValueError("sort must be introduced or adoption")
|
|
504
|
+
entries = [e for e in data["benchmarks"] if e.get("claims") and e.get("kind") == kind]
|
|
505
|
+
if a.get("edition"):
|
|
506
|
+
entries = [e for e in entries if e["first_claim"] == a["edition"].lower()]
|
|
507
|
+
if a.get("venue"):
|
|
508
|
+
entries = [e for e in entries if e["first_claim"].rsplit("-", 1)[0] == a["venue"].lower()]
|
|
509
|
+
if a.get("years"):
|
|
510
|
+
entries = [e for e in entries if int(e["first_claim"].rsplit("-", 1)[1]) in a["years"]]
|
|
511
|
+
if a.get("topic") or a.get("paper_ids") is not None:
|
|
512
|
+
if error := self._require(data):
|
|
513
|
+
return error
|
|
514
|
+
selected, _ = self._select(data, {k: v for k, v in a.items() if k in ("topic", "paper_ids")})
|
|
515
|
+
ids = {p["paper_id"] for p in selected}
|
|
516
|
+
entries = [e for e in entries if any(c["arxiv_base"] in ids for c in e["claims"])]
|
|
517
|
+
entries.sort(key=lambda e: (-int(e["first_claim"].rsplit("-", 1)[1]), e["id"]))
|
|
518
|
+
stats = {}
|
|
519
|
+
if a.get("include_adoption") or sort == "adoption":
|
|
520
|
+
if error := self._require(data):
|
|
521
|
+
return error
|
|
522
|
+
wanted = {e["id"] for e in entries}
|
|
523
|
+
using = collections.defaultdict(list)
|
|
524
|
+
for p in data["papers"]:
|
|
525
|
+
relevant = set(p["uses"]) & wanted
|
|
526
|
+
relevant |= {bid for u in p.get("unattributed", []) for bid in u.get("candidates", []) if bid in wanted}
|
|
527
|
+
for bid in relevant:
|
|
528
|
+
using[bid].append(p)
|
|
529
|
+
stats = {e["id"]: self._adoption(e, using[e["id"]]) for e in entries}
|
|
530
|
+
role = self._role({"role": a.get("adoption_role", "trains_on" if kind == "dataset" else "evaluates_on")})
|
|
531
|
+
if sort == "adoption":
|
|
532
|
+
entries.sort(key=lambda e: (-stats[e["id"]][role]["others"], e["id"]))
|
|
533
|
+
offset, limit = max(0, int(a.get("offset", 0))), max(1, min(int(a.get("limit", 10)), 100))
|
|
534
|
+
return {**self._base(data), "total": len(entries), "kind": kind,
|
|
535
|
+
"results": [{**{k: e.get(k) for k in ("id", "name", "kind", "first_claim", "claims", "homonym_of")},
|
|
536
|
+
**self._links(data, e),
|
|
537
|
+
**({"adoption": stats[e["id"]]} if e["id"] in stats else {})}
|
|
538
|
+
for e in entries[offset:offset + limit]],
|
|
539
|
+
"truncated": offset + limit < len(entries),
|
|
540
|
+
"sort": sort,
|
|
541
|
+
"adoption_scope": "All installed editions, no field filter; introduction filters do not filter using papers." if stats else None,
|
|
542
|
+
"note": "Reviewed introductions are listed regardless of adoption. first_claim is inside this "
|
|
543
|
+
"corpus, not global priority. Call benchmark_adoption for use counts; unavailable is not zero. "
|
|
544
|
+
+ LINK_NOTE}
|
|
545
|
+
|
|
546
|
+
def verify(self, paper_id: str, quote: str) -> dict | None:
|
|
547
|
+
if len(quote.strip()) < 20:
|
|
548
|
+
return None
|
|
549
|
+
q = " ".join(quote.split())
|
|
550
|
+
data = self.data()
|
|
551
|
+
for p in data["papers"]:
|
|
552
|
+
if p["paper_id"] == paper_id:
|
|
553
|
+
for use in p["uses"].values():
|
|
554
|
+
for evidence in use["roles"].values():
|
|
555
|
+
if any(not ev.get("where", "").endswith(":cell") and
|
|
556
|
+
q in " ".join(ev["text"].split()) for ev in evidence):
|
|
557
|
+
return {"title": p["title"], "edition": p["edition"]}
|
|
558
|
+
for e in data["benchmarks"]:
|
|
559
|
+
for c in e.get("claims", []):
|
|
560
|
+
if c["arxiv_base"] == paper_id and q in " ".join(c["evidence"].split()):
|
|
561
|
+
return {"title": c["title"], "edition": c["edition"]}
|
|
562
|
+
return None
|