benchkit-cli 0.1.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.
Files changed (149) hide show
  1. benchkit/__init__.py +1 -0
  2. benchkit/_pi_proxy.py +219 -0
  3. benchkit/answer_key_guard.ts +49 -0
  4. benchkit/artifacts.py +87 -0
  5. benchkit/benchmarks/__init__.py +105 -0
  6. benchkit/benchmarks/aider_polyglot.py +405 -0
  7. benchkit/benchmarks/arc.py +52 -0
  8. benchkit/benchmarks/banking77.py +149 -0
  9. benchkit/benchmarks/base.py +19 -0
  10. benchkit/benchmarks/boolq.py +49 -0
  11. benchkit/benchmarks/chartqa.py +133 -0
  12. benchkit/benchmarks/code_verifier.py +77 -0
  13. benchkit/benchmarks/downloads.py +166 -0
  14. benchkit/benchmarks/evalplus.py +288 -0
  15. benchkit/benchmarks/git_surgery.py +485 -0
  16. benchkit/benchmarks/gpqa.py +56 -0
  17. benchkit/benchmarks/gsm8k.py +97 -0
  18. benchkit/benchmarks/hellaswag.py +49 -0
  19. benchkit/benchmarks/humaneval.py +103 -0
  20. benchkit/benchmarks/ifeval.py +56 -0
  21. benchkit/benchmarks/ifeval_instructions.py +477 -0
  22. benchkit/benchmarks/mbpp.py +60 -0
  23. benchkit/benchmarks/mc_arena.py +479 -0
  24. benchkit/benchmarks/mcq.py +94 -0
  25. benchkit/benchmarks/medfailbench.py +537 -0
  26. benchkit/benchmarks/mmlu.py +54 -0
  27. benchkit/benchmarks/mmlu_pro.py +62 -0
  28. benchkit/benchmarks/mmvp.py +115 -0
  29. benchkit/benchmarks/ocrbench.py +96 -0
  30. benchkit/benchmarks/openbookqa.py +52 -0
  31. benchkit/benchmarks/patcheval.py +929 -0
  32. benchkit/benchmarks/piqa.py +52 -0
  33. benchkit/benchmarks/ruler.py +172 -0
  34. benchkit/benchmarks/ruler_generator.py +535 -0
  35. benchkit/benchmarks/sanity.py +174 -0
  36. benchkit/benchmarks/treejs_arena.py +268 -0
  37. benchkit/benchmarks/truthfulqa.py +52 -0
  38. benchkit/benchmarks/utils.py +25 -0
  39. benchkit/benchmarks/vision.py +230 -0
  40. benchkit/benchmarks/vstar.py +108 -0
  41. benchkit/benchmarks/winogrande.py +52 -0
  42. benchkit/benchmarks/xstest.py +116 -0
  43. benchkit/browser.py +429 -0
  44. benchkit/catalog.py +121 -0
  45. benchkit/cli.py +1000 -0
  46. benchkit/client.py +1413 -0
  47. benchkit/controls.py +87 -0
  48. benchkit/datasets/MMLU_LICENSE.txt +21 -0
  49. benchkit/datasets/README.md +142 -0
  50. benchkit/datasets/arc_challenge.jsonl +1172 -0
  51. benchkit/datasets/banking77.jsonl +3080 -0
  52. benchkit/datasets/banking77_intents.json +79 -0
  53. benchkit/datasets/boolq.jsonl +3270 -0
  54. benchkit/datasets/code_verifier.jsonl +500 -0
  55. benchkit/datasets/gpqa_diamond.jsonl +198 -0
  56. benchkit/datasets/gsm8k.jsonl +1319 -0
  57. benchkit/datasets/hellaswag.jsonl +1000 -0
  58. benchkit/datasets/humaneval.jsonl +164 -0
  59. benchkit/datasets/ifeval.jsonl +541 -0
  60. benchkit/datasets/mbpp.jsonl +500 -0
  61. benchkit/datasets/mc_blocks_1_20_1.jsonl +1003 -0
  62. benchkit/datasets/medfailbench_hard30.jsonl +30 -0
  63. benchkit/datasets/medfailbench_hard30.md +13 -0
  64. benchkit/datasets/openbookqa.jsonl +500 -0
  65. benchkit/datasets/piqa.jsonl +1838 -0
  66. benchkit/datasets/sanity_code.jsonl +6 -0
  67. benchkit/datasets/truthfulqa.jsonl +817 -0
  68. benchkit/datasets/winogrande.jsonl +1267 -0
  69. benchkit/datasets/xstest.csv +451 -0
  70. benchkit/decision.py +194 -0
  71. benchkit/demo.py +451 -0
  72. benchkit/dockerfiles.py +247 -0
  73. benchkit/engine.py +1270 -0
  74. benchkit/evaluation.py +195 -0
  75. benchkit/events.py +141 -0
  76. benchkit/executor.py +96 -0
  77. benchkit/git_surgery/backport-release-stack/setup.sh +328 -0
  78. benchkit/git_surgery/backport-release-stack/verify.sh +119 -0
  79. benchkit/git_surgery/bisect-the-regression/setup.sh +49 -0
  80. benchkit/git_surgery/bisect-the-regression/verify.sh +34 -0
  81. benchkit/git_surgery/rebase-conflict-chain/setup.sh +65 -0
  82. benchkit/git_surgery/rebase-conflict-chain/verify.sh +43 -0
  83. benchkit/git_surgery/recover-complex-stash/setup.sh +152 -0
  84. benchkit/git_surgery/recover-complex-stash/verify.sh +95 -0
  85. benchkit/git_surgery/recover-lost-work/setup.sh +40 -0
  86. benchkit/git_surgery/recover-lost-work/verify.sh +36 -0
  87. benchkit/git_surgery/repair-force-pushed-remote/setup.sh +171 -0
  88. benchkit/git_surgery/repair-force-pushed-remote/verify.sh +83 -0
  89. benchkit/git_surgery/revert-merge-with-followups/setup.sh +198 -0
  90. benchkit/git_surgery/revert-merge-with-followups/verify.sh +98 -0
  91. benchkit/git_surgery/secret-in-history/README.md +27 -0
  92. benchkit/git_surgery/secret-in-history/setup.sh +107 -0
  93. benchkit/git_surgery/secret-in-history/verify.sh +106 -0
  94. benchkit/git_surgery/split-the-mega-commit/setup.sh +50 -0
  95. benchkit/git_surgery/split-the-mega-commit/verify.sh +48 -0
  96. benchkit/git_surgery/untangle-nested-submodules/setup.sh +193 -0
  97. benchkit/git_surgery/untangle-nested-submodules/verify.sh +90 -0
  98. benchkit/history.py +474 -0
  99. benchkit/jobs.py +332 -0
  100. benchkit/leakage.py +51 -0
  101. benchkit/looping.py +873 -0
  102. benchkit/mc_render.py +274 -0
  103. benchkit/mc_viewer/dist/atlas.png +0 -0
  104. benchkit/mc_viewer/dist/blockStates.json.gz +0 -0
  105. benchkit/mc_viewer/dist/version.json +4 -0
  106. benchkit/mc_viewer/dist/viewer.html +23 -0
  107. benchkit/mc_viewer/dist/viewer.js +2 -0
  108. benchkit/mc_viewer/dist/viewer.js.LICENSE.txt +25 -0
  109. benchkit/mc_viewer/dist/worker.js +2 -0
  110. benchkit/mc_viewer/dist/worker.js.LICENSE.txt +19 -0
  111. benchkit/metrics.py +100 -0
  112. benchkit/ollama.py +60 -0
  113. benchkit/perf.py +353 -0
  114. benchkit/perf_report.py +207 -0
  115. benchkit/perturbations.py +208 -0
  116. benchkit/pi_agent.py +476 -0
  117. benchkit/pi_package/package-lock.json +1827 -0
  118. benchkit/pi_package/package.json +8 -0
  119. benchkit/report.py +142 -0
  120. benchkit/report_markdown.py +645 -0
  121. benchkit/results.py +688 -0
  122. benchkit/retry.py +163 -0
  123. benchkit/runner.py +769 -0
  124. benchkit/sandbox.py +1167 -0
  125. benchkit/template_check.py +291 -0
  126. benchkit/templates/arena.html +504 -0
  127. benchkit/templates/history.html +58 -0
  128. benchkit/templates/perf.html +509 -0
  129. benchkit/templates/report.html +769 -0
  130. benchkit/tui/__init__.py +5 -0
  131. benchkit/tui/app.py +87 -0
  132. benchkit/tui/app.tcss +512 -0
  133. benchkit/tui/formatting.py +84 -0
  134. benchkit/tui/screens/__init__.py +1 -0
  135. benchkit/tui/screens/connect.py +191 -0
  136. benchkit/tui/screens/detail.py +288 -0
  137. benchkit/tui/screens/help.py +84 -0
  138. benchkit/tui/screens/modals.py +544 -0
  139. benchkit/tui/screens/results.py +361 -0
  140. benchkit/tui/screens/run.py +988 -0
  141. benchkit/tui/screens/setup.py +733 -0
  142. benchkit/tui/theme.py +135 -0
  143. benchkit/tui/widgets.py +120 -0
  144. benchkit_cli-0.1.0.dist-info/METADATA +294 -0
  145. benchkit_cli-0.1.0.dist-info/RECORD +149 -0
  146. benchkit_cli-0.1.0.dist-info/WHEEL +5 -0
  147. benchkit_cli-0.1.0.dist-info/entry_points.txt +3 -0
  148. benchkit_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  149. benchkit_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,133 @@
1
+ """ChartQA - questions about bar, line and pie charts (2,500 test images).
2
+
3
+ Half the questions are human-written and need reasoning across the chart;
4
+ half are machine-generated lookups. Scoring is the standard relaxed accuracy:
5
+ a numeric answer passes within 5% of the reference, anything else must match
6
+ exactly up to case. Slices interleave the two halves.
7
+
8
+ One deliberate leniency over the reference scorer: no test label carries a
9
+ "%", yet chat models write "6.8%" for a label of 6.8. A percent answer is
10
+ therefore read both as written and divided by 100, and passes if either fits.
11
+ """
12
+
13
+ import re
14
+ from functools import cache
15
+
16
+ from benchkit.benchmarks.base import Task
17
+ from benchkit.benchmarks.utils import strip_think_tags
18
+ from benchkit.benchmarks.vision import (
19
+ category_scores,
20
+ load_rows,
21
+ spread,
22
+ vision_dataset,
23
+ )
24
+ from benchkit.evaluation import EvaluationResult
25
+
26
+ DATASET = "HuggingFaceM4/ChartQA"
27
+ PARQUET = "data/test-00000-of-00001-e2cd0b7a0f9eb20d.parquet"
28
+ REVISION = "b605b6e08b57faf4359aeb2fe6a3ca595f99b6c5"
29
+
30
+ INSTRUCTION = (
31
+ "Answer the question about the chart. You may reason first, but end with "
32
+ 'a final line of the form "Answer: X", where X is a single word or a '
33
+ "number without units."
34
+ )
35
+
36
+ _ANSWER = re.compile(r"answer\s*[::]\s*(.+)", re.IGNORECASE)
37
+ _WRAPPERS = " \t`'\"*_$"
38
+
39
+
40
+ def _convert(index: int, row: dict) -> dict:
41
+ return {
42
+ "task_id": f"chartqa/{index}",
43
+ "category": "augmented" if row["human_or_machine"] else "human",
44
+ "question": row["query"],
45
+ "answers": list(row["label"]),
46
+ }
47
+
48
+
49
+ def extract_answer(response: str) -> str:
50
+ """The text after the last "Answer:", else the last non-empty line."""
51
+ text = strip_think_tags(response).strip()
52
+ found = _ANSWER.findall(text)
53
+ if found:
54
+ answer = found[-1]
55
+ else:
56
+ lines = [line for line in text.splitlines() if line.strip()]
57
+ answer = lines[-1] if lines else ""
58
+ return answer.strip(_WRAPPERS).removesuffix(".").strip(_WRAPPERS)
59
+
60
+
61
+ def _to_floats(text: str) -> list[float]:
62
+ """Numeric readings of an answer: a percent is tried over 100 and raw."""
63
+ text = text.replace(",", "").strip()
64
+ try:
65
+ value = float(text.removesuffix("%").strip())
66
+ except ValueError:
67
+ return []
68
+ return [value / 100.0, value] if text.endswith("%") else [value]
69
+
70
+
71
+ def relaxed_match(prediction: str, target: str, tolerance: float = 0.05) -> bool:
72
+ expected = _to_floats(target)
73
+ predicted = _to_floats(prediction)
74
+ if predicted and expected and expected[0]:
75
+ return any(
76
+ abs(value - expected[0]) / abs(expected[0]) <= tolerance
77
+ for value in predicted
78
+ )
79
+ return prediction.strip().lower() == target.strip().lower()
80
+
81
+
82
+ @cache
83
+ def _rows() -> tuple[dict, ...]:
84
+ return tuple(load_rows("chartqa", DATASET, PARQUET, REVISION, _convert))
85
+
86
+
87
+ class ChartQA:
88
+ name = "chartqa"
89
+ vision = True
90
+ task_count = 2500
91
+ data_source = vision_dataset(
92
+ "chartqa", REVISION, "ChartQA test charts from Hugging Face", _rows
93
+ )
94
+
95
+ def load_tasks(self) -> list[Task]:
96
+ tasks = [
97
+ Task(
98
+ id=row["task_id"],
99
+ prompt=row["question"],
100
+ metadata={
101
+ "answers": row["answers"],
102
+ "category": row["category"],
103
+ "images": [row["image"]],
104
+ },
105
+ )
106
+ for row in _rows()
107
+ ]
108
+ return spread(tasks, lambda task: task.metadata["category"])
109
+
110
+ def build_prompt(self, task: Task) -> str:
111
+ return f"{INSTRUCTION}\n\nQuestion: {task.prompt}"
112
+
113
+ def result_metadata(self, variant: str | None = None) -> dict:
114
+ return {"dataset_revision": REVISION}
115
+
116
+ def evaluate_with_feedback(self, task: Task, response: str) -> EvaluationResult:
117
+ predicted = extract_answer(response)
118
+ passed = any(relaxed_match(predicted, a) for a in task.metadata["answers"])
119
+ return EvaluationResult(
120
+ score=float(passed),
121
+ feedback=""
122
+ if passed
123
+ else 'The final answer is wrong. End with one line "Answer: X".',
124
+ details={"predicted_answer": predicted},
125
+ )
126
+
127
+ def evaluate(self, task: Task, response: str) -> bool:
128
+ return self.evaluate_with_feedback(task, response).passed
129
+
130
+ def summary_fields(self, records: list[object]) -> dict:
131
+ categories = {row["task_id"]: row["category"] for row in _rows()}
132
+ scores = category_scores(records, categories)
133
+ return {"category_scores": scores} if scores else {}
@@ -0,0 +1,77 @@
1
+ """Code verifier - does a model-written HumanEval+ solution pass its tests?"""
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+
7
+ from benchkit.benchmarks.base import Task
8
+ from benchkit.benchmarks.utils import strip_think_tags
9
+
10
+ DATASET = Path(__file__).parent.parent / "datasets" / "code_verifier.jsonl"
11
+
12
+ # One neutral question for both harnesses, so neither is nudged either way.
13
+ QUESTION = "Does this solution correctly solve the problem?"
14
+
15
+
16
+ def _extract_answer(response: str) -> str | None:
17
+ text = strip_think_tags(response)
18
+ answers = re.findall(r"\b(?:yes|no)\b", text.lower())
19
+ return answers[-1] if answers else None
20
+
21
+
22
+ class CodeVerifier:
23
+ name = "code-verifier"
24
+ # Yes/no without a choices list; two options means no repair.
25
+ answer_options = 2
26
+ decision_instructions = QUESTION
27
+ task_count = 500
28
+
29
+ def load_tasks(self) -> list[Task]:
30
+ tasks = []
31
+ with open(DATASET, encoding="utf-8") as f:
32
+ for line in f:
33
+ d = json.loads(line)
34
+ tasks.append(
35
+ Task(
36
+ id=d["id"],
37
+ prompt=(
38
+ f"Problem:\n```python\n{d['prompt'].strip()}\n```\n\n"
39
+ f"Solution:\n```python\n"
40
+ f"{d['solution'].strip()}\n```"
41
+ ),
42
+ metadata={
43
+ "answer": "yes" if d["passes"] else "no",
44
+ "task_id": d["task_id"],
45
+ "model": d["model"],
46
+ },
47
+ )
48
+ )
49
+ return tasks
50
+
51
+ def build_prompt(self, task: Task) -> str:
52
+ return f"{task.prompt}\n\n{QUESTION} Reply with only YES or NO."
53
+
54
+ def evaluate(self, task: Task, response: str) -> bool:
55
+ return _extract_answer(response) == task.metadata["answer"]
56
+
57
+ def summary_fields(self, records: list[object]) -> dict:
58
+ """Score passing and failing solutions separately.
59
+
60
+ The suite is balanced, so a verifier that always says "yes" scores
61
+ 50% overall; the per-class split shows which way a model leans.
62
+ """
63
+ answers = {task.id: task.metadata["answer"] for task in self.load_tasks()}
64
+ passing = [r for r in records if answers.get(r.task_id) == "yes"]
65
+ failing = [r for r in records if answers.get(r.task_id) == "no"]
66
+
67
+ def accuracy(rows: list[object]) -> float | None:
68
+ if not rows:
69
+ return None
70
+ return round(sum(r.passed for r in rows) / len(rows) * 100, 1)
71
+
72
+ return {
73
+ "verifier_pass_total": len(passing),
74
+ "verifier_fail_total": len(failing),
75
+ "verifier_pass_accuracy": accuracy(passing),
76
+ "verifier_fail_accuracy": accuracy(failing),
77
+ }
@@ -0,0 +1,166 @@
1
+ """Datasets that are fetched on demand instead of shipping in the wheel.
2
+
3
+ Small suites are bundled so ``--demo`` and smoke runs work offline. Anything
4
+ large lives in a cache and is described by a :class:`Dataset` on the benchmark
5
+ class, which lets ``benchkit datasets`` and the TUI list what is installed and
6
+ install what is missing without loading any tasks.
7
+
8
+ Plain files such as ``mmlu.jsonl`` are :class:`RemoteFile` downloads: a pinned
9
+ URL plus a sha256, verified before the file is moved into place, so a partial
10
+ or tampered download never reaches a run. They are cached under
11
+ ``~/.cache/benchkit/datasets`` (override with ``BENCHKIT_DATASET_CACHE``).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import os
18
+ import threading
19
+ import time
20
+ from collections.abc import Callable
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+
24
+ import httpx
25
+
26
+ Progress = Callable[[int, int | None], None]
27
+ """Called with ``(bytes_done, bytes_total)`` while a download streams."""
28
+
29
+ _ATTEMPTS = 4
30
+ _LOCK = threading.Lock()
31
+ # Pinned commit of this repository that still carried the bundled copies. A
32
+ # commit pin plus a checksum keeps every install byte-identical to the file
33
+ # earlier results were scored against.
34
+ _PIN = "5789a5e2dcbb056abcbf32f810a1ed01de1cdad6"
35
+ _RAW = f"https://raw.githubusercontent.com/DogukanUrker/BenchKit/{_PIN}"
36
+
37
+
38
+ def cache_root() -> Path:
39
+ configured = os.environ.get("BENCHKIT_DATASET_CACHE")
40
+ if configured:
41
+ return Path(configured).expanduser()
42
+ return Path.home() / ".cache" / "benchkit" / "datasets"
43
+
44
+
45
+ def fmt_bytes(size: float) -> str:
46
+ """Decimal units, matching rich's download progress column."""
47
+ for unit in ("B", "KB", "MB", "GB"):
48
+ if size < 1000 or unit == "GB":
49
+ return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}"
50
+ size /= 1000
51
+ raise AssertionError("unreachable")
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class Dataset:
56
+ """What a benchmark needs on disk, and how to get it.
57
+
58
+ ``install`` is ``None`` for data BenchKit cannot fetch itself; ``hint``
59
+ then says how to provide it. ``size`` is the download size in bytes when it
60
+ is known (``approximate`` when it can drift, like a git clone); once the
61
+ data is installed, listings measure what is actually on disk instead.
62
+ """
63
+
64
+ summary: str
65
+ size: int | None
66
+ location: Callable[[], Path]
67
+ is_installed: Callable[[], bool]
68
+ install: Callable[[Progress | None], object] | None
69
+ hint: str = ""
70
+ approximate: bool = False
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class RemoteFile:
75
+ """One checksummed file fetched into the dataset cache on first use."""
76
+
77
+ name: str
78
+ url: str
79
+ sha256: str
80
+ size: int
81
+
82
+ @property
83
+ def path(self) -> Path:
84
+ return cache_root() / self.name
85
+
86
+ def installed(self) -> bool:
87
+ path = self.path
88
+ return path.is_file() and path.stat().st_size == self.size
89
+
90
+ def ensure(self, progress: Progress | None = None) -> Path:
91
+ """Return the cached path, downloading and verifying it if needed."""
92
+ with _LOCK:
93
+ if not self.installed():
94
+ self._download(progress)
95
+ return self.path
96
+
97
+ def source(self, summary: str) -> Dataset:
98
+ return Dataset(
99
+ summary=summary,
100
+ size=self.size,
101
+ location=lambda: self.path,
102
+ is_installed=self.installed,
103
+ install=self.ensure,
104
+ )
105
+
106
+ def _download(self, progress: Progress | None) -> None:
107
+ target = self.path
108
+ target.parent.mkdir(parents=True, exist_ok=True)
109
+ partial = target.with_name(target.name + ".part")
110
+ for attempt in range(_ATTEMPTS):
111
+ try:
112
+ digest = hashlib.sha256()
113
+ done = 0
114
+ with httpx.stream(
115
+ "GET", self.url, follow_redirects=True, timeout=60
116
+ ) as r:
117
+ r.raise_for_status()
118
+ with open(partial, "wb") as f:
119
+ for chunk in r.iter_bytes():
120
+ f.write(chunk)
121
+ digest.update(chunk)
122
+ done += len(chunk)
123
+ if progress:
124
+ progress(done, self.size)
125
+ break
126
+ except (httpx.TransportError, httpx.HTTPStatusError) as exc:
127
+ retryable = not isinstance(exc, httpx.HTTPStatusError) or (
128
+ exc.response.status_code in {429, 500, 502, 503, 504}
129
+ )
130
+ if not retryable or attempt == _ATTEMPTS - 1:
131
+ partial.unlink(missing_ok=True)
132
+ raise DatasetUnavailable(self, str(exc)) from exc
133
+ time.sleep(2**attempt)
134
+
135
+ if digest.hexdigest() != self.sha256:
136
+ partial.unlink(missing_ok=True)
137
+ raise DatasetUnavailable(
138
+ self, f"checksum mismatch (got {digest.hexdigest()[:12]}…)"
139
+ )
140
+ partial.replace(target)
141
+
142
+
143
+ class DatasetUnavailable(RuntimeError):
144
+ """A dataset is not cached and could not be fetched."""
145
+
146
+ def __init__(self, remote: RemoteFile, reason: str) -> None:
147
+ super().__init__(
148
+ f"{remote.name} is not installed and could not be downloaded "
149
+ f"({reason}). Run `benchkit datasets install` once you are online, "
150
+ f"or place the file at {remote.path} (sha256 {remote.sha256})."
151
+ )
152
+
153
+
154
+ MMLU = RemoteFile(
155
+ name="mmlu.jsonl",
156
+ url=f"{_RAW}/src/benchkit/datasets/mmlu.jsonl",
157
+ sha256="e3e9beb9b3f6b342b99ef59ead76a03c7553fb86705ce17ce95e58e51c1cc37d",
158
+ size=8_563_175,
159
+ )
160
+
161
+ MMLU_PRO = RemoteFile(
162
+ name="mmlu_pro.jsonl",
163
+ url=f"{_RAW}/src/benchkit/datasets/mmlu_pro.jsonl",
164
+ sha256="92ec532d9e2bae342ec7256005c20b6e5defbed76504280df775fb77068f077e",
165
+ size=10_260_787,
166
+ )
@@ -0,0 +1,288 @@
1
+ """EvalPlus benchmarks with stronger HumanEval+ and MBPP+ test suites."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import multiprocessing
7
+ import os
8
+ import platform
9
+ import textwrap
10
+ from functools import cache, lru_cache
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from benchkit.benchmarks.base import Task
15
+ from benchkit.benchmarks.downloads import Dataset
16
+ from benchkit.benchmarks.utils import strip_think_tags
17
+
18
+ HUMANEVAL_SYSTEM = (
19
+ "Complete the following Python function. "
20
+ "Output ONLY the function body. "
21
+ "No markdown fences, no explanation, no examples."
22
+ )
23
+ MBPP_SYSTEM = (
24
+ "Write a Python function to solve the following task. "
25
+ "Output ONLY the function. "
26
+ "No markdown fences, no explanation."
27
+ )
28
+
29
+
30
+ @contextlib.contextmanager
31
+ def _fork_multiprocessing():
32
+ """Temporarily swap ``multiprocessing`` primitives to use the *fork* context.
33
+
34
+ EvalPlus calls ``multiprocessing.Process`` (and friends) using the
35
+ module-level default, which on Python 3.13 / macOS is ``spawn``. The
36
+ ``spawn`` path validates every FD it intends to keep open across
37
+ ``fork_exec``, raising ``ValueError: bad value(s) in fds_to_keep`` when
38
+ the Textual TUI has high-numbered or already-closed descriptors.
39
+
40
+ Switching to ``fork`` side-steps the issue entirely: the child inherits a
41
+ copy of the parent's address space and no FDs need to be explicitly
42
+ forwarded. The swap is scoped so the rest of the process is unaffected.
43
+
44
+ EvalPlus's ``evalplus.eval`` does ``from multiprocessing import Array, Value``
45
+ so those names are bound on that module and must be patched there as well.
46
+ """
47
+ import evalplus.eval as _ev
48
+
49
+ fork_ctx = multiprocessing.get_context("fork")
50
+ mp_originals = (
51
+ multiprocessing.Process,
52
+ multiprocessing.Value,
53
+ multiprocessing.Array,
54
+ )
55
+ ev_originals = (_ev.Value, _ev.Array)
56
+ try:
57
+ multiprocessing.Process = fork_ctx.Process
58
+ multiprocessing.Value = fork_ctx.Value
59
+ multiprocessing.Array = fork_ctx.Array
60
+ _ev.Value = fork_ctx.Value
61
+ _ev.Array = fork_ctx.Array
62
+ yield
63
+ finally:
64
+ multiprocessing.Process, multiprocessing.Value, multiprocessing.Array = (
65
+ mp_originals
66
+ )
67
+ _ev.Value, _ev.Array = ev_originals
68
+
69
+
70
+ def _evalplus_api():
71
+ """Import EvalPlus lazily so normal startup stays lightweight."""
72
+ # RLIMIT_AS is not reliably supported on macOS. EvalPlus otherwise tries
73
+ # to impose a 4 GiB address-space limit that can make every check fail
74
+ # before generated code starts.
75
+ if platform.system() == "Darwin":
76
+ os.environ.setdefault("EVALPLUS_MAX_MEMORY_BYTES", "-1")
77
+
78
+ try:
79
+ from evalplus.data import get_human_eval_plus, get_mbpp_plus
80
+ from evalplus.eval import PASS
81
+ from evalplus.eval._special_oracle import MBPP_OUTPUT_NOT_NONE_TASKS
82
+ from evalplus.evaluate import check_correctness
83
+ from evalplus.gen.util import trusted_exec
84
+ except ImportError as exc:
85
+ raise RuntimeError(
86
+ "EvalPlus is missing from the environment. Run: uv sync"
87
+ ) from exc
88
+
89
+ return (
90
+ get_human_eval_plus,
91
+ get_mbpp_plus,
92
+ PASS,
93
+ MBPP_OUTPUT_NOT_NONE_TASKS,
94
+ check_correctness,
95
+ trusted_exec,
96
+ )
97
+
98
+
99
+ @lru_cache(maxsize=2)
100
+ def _problems(dataset: str) -> dict[str, dict[str, Any]]:
101
+ get_human_eval_plus, get_mbpp_plus, *_ = _evalplus_api()
102
+ if dataset == "humaneval":
103
+ return get_human_eval_plus()
104
+ if dataset == "mbpp":
105
+ return get_mbpp_plus()
106
+ raise ValueError(f"Unsupported EvalPlus dataset: {dataset}")
107
+
108
+
109
+ @cache
110
+ def _expected_output(dataset: str, task_id: str) -> dict[str, list[Any]]:
111
+ """Compute the official oracle outputs once per task and process."""
112
+ (
113
+ _,
114
+ _,
115
+ _,
116
+ output_not_none_tasks,
117
+ _,
118
+ trusted_exec,
119
+ ) = _evalplus_api()
120
+ problem = _problems(dataset)[task_id]
121
+ oracle_code = problem["prompt"] + problem["canonical_solution"]
122
+ output_not_none = (
123
+ dataset == "mbpp" and problem["entry_point"] in output_not_none_tasks
124
+ )
125
+
126
+ base, base_time = trusted_exec(
127
+ oracle_code,
128
+ problem["base_input"],
129
+ problem["entry_point"],
130
+ record_time=True,
131
+ output_not_none=output_not_none,
132
+ )
133
+ plus, plus_time = trusted_exec(
134
+ oracle_code,
135
+ problem["plus_input"],
136
+ problem["entry_point"],
137
+ record_time=True,
138
+ output_not_none=output_not_none,
139
+ )
140
+ return {
141
+ "base": base,
142
+ "base_time": base_time,
143
+ "plus": plus,
144
+ "plus_time": plus_time,
145
+ }
146
+
147
+
148
+ def _extract_code(response: str) -> str:
149
+ """Strip reasoning and a single Markdown code fence."""
150
+ text = strip_think_tags(response).rstrip()
151
+ if "```python" in text:
152
+ text = text.split("```python", 1)[1].split("```", 1)[0]
153
+ elif "```" in text:
154
+ text = text.split("```", 1)[1].split("```", 1)[0]
155
+ return text.strip("\n")
156
+
157
+
158
+ def _human_solution(problem: dict[str, Any], response: str) -> str:
159
+ code = _extract_code(response)
160
+ entry_point = problem["entry_point"]
161
+ if f"def {entry_point}" not in code:
162
+ normalized = textwrap.dedent(code)
163
+ solution = problem["prompt"] + textwrap.indent(normalized, " ")
164
+ try:
165
+ compile(solution, "<benchkit-humaneval>", "exec")
166
+ except (IndentationError, TabError):
167
+ lines = code.splitlines()
168
+ first_index = next(
169
+ (index for index, line in enumerate(lines) if line.strip()), None
170
+ )
171
+ if first_index is not None and not lines[first_index].startswith(
172
+ (" ", "\t")
173
+ ):
174
+ lines[first_index] = " " + lines[first_index]
175
+ normalized = textwrap.dedent("\n".join(lines))
176
+ repaired = problem["prompt"] + textwrap.indent(normalized, " ")
177
+ try:
178
+ compile(repaired, "<benchkit-humaneval>", "exec")
179
+ except (IndentationError, TabError):
180
+ pass
181
+ else:
182
+ solution = repaired
183
+ return solution
184
+
185
+ imports = [
186
+ line
187
+ for line in problem["prompt"].splitlines()
188
+ if line.startswith(("import ", "from "))
189
+ ]
190
+ import_block = "\n".join(imports)
191
+ return f"{import_block}\n\n{code}" if imports else code
192
+
193
+
194
+ def _evalplus_cache(dataset: str) -> Path:
195
+ from evalplus.data.humaneval import HUMANEVAL_PLUS_VERSION
196
+ from evalplus.data.mbpp import MBPP_PLUS_VERSION
197
+ from evalplus.data.utils import get_dataset_metadata
198
+
199
+ if dataset == "humaneval":
200
+ name, version = "HumanEvalPlus", HUMANEVAL_PLUS_VERSION
201
+ else:
202
+ name, version = "MbppPlus", MBPP_PLUS_VERSION
203
+ return Path(get_dataset_metadata(name, version, mini=False)[1])
204
+
205
+
206
+ def _evalplus_installed(dataset: str) -> bool:
207
+ try:
208
+ return _evalplus_cache(dataset).exists()
209
+ except ImportError:
210
+ return False
211
+
212
+
213
+ def _evalplus_source(dataset: str, summary: str, size: int) -> Dataset:
214
+ return Dataset(
215
+ summary=summary,
216
+ size=size,
217
+ location=lambda: _evalplus_cache(dataset),
218
+ is_installed=lambda: _evalplus_installed(dataset),
219
+ install=lambda _progress: _quiet_problems(dataset),
220
+ )
221
+
222
+
223
+ def _quiet_problems(dataset: str) -> None:
224
+ # EvalPlus downloads through wget, which draws its own progress bar on
225
+ # stdout and tears up the CLI's live progress line and the TUI.
226
+ with open(os.devnull, "w") as sink, contextlib.redirect_stdout(sink):
227
+ _problems(dataset)
228
+
229
+
230
+ class _EvalPlusBenchmark:
231
+ dataset: str
232
+ name: str
233
+ task_count: int
234
+ system: str
235
+ evaluation_activity = "executing code and running EvalPlus tests"
236
+
237
+ def load_tasks(self) -> list[Task]:
238
+ return [
239
+ Task(
240
+ id=task_id,
241
+ prompt=problem["prompt"],
242
+ metadata={
243
+ "entry_point": problem["entry_point"],
244
+ "canonical_solution": problem["canonical_solution"],
245
+ },
246
+ )
247
+ for task_id, problem in _problems(self.dataset).items()
248
+ ]
249
+
250
+ def build_prompt(self, task: Task) -> str:
251
+ return f"{self.system}\n\n{task.prompt}"
252
+
253
+ def evaluate(self, task: Task, response: str) -> bool:
254
+ *_, pass_status, _, check_correctness, _ = _evalplus_api()
255
+ problem = _problems(self.dataset)[task.id]
256
+ if self.dataset == "humaneval":
257
+ solution = _human_solution(problem, response)
258
+ else:
259
+ solution = _extract_code(response)
260
+
261
+ with _fork_multiprocessing():
262
+ result = check_correctness(
263
+ self.dataset,
264
+ completion_id=0,
265
+ problem=problem,
266
+ solution=solution,
267
+ expected_output=_expected_output(self.dataset, task.id),
268
+ fast_check=True,
269
+ )
270
+ return result["base"][0] == pass_status and result["plus"][0] == pass_status
271
+
272
+
273
+ class HumanEvalPlus(_EvalPlusBenchmark):
274
+ name = "humaneval-plus"
275
+ dataset = "humaneval"
276
+ task_count = 164
277
+ system = HUMANEVAL_SYSTEM
278
+ data_source = _evalplus_source(
279
+ "humaneval", "HumanEval+ release from EvalPlus", size=925_932
280
+ )
281
+
282
+
283
+ class MBPPPlus(_EvalPlusBenchmark):
284
+ name = "mbpp-plus"
285
+ dataset = "mbpp"
286
+ task_count = 378
287
+ system = MBPP_SYSTEM
288
+ data_source = _evalplus_source("mbpp", "MBPP+ release from EvalPlus", size=336_032)