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.
- benchkit/__init__.py +1 -0
- benchkit/_pi_proxy.py +219 -0
- benchkit/answer_key_guard.ts +49 -0
- benchkit/artifacts.py +87 -0
- benchkit/benchmarks/__init__.py +105 -0
- benchkit/benchmarks/aider_polyglot.py +405 -0
- benchkit/benchmarks/arc.py +52 -0
- benchkit/benchmarks/banking77.py +149 -0
- benchkit/benchmarks/base.py +19 -0
- benchkit/benchmarks/boolq.py +49 -0
- benchkit/benchmarks/chartqa.py +133 -0
- benchkit/benchmarks/code_verifier.py +77 -0
- benchkit/benchmarks/downloads.py +166 -0
- benchkit/benchmarks/evalplus.py +288 -0
- benchkit/benchmarks/git_surgery.py +485 -0
- benchkit/benchmarks/gpqa.py +56 -0
- benchkit/benchmarks/gsm8k.py +97 -0
- benchkit/benchmarks/hellaswag.py +49 -0
- benchkit/benchmarks/humaneval.py +103 -0
- benchkit/benchmarks/ifeval.py +56 -0
- benchkit/benchmarks/ifeval_instructions.py +477 -0
- benchkit/benchmarks/mbpp.py +60 -0
- benchkit/benchmarks/mc_arena.py +479 -0
- benchkit/benchmarks/mcq.py +94 -0
- benchkit/benchmarks/medfailbench.py +537 -0
- benchkit/benchmarks/mmlu.py +54 -0
- benchkit/benchmarks/mmlu_pro.py +62 -0
- benchkit/benchmarks/mmvp.py +115 -0
- benchkit/benchmarks/ocrbench.py +96 -0
- benchkit/benchmarks/openbookqa.py +52 -0
- benchkit/benchmarks/patcheval.py +929 -0
- benchkit/benchmarks/piqa.py +52 -0
- benchkit/benchmarks/ruler.py +172 -0
- benchkit/benchmarks/ruler_generator.py +535 -0
- benchkit/benchmarks/sanity.py +174 -0
- benchkit/benchmarks/treejs_arena.py +268 -0
- benchkit/benchmarks/truthfulqa.py +52 -0
- benchkit/benchmarks/utils.py +25 -0
- benchkit/benchmarks/vision.py +230 -0
- benchkit/benchmarks/vstar.py +108 -0
- benchkit/benchmarks/winogrande.py +52 -0
- benchkit/benchmarks/xstest.py +116 -0
- benchkit/browser.py +429 -0
- benchkit/catalog.py +121 -0
- benchkit/cli.py +1000 -0
- benchkit/client.py +1413 -0
- benchkit/controls.py +87 -0
- benchkit/datasets/MMLU_LICENSE.txt +21 -0
- benchkit/datasets/README.md +142 -0
- benchkit/datasets/arc_challenge.jsonl +1172 -0
- benchkit/datasets/banking77.jsonl +3080 -0
- benchkit/datasets/banking77_intents.json +79 -0
- benchkit/datasets/boolq.jsonl +3270 -0
- benchkit/datasets/code_verifier.jsonl +500 -0
- benchkit/datasets/gpqa_diamond.jsonl +198 -0
- benchkit/datasets/gsm8k.jsonl +1319 -0
- benchkit/datasets/hellaswag.jsonl +1000 -0
- benchkit/datasets/humaneval.jsonl +164 -0
- benchkit/datasets/ifeval.jsonl +541 -0
- benchkit/datasets/mbpp.jsonl +500 -0
- benchkit/datasets/mc_blocks_1_20_1.jsonl +1003 -0
- benchkit/datasets/medfailbench_hard30.jsonl +30 -0
- benchkit/datasets/medfailbench_hard30.md +13 -0
- benchkit/datasets/openbookqa.jsonl +500 -0
- benchkit/datasets/piqa.jsonl +1838 -0
- benchkit/datasets/sanity_code.jsonl +6 -0
- benchkit/datasets/truthfulqa.jsonl +817 -0
- benchkit/datasets/winogrande.jsonl +1267 -0
- benchkit/datasets/xstest.csv +451 -0
- benchkit/decision.py +194 -0
- benchkit/demo.py +451 -0
- benchkit/dockerfiles.py +247 -0
- benchkit/engine.py +1270 -0
- benchkit/evaluation.py +195 -0
- benchkit/events.py +141 -0
- benchkit/executor.py +96 -0
- benchkit/git_surgery/backport-release-stack/setup.sh +328 -0
- benchkit/git_surgery/backport-release-stack/verify.sh +119 -0
- benchkit/git_surgery/bisect-the-regression/setup.sh +49 -0
- benchkit/git_surgery/bisect-the-regression/verify.sh +34 -0
- benchkit/git_surgery/rebase-conflict-chain/setup.sh +65 -0
- benchkit/git_surgery/rebase-conflict-chain/verify.sh +43 -0
- benchkit/git_surgery/recover-complex-stash/setup.sh +152 -0
- benchkit/git_surgery/recover-complex-stash/verify.sh +95 -0
- benchkit/git_surgery/recover-lost-work/setup.sh +40 -0
- benchkit/git_surgery/recover-lost-work/verify.sh +36 -0
- benchkit/git_surgery/repair-force-pushed-remote/setup.sh +171 -0
- benchkit/git_surgery/repair-force-pushed-remote/verify.sh +83 -0
- benchkit/git_surgery/revert-merge-with-followups/setup.sh +198 -0
- benchkit/git_surgery/revert-merge-with-followups/verify.sh +98 -0
- benchkit/git_surgery/secret-in-history/README.md +27 -0
- benchkit/git_surgery/secret-in-history/setup.sh +107 -0
- benchkit/git_surgery/secret-in-history/verify.sh +106 -0
- benchkit/git_surgery/split-the-mega-commit/setup.sh +50 -0
- benchkit/git_surgery/split-the-mega-commit/verify.sh +48 -0
- benchkit/git_surgery/untangle-nested-submodules/setup.sh +193 -0
- benchkit/git_surgery/untangle-nested-submodules/verify.sh +90 -0
- benchkit/history.py +474 -0
- benchkit/jobs.py +332 -0
- benchkit/leakage.py +51 -0
- benchkit/looping.py +873 -0
- benchkit/mc_render.py +274 -0
- benchkit/mc_viewer/dist/atlas.png +0 -0
- benchkit/mc_viewer/dist/blockStates.json.gz +0 -0
- benchkit/mc_viewer/dist/version.json +4 -0
- benchkit/mc_viewer/dist/viewer.html +23 -0
- benchkit/mc_viewer/dist/viewer.js +2 -0
- benchkit/mc_viewer/dist/viewer.js.LICENSE.txt +25 -0
- benchkit/mc_viewer/dist/worker.js +2 -0
- benchkit/mc_viewer/dist/worker.js.LICENSE.txt +19 -0
- benchkit/metrics.py +100 -0
- benchkit/ollama.py +60 -0
- benchkit/perf.py +353 -0
- benchkit/perf_report.py +207 -0
- benchkit/perturbations.py +208 -0
- benchkit/pi_agent.py +476 -0
- benchkit/pi_package/package-lock.json +1827 -0
- benchkit/pi_package/package.json +8 -0
- benchkit/report.py +142 -0
- benchkit/report_markdown.py +645 -0
- benchkit/results.py +688 -0
- benchkit/retry.py +163 -0
- benchkit/runner.py +769 -0
- benchkit/sandbox.py +1167 -0
- benchkit/template_check.py +291 -0
- benchkit/templates/arena.html +504 -0
- benchkit/templates/history.html +58 -0
- benchkit/templates/perf.html +509 -0
- benchkit/templates/report.html +769 -0
- benchkit/tui/__init__.py +5 -0
- benchkit/tui/app.py +87 -0
- benchkit/tui/app.tcss +512 -0
- benchkit/tui/formatting.py +84 -0
- benchkit/tui/screens/__init__.py +1 -0
- benchkit/tui/screens/connect.py +191 -0
- benchkit/tui/screens/detail.py +288 -0
- benchkit/tui/screens/help.py +84 -0
- benchkit/tui/screens/modals.py +544 -0
- benchkit/tui/screens/results.py +361 -0
- benchkit/tui/screens/run.py +988 -0
- benchkit/tui/screens/setup.py +733 -0
- benchkit/tui/theme.py +135 -0
- benchkit/tui/widgets.py +120 -0
- benchkit_cli-0.1.0.dist-info/METADATA +294 -0
- benchkit_cli-0.1.0.dist-info/RECORD +149 -0
- benchkit_cli-0.1.0.dist-info/WHEEL +5 -0
- benchkit_cli-0.1.0.dist-info/entry_points.txt +3 -0
- benchkit_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- benchkit_cli-0.1.0.dist-info/top_level.txt +1 -0
benchkit/demo.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""Offline stand-in inference client used by ``benchkit --demo``.
|
|
2
|
+
|
|
3
|
+
Demo mode exists so the interface can be explored (and reviewed) without an
|
|
4
|
+
inference server: it answers prompts locally with a per-model skill level, so
|
|
5
|
+
scores, speeds and pass/fail streaks look like a real run.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import importlib
|
|
12
|
+
import json
|
|
13
|
+
import random
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from benchkit.benchmarks.base import Task
|
|
20
|
+
from benchkit.client import GenerationUpdate
|
|
21
|
+
from benchkit.engine import benchmark, prompt_for, tasks_for
|
|
22
|
+
from benchkit.perturbations import perturb_task
|
|
23
|
+
|
|
24
|
+
LETTERS = "ABCDEFGHIJ"
|
|
25
|
+
|
|
26
|
+
DEMO_MODELS = [
|
|
27
|
+
{"name": "demo-nano:0.5b", "size": 0.4e9, "skill": 0.30, "speed": 240.0},
|
|
28
|
+
{"name": "demo-mini:3b", "size": 2.1e9, "skill": 0.55, "speed": 130.0},
|
|
29
|
+
{"name": "demo-pro:8b", "size": 5.2e9, "skill": 0.76, "speed": 68.0},
|
|
30
|
+
{"name": "demo-max:14b", "size": 9.4e9, "skill": 0.91, "speed": 34.0},
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _dataset_path(key: str) -> Path | None:
|
|
35
|
+
"""The bundled dataset file, if any; downloaded ones carry no solutions."""
|
|
36
|
+
module = importlib.import_module(type(benchmark(key)).__module__)
|
|
37
|
+
path = getattr(module, "DATASET", None)
|
|
38
|
+
return path if isinstance(path, Path) else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _canonical_solutions(key: str) -> dict[str, str]:
|
|
42
|
+
"""Reference solutions from a dataset file, when it ships any."""
|
|
43
|
+
path = _dataset_path(key)
|
|
44
|
+
if path is None:
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
solutions: dict[str, str] = {}
|
|
48
|
+
try:
|
|
49
|
+
with open(path, encoding="utf-8") as handle:
|
|
50
|
+
for line in handle:
|
|
51
|
+
row = json.loads(line)
|
|
52
|
+
solution = row.get("canonical_solution") or row.get("code")
|
|
53
|
+
if solution:
|
|
54
|
+
solutions[row["task_id"]] = solution
|
|
55
|
+
# ValueError covers both JSONDecodeError and UnicodeDecodeError.
|
|
56
|
+
except (OSError, ValueError, KeyError):
|
|
57
|
+
return {}
|
|
58
|
+
return solutions
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _letters(task: Task) -> str:
|
|
62
|
+
"""Option letters a multiple-choice task actually offers."""
|
|
63
|
+
choices = task.metadata.get("choices")
|
|
64
|
+
if isinstance(choices, (list, tuple)) and choices:
|
|
65
|
+
return LETTERS[: len(choices)]
|
|
66
|
+
return LETTERS[:4]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _wrong_letter(answer: str, letters: str) -> str:
|
|
70
|
+
options = [letter for letter in letters if letter != answer]
|
|
71
|
+
return random.choice(options) if options else "A"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_FILLER_SENTENCES = (
|
|
75
|
+
"Here is a considered response to the request at hand.",
|
|
76
|
+
"The plan below keeps every stated requirement in view.",
|
|
77
|
+
"Each step is written plainly so nothing is left implicit.",
|
|
78
|
+
"A short pause for structure makes the result easier to follow.",
|
|
79
|
+
"The remaining detail fills out the picture without extra noise.",
|
|
80
|
+
"This closing thought ties the pieces back together.",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _ifeval_body(sentences: int, words: int | None) -> str:
|
|
85
|
+
"""Filler prose sized to the sentence and word budget of a task."""
|
|
86
|
+
text: list[str] = []
|
|
87
|
+
for index in range(max(sentences, 1)):
|
|
88
|
+
text.append(_FILLER_SENTENCES[index % len(_FILLER_SENTENCES)])
|
|
89
|
+
|
|
90
|
+
if words:
|
|
91
|
+
while len(" ".join(text).split()) < words:
|
|
92
|
+
text[-1] = text[-1].rstrip(".") + " and a little more detail follows."
|
|
93
|
+
return " ".join(text)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _ifeval_answer(task: Task) -> str:
|
|
97
|
+
"""Compose a response that satisfies the simpler IFEval constraints.
|
|
98
|
+
|
|
99
|
+
Demo mode never calls a real model, so the strongest demo answer is one
|
|
100
|
+
built directly from the instruction metadata. Constraints that need real
|
|
101
|
+
language understanding (response language, letter frequency) are skipped,
|
|
102
|
+
which keeps demo scores below a perfect run.
|
|
103
|
+
"""
|
|
104
|
+
args = {
|
|
105
|
+
instruction: dict(kwargs or {})
|
|
106
|
+
for instruction, kwargs in zip(
|
|
107
|
+
task.metadata["instruction_id_list"],
|
|
108
|
+
task.metadata["kwargs"],
|
|
109
|
+
strict=False,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if "detectable_format:json_format" in args:
|
|
114
|
+
return '{"answer": "demo response", "complete": true}'
|
|
115
|
+
if "detectable_format:constrained_response" in args:
|
|
116
|
+
return "My answer is maybe."
|
|
117
|
+
|
|
118
|
+
sentence_spec = args.get("length_constraints:number_sentences")
|
|
119
|
+
sentences = 4
|
|
120
|
+
if sentence_spec:
|
|
121
|
+
target = int(sentence_spec["num_sentences"])
|
|
122
|
+
sentences = (
|
|
123
|
+
target if sentence_spec["relation"] == "at least" else max(target - 1, 1)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
word_spec = args.get("length_constraints:number_words")
|
|
127
|
+
words = None
|
|
128
|
+
if word_spec and word_spec["relation"] == "at least":
|
|
129
|
+
words = int(word_spec["num_words"])
|
|
130
|
+
elif word_spec:
|
|
131
|
+
sentences = min(sentences, 2)
|
|
132
|
+
|
|
133
|
+
body = _ifeval_body(sentences, words)
|
|
134
|
+
|
|
135
|
+
keywords = args.get("keywords:existence", {}).get("keywords", [])
|
|
136
|
+
if keywords:
|
|
137
|
+
body += " Keywords in play: " + " ".join(keywords) + "."
|
|
138
|
+
|
|
139
|
+
frequency = args.get("keywords:frequency")
|
|
140
|
+
if frequency and frequency["relation"] == "at least":
|
|
141
|
+
repeats = [frequency["keyword"]] * int(frequency["frequency"])
|
|
142
|
+
body += " " + " ".join(repeats) + "."
|
|
143
|
+
|
|
144
|
+
placeholders = args.get("detectable_content:number_placeholders")
|
|
145
|
+
if placeholders:
|
|
146
|
+
count = int(placeholders["num_placeholders"])
|
|
147
|
+
body += " " + " ".join(f"[field {i + 1}]" for i in range(count))
|
|
148
|
+
|
|
149
|
+
highlights = args.get("detectable_format:number_highlighted_sections")
|
|
150
|
+
if highlights:
|
|
151
|
+
count = int(highlights["num_highlights"])
|
|
152
|
+
body += "\n" + "\n".join(f"*highlight {i + 1}*" for i in range(count))
|
|
153
|
+
|
|
154
|
+
bullets = args.get("detectable_format:number_bullet_lists")
|
|
155
|
+
if bullets:
|
|
156
|
+
count = int(bullets["num_bullets"])
|
|
157
|
+
body += "\n" + "\n".join(f"* point {i + 1}" for i in range(count))
|
|
158
|
+
|
|
159
|
+
sections = args.get("detectable_format:multiple_sections")
|
|
160
|
+
if sections:
|
|
161
|
+
spliter = sections["section_spliter"]
|
|
162
|
+
count = int(sections["num_sections"])
|
|
163
|
+
body = "\n".join(f"{spliter} {i + 1}\n{body}" for i in range(count))
|
|
164
|
+
|
|
165
|
+
paragraphs = args.get("length_constraints:number_paragraphs")
|
|
166
|
+
if paragraphs:
|
|
167
|
+
count = int(paragraphs["num_paragraphs"])
|
|
168
|
+
body = "\n***\n".join([body] * count)
|
|
169
|
+
|
|
170
|
+
if "detectable_format:title" in args:
|
|
171
|
+
body = f"<<Demo Response>>\n{body}"
|
|
172
|
+
|
|
173
|
+
if "combination:repeat_prompt" in args:
|
|
174
|
+
prompt = args["combination:repeat_prompt"].get("prompt_to_repeat", task.prompt)
|
|
175
|
+
body = f"{prompt}\n{body}"
|
|
176
|
+
|
|
177
|
+
if "combination:two_responses" in args:
|
|
178
|
+
body = f"{body}\n******\n{body}"
|
|
179
|
+
|
|
180
|
+
postscript = args.get("detectable_content:postscript")
|
|
181
|
+
if postscript:
|
|
182
|
+
body += f"\n\n{postscript['postscript_marker']} That is the whole answer."
|
|
183
|
+
|
|
184
|
+
end_phrase = args.get("startend:end_checker")
|
|
185
|
+
if end_phrase:
|
|
186
|
+
body += f" {end_phrase['end_phrase']}"
|
|
187
|
+
|
|
188
|
+
forbidden = args.get("keywords:forbidden_words", {}).get("forbidden_words", [])
|
|
189
|
+
for word in forbidden:
|
|
190
|
+
body = body.replace(word, "…").replace(word.capitalize(), "…")
|
|
191
|
+
|
|
192
|
+
if "punctuation:no_comma" in args:
|
|
193
|
+
body = body.replace(",", "")
|
|
194
|
+
|
|
195
|
+
if "change_case:english_capital" in args:
|
|
196
|
+
body = body.upper()
|
|
197
|
+
elif "change_case:english_lowercase" in args:
|
|
198
|
+
body = body.lower()
|
|
199
|
+
|
|
200
|
+
if "startend:quotation" in args:
|
|
201
|
+
body = f'"{body}"'
|
|
202
|
+
|
|
203
|
+
return body
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _good_answer(task: Task, solution: str | None) -> str:
|
|
207
|
+
metadata = task.metadata
|
|
208
|
+
solution = solution or metadata.get("canonical_solution")
|
|
209
|
+
if solution is not None:
|
|
210
|
+
return solution
|
|
211
|
+
|
|
212
|
+
if "instruction_id_list" in metadata:
|
|
213
|
+
return _ifeval_answer(task)
|
|
214
|
+
|
|
215
|
+
answers = metadata.get("answers")
|
|
216
|
+
if isinstance(answers, (list, tuple)):
|
|
217
|
+
return ", ".join(str(answer) for answer in answers)
|
|
218
|
+
|
|
219
|
+
answer = metadata.get("answer")
|
|
220
|
+
if isinstance(answer, bool):
|
|
221
|
+
return "Yes" if answer else "No"
|
|
222
|
+
if isinstance(answer, str) and answer.lower() in {"yes", "no"}:
|
|
223
|
+
return answer.capitalize()
|
|
224
|
+
if isinstance(answer, str) and answer.upper() in _letters(task):
|
|
225
|
+
return f"The answer is {answer.upper()}."
|
|
226
|
+
if answer is not None:
|
|
227
|
+
return f"Working through it step by step.\n\n#### {answer}"
|
|
228
|
+
return "A"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _bad_answer(task: Task, has_solution: bool) -> str:
|
|
232
|
+
metadata = task.metadata
|
|
233
|
+
if has_solution or "test" in metadata:
|
|
234
|
+
return " raise NotImplementedError"
|
|
235
|
+
|
|
236
|
+
if "instruction_id_list" in metadata:
|
|
237
|
+
return "Sure, here is a quick answer that ignores the formatting rules."
|
|
238
|
+
|
|
239
|
+
if isinstance(metadata.get("answers"), (list, tuple)):
|
|
240
|
+
return "NOT_FOUND"
|
|
241
|
+
|
|
242
|
+
answer = metadata.get("answer")
|
|
243
|
+
if isinstance(answer, bool):
|
|
244
|
+
return "No" if answer else "Yes"
|
|
245
|
+
if isinstance(answer, str) and answer.lower() in {"yes", "no"}:
|
|
246
|
+
return "No" if answer.lower() == "yes" else "Yes"
|
|
247
|
+
if isinstance(answer, str) and answer.upper() in _letters(task):
|
|
248
|
+
return f"The answer is {_wrong_letter(answer.upper(), _letters(task))}."
|
|
249
|
+
if answer is not None:
|
|
250
|
+
return "Let me compute that.\n\n#### 42"
|
|
251
|
+
return "B"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class DemoClient:
|
|
255
|
+
"""Mimics :class:`benchkit.client.InferenceClient` without a server."""
|
|
256
|
+
|
|
257
|
+
host = "demo://offline"
|
|
258
|
+
label = "Demo mode"
|
|
259
|
+
simulated_timing = True
|
|
260
|
+
provider = "demo"
|
|
261
|
+
timeout = 0.0
|
|
262
|
+
|
|
263
|
+
def __init__(self, speed: float = 1.0) -> None:
|
|
264
|
+
self.speed = max(speed, 0.01)
|
|
265
|
+
self._answers: dict[str, tuple[Task, str | None]] = {}
|
|
266
|
+
self._primed: set[tuple[str, str | None, int]] = set()
|
|
267
|
+
|
|
268
|
+
def prime(
|
|
269
|
+
self,
|
|
270
|
+
benchmark_keys: list[str],
|
|
271
|
+
perturbations: list[tuple[str, int]] | None = None,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Index prompts for the benchmarks about to run."""
|
|
274
|
+
specs: list[tuple[str | None, int]] = [(None, 42)]
|
|
275
|
+
specs.extend(perturbations or [])
|
|
276
|
+
for key in benchmark_keys:
|
|
277
|
+
bench = benchmark(key)
|
|
278
|
+
solutions = _canonical_solutions(key)
|
|
279
|
+
selected_tasks = tasks_for(key)
|
|
280
|
+
variants = getattr(bench, "variants", None)
|
|
281
|
+
select = getattr(bench, "tasks_for_variant", None)
|
|
282
|
+
if callable(variants) and callable(select):
|
|
283
|
+
selected_tasks = [
|
|
284
|
+
task
|
|
285
|
+
for variant in variants(self, DEMO_MODELS[0]["name"])
|
|
286
|
+
for task in select(selected_tasks, variant)
|
|
287
|
+
]
|
|
288
|
+
for perturbation, seed in specs:
|
|
289
|
+
cache_key = (key, perturbation, seed)
|
|
290
|
+
if cache_key in self._primed:
|
|
291
|
+
continue
|
|
292
|
+
for task in selected_tasks:
|
|
293
|
+
case = perturb_task(key, task, perturbation, seed)
|
|
294
|
+
prompt = prompt_for(
|
|
295
|
+
bench,
|
|
296
|
+
case.prompt_task,
|
|
297
|
+
self,
|
|
298
|
+
DEMO_MODELS[0]["name"],
|
|
299
|
+
)
|
|
300
|
+
solution = solutions.get(task.id) or task.metadata.get(
|
|
301
|
+
"canonical_solution"
|
|
302
|
+
)
|
|
303
|
+
self._answers[prompt] = (case.evaluation_task, solution)
|
|
304
|
+
self._primed.add(cache_key)
|
|
305
|
+
|
|
306
|
+
def list_models(self) -> list[dict]:
|
|
307
|
+
return [
|
|
308
|
+
{
|
|
309
|
+
"name": model["name"],
|
|
310
|
+
"size": model["size"],
|
|
311
|
+
"status": "demo",
|
|
312
|
+
"context_length": self.context_length(model["name"]),
|
|
313
|
+
}
|
|
314
|
+
for model in DEMO_MODELS
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
def max_parallel_requests(self, model: str) -> int:
|
|
318
|
+
"""Keep the deterministic offline simulation single-threaded."""
|
|
319
|
+
return 1
|
|
320
|
+
|
|
321
|
+
def context_length(self, _model: str) -> int:
|
|
322
|
+
"""Keep the offline demo representative without building huge prompts."""
|
|
323
|
+
return 8192
|
|
324
|
+
|
|
325
|
+
def unload_model(self, model: str) -> None:
|
|
326
|
+
time.sleep(0.05 / self.speed)
|
|
327
|
+
|
|
328
|
+
def force_unload_model(self, model: str) -> None:
|
|
329
|
+
self.unload_model(model)
|
|
330
|
+
|
|
331
|
+
def _profile(self, model: str) -> dict:
|
|
332
|
+
for entry in DEMO_MODELS:
|
|
333
|
+
if entry["name"] == model:
|
|
334
|
+
return entry
|
|
335
|
+
return DEMO_MODELS[-1]
|
|
336
|
+
|
|
337
|
+
def generate(
|
|
338
|
+
self,
|
|
339
|
+
model: str,
|
|
340
|
+
prompt: str,
|
|
341
|
+
on_progress: Callable[[GenerationUpdate], None] | None = None,
|
|
342
|
+
cancel_event: threading.Event | None = None,
|
|
343
|
+
images: list[str] | None = None,
|
|
344
|
+
) -> dict:
|
|
345
|
+
profile = self._profile(model)
|
|
346
|
+
entry = self._answers.get(prompt)
|
|
347
|
+
|
|
348
|
+
# Deterministic per model+prompt so repeat runs stay comparable.
|
|
349
|
+
digest = hashlib.sha256(f"{model}{prompt}".encode()).hexdigest()
|
|
350
|
+
roll = int(digest[:8], 16) / 0xFFFFFFFF
|
|
351
|
+
random.seed(digest)
|
|
352
|
+
|
|
353
|
+
if entry is None:
|
|
354
|
+
response = "A"
|
|
355
|
+
else:
|
|
356
|
+
task, solution = entry
|
|
357
|
+
correct = roll < profile["skill"]
|
|
358
|
+
response = (
|
|
359
|
+
_good_answer(task, solution)
|
|
360
|
+
if correct
|
|
361
|
+
else _bad_answer(task, solution is not None)
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
healthy_thinking = (
|
|
365
|
+
"I will identify the requested output, check the relevant facts, "
|
|
366
|
+
"compare the available choices, and then return the concise final "
|
|
367
|
+
"answer. The evidence points consistently toward one option. "
|
|
368
|
+
)
|
|
369
|
+
looping = roll > profile["skill"] + 0.12
|
|
370
|
+
thinking = (
|
|
371
|
+
(
|
|
372
|
+
"I should reconsider this carefully because the previous path "
|
|
373
|
+
"may be wrong, so I will inspect the same evidence once more. "
|
|
374
|
+
)
|
|
375
|
+
* 14
|
|
376
|
+
if looping
|
|
377
|
+
else healthy_thinking
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
tokens = max(40 + int(roll * 260), len((thinking + response).split()))
|
|
381
|
+
tok_s = profile["speed"] * (0.85 + roll * 0.3)
|
|
382
|
+
elapsed = tokens / tok_s
|
|
383
|
+
emitted_thinking = ""
|
|
384
|
+
|
|
385
|
+
def cancelled_result() -> dict:
|
|
386
|
+
return {
|
|
387
|
+
"thinking": emitted_thinking,
|
|
388
|
+
"response": "",
|
|
389
|
+
"trace_status": "observed" if emitted_thinking else "unavailable",
|
|
390
|
+
"tok_s": 0.0,
|
|
391
|
+
"eval_count": 0,
|
|
392
|
+
"eval_duration_ns": 0,
|
|
393
|
+
"response_time_s": 0.0,
|
|
394
|
+
"done_reason": "cancelled",
|
|
395
|
+
"timed_out": False,
|
|
396
|
+
"cancelled": True,
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
400
|
+
return cancelled_result()
|
|
401
|
+
if on_progress is not None:
|
|
402
|
+
chunks = [
|
|
403
|
+
thinking[index : index + 80] for index in range(0, len(thinking), 80)
|
|
404
|
+
]
|
|
405
|
+
for index, chunk in enumerate(chunks, start=1):
|
|
406
|
+
time.sleep(min(0.012 / self.speed, 0.03))
|
|
407
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
408
|
+
return cancelled_result()
|
|
409
|
+
emitted_thinking += chunk
|
|
410
|
+
on_progress(
|
|
411
|
+
GenerationUpdate(
|
|
412
|
+
thinking=chunk,
|
|
413
|
+
elapsed_s=elapsed * index / (len(chunks) + 1),
|
|
414
|
+
reasoning_channel_seen=True,
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
on_progress(
|
|
418
|
+
GenerationUpdate(
|
|
419
|
+
response=response,
|
|
420
|
+
elapsed_s=elapsed,
|
|
421
|
+
reasoning_channel_seen=True,
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
on_progress(
|
|
425
|
+
GenerationUpdate(
|
|
426
|
+
elapsed_s=elapsed,
|
|
427
|
+
reasoning_channel_seen=True,
|
|
428
|
+
done=True,
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
else:
|
|
432
|
+
time.sleep(min(elapsed / (40 * self.speed), 0.35))
|
|
433
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
434
|
+
return cancelled_result()
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
"thinking": thinking,
|
|
438
|
+
"response": response,
|
|
439
|
+
"trace_status": "observed",
|
|
440
|
+
"tok_s": tok_s,
|
|
441
|
+
"eval_count": tokens,
|
|
442
|
+
"eval_duration_ns": int(elapsed * 1e9),
|
|
443
|
+
"response_time_s": elapsed,
|
|
444
|
+
"done_reason": "stop",
|
|
445
|
+
"timed_out": False,
|
|
446
|
+
"cancelled": False,
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def is_demo_client(client: object) -> bool:
|
|
451
|
+
return isinstance(client, DemoClient)
|
benchkit/dockerfiles.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""What each sandbox image contains.
|
|
2
|
+
|
|
3
|
+
The Dockerfiles for the Pi harness (stock, Aider Polyglot toolchains, Git
|
|
4
|
+
Surgery, PatchEval task runtimes) and the mc-arena script runner. Nothing here
|
|
5
|
+
talks to Docker: :mod:`benchkit.sandbox` builds, runs and removes the images.
|
|
6
|
+
Every tool and base image is pinned, so a change here changes what a run
|
|
7
|
+
measures.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
AIDER_POLYGLOT_COMMIT = "7e0611e77b54e2dea774cdc0aa00cf9f7ed6144f"
|
|
17
|
+
_UV_CACHE_DIR = "/root/.cache/uv"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_PI_INSTALL = """\
|
|
21
|
+
COPY pi-package/package.json pi-package/package-lock.json /opt/benchkit/pi/
|
|
22
|
+
RUN cd /opt/benchkit/pi \\
|
|
23
|
+
&& npm ci --omit=dev \\
|
|
24
|
+
&& ln -s /opt/benchkit/pi/node_modules/.bin/pi /usr/local/bin/pi
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
PI_DOCKERFILE = f"""\
|
|
28
|
+
FROM node:24-bookworm-slim
|
|
29
|
+
|
|
30
|
+
RUN apt-get update \\
|
|
31
|
+
&& apt-get install -y --no-install-recommends bash ca-certificates git python3 ripgrep \\
|
|
32
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
33
|
+
{_PI_INSTALL}
|
|
34
|
+
|
|
35
|
+
COPY inference_proxy.py /opt/benchkit/inference_proxy.py
|
|
36
|
+
COPY answer_key_guard.ts /opt/benchkit/answer_key_guard.ts
|
|
37
|
+
RUN mkdir -p /workspace /home/node/.pi/agent \\
|
|
38
|
+
&& chown -R node:node /workspace /home/node/.pi
|
|
39
|
+
|
|
40
|
+
USER node
|
|
41
|
+
WORKDIR /workspace
|
|
42
|
+
ENV PI_OFFLINE=1 PI_TELEMETRY=0
|
|
43
|
+
CMD ["sleep", "infinity"]
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
AIDER_PI_DOCKERFILE = f"""\
|
|
47
|
+
FROM node:24-bookworm
|
|
48
|
+
|
|
49
|
+
RUN apt-get update \\
|
|
50
|
+
&& apt-get install -y --no-install-recommends \\
|
|
51
|
+
bash ca-certificates cmake curl g++ git libboost-date-time-dev \\
|
|
52
|
+
openjdk-17-jdk \\
|
|
53
|
+
python3 ripgrep unzip \\
|
|
54
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
55
|
+
{_PI_INSTALL}
|
|
56
|
+
ARG TARGETARCH
|
|
57
|
+
RUN curl -fsSL "https://go.dev/dl/go1.21.5.linux-${{TARGETARCH}}.tar.gz" \\
|
|
58
|
+
-o /tmp/go.tar.gz \\
|
|
59
|
+
&& tar -C /usr/local -xzf /tmp/go.tar.gz \\
|
|
60
|
+
&& rm /tmp/go.tar.gz
|
|
61
|
+
ENV PATH="/usr/local/go/bin:$PATH" RUSTUP_HOME="/opt/rustup" \\
|
|
62
|
+
CARGO_HOME="/opt/cargo-home"
|
|
63
|
+
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \\
|
|
64
|
+
| sh -s -- -y --no-modify-path
|
|
65
|
+
RUN mkdir -p /opt/javascript-deps \\
|
|
66
|
+
&& cd /opt/javascript-deps \\
|
|
67
|
+
&& npm init -y \\
|
|
68
|
+
&& npm install jest@29.7.0 @babel/core@7.25.2 \\
|
|
69
|
+
@exercism/babel-preset-javascript@0.2.1 \\
|
|
70
|
+
@exercism/eslint-config-javascript@0.6.0 @types/jest@29.5.12 \\
|
|
71
|
+
@types/node@20.12.12 babel-jest@29.6.4 core-js@3.37.1 eslint@8.49.0
|
|
72
|
+
RUN curl -fsSL https://services.gradle.org/distributions/gradle-8.10.2-bin.zip \\
|
|
73
|
+
-o /tmp/gradle.zip \\
|
|
74
|
+
&& unzip -q /tmp/gradle.zip -d /opt \\
|
|
75
|
+
&& rm /tmp/gradle.zip
|
|
76
|
+
ENV PATH="/opt/gradle-8.10.2/bin:/opt/javascript-deps/node_modules/.bin:/opt/cargo-home/bin:$PATH" \\
|
|
77
|
+
NODE_PATH="/opt/javascript-deps/node_modules" \\
|
|
78
|
+
GOPATH="/opt/go" GRADLE_USER_HOME="/opt/gradle-cache"
|
|
79
|
+
RUN git clone https://github.com/Aider-AI/polyglot-benchmark.git \\
|
|
80
|
+
/opt/aider-polyglot \\
|
|
81
|
+
&& git -C /opt/aider-polyglot checkout {AIDER_POLYGLOT_COMMIT} \\
|
|
82
|
+
&& find /opt/aider-polyglot -name Cargo.toml -execdir cargo fetch \\; \\
|
|
83
|
+
&& find /opt/aider-polyglot/rust/exercises/practice \\
|
|
84
|
+
-name Cargo-example.toml -exec sh -c '\
|
|
85
|
+
for manifest do \
|
|
86
|
+
cache_dir=$(mktemp -d); \
|
|
87
|
+
mkdir "$cache_dir/src"; \
|
|
88
|
+
cp "$manifest" "$cache_dir/Cargo.toml"; \
|
|
89
|
+
cp "$(dirname "$manifest")/example.rs" "$cache_dir/src/lib.rs"; \
|
|
90
|
+
cargo fetch --manifest-path "$cache_dir/Cargo.toml"; \
|
|
91
|
+
rm -rf "$cache_dir"; \
|
|
92
|
+
done' sh {{}} + \\
|
|
93
|
+
&& find /opt/aider-polyglot/go/exercises/practice -name go.mod \\
|
|
94
|
+
-execdir go mod download \\; \\
|
|
95
|
+
&& find /opt/aider-polyglot/java/exercises/practice -name build.gradle \\
|
|
96
|
+
-execdir gradle test --test-dry-run --no-daemon \\;
|
|
97
|
+
RUN find /opt/aider-polyglot -depth \\
|
|
98
|
+
\\( -name .meta -o -name .approaches \\
|
|
99
|
+
-o -iname '*example*' -o -iname '*reference*' -o -iname '*proof*' \\) \\
|
|
100
|
+
-exec rm -rf {{}} + \\
|
|
101
|
+
&& rm -rf /opt/aider-polyglot/.git
|
|
102
|
+
|
|
103
|
+
COPY inference_proxy.py /opt/benchkit/inference_proxy.py
|
|
104
|
+
COPY answer_key_guard.ts /opt/benchkit/answer_key_guard.ts
|
|
105
|
+
RUN mkdir -p /workspace /home/node/.pi/agent /opt/go \\
|
|
106
|
+
&& chown -R node:node /workspace /home/node/.pi /opt/cargo-home \\
|
|
107
|
+
/opt/rustup /opt/go /opt/gradle-cache \\
|
|
108
|
+
&& chmod -R a+rX /opt/aider-polyglot
|
|
109
|
+
|
|
110
|
+
USER node
|
|
111
|
+
WORKDIR /workspace
|
|
112
|
+
ENV PI_OFFLINE=1 PI_TELEMETRY=0 CARGO_NET_OFFLINE=true
|
|
113
|
+
CMD ["sleep", "infinity"]
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
GIT_SURGERY_PI_DOCKERFILE = f"""\
|
|
117
|
+
FROM node:24-bookworm-slim
|
|
118
|
+
|
|
119
|
+
ARG GIT_DEBIAN_VERSION=1:2.39.5-0+deb12u3
|
|
120
|
+
RUN apt-get update \\
|
|
121
|
+
&& apt-get install -y --no-install-recommends \\
|
|
122
|
+
bash ca-certificates git=${{GIT_DEBIAN_VERSION}} python3 ripgrep \\
|
|
123
|
+
&& test "$(git --version)" = "git version 2.39.5" \\
|
|
124
|
+
&& git config --system protocol.file.allow always \\
|
|
125
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
126
|
+
{_PI_INSTALL}
|
|
127
|
+
|
|
128
|
+
COPY inference_proxy.py /opt/benchkit/inference_proxy.py
|
|
129
|
+
COPY answer_key_guard.ts /opt/benchkit/answer_key_guard.ts
|
|
130
|
+
COPY git-surgery /opt/git-surgery
|
|
131
|
+
RUN chmod +x /opt/git-surgery/*/setup.sh /opt/git-surgery/*/verify.sh \\
|
|
132
|
+
&& mkdir -p /workspace /home/node/.pi/agent \\
|
|
133
|
+
&& chown -R node:node /workspace /home/node/.pi
|
|
134
|
+
|
|
135
|
+
USER node
|
|
136
|
+
WORKDIR /workspace
|
|
137
|
+
ENV PI_OFFLINE=1 PI_TELEMETRY=0
|
|
138
|
+
CMD ["sleep", "infinity"]
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# mc-arena runs one untrusted, model-written Python script per task. The script
|
|
143
|
+
# only has to print JSON, so the image is a stock Python plus uv: the suite's
|
|
144
|
+
# answer contract is a PEP 723 script, and uv is what runs those.
|
|
145
|
+
MC_ARENA_UV_VERSION = "0.8.17"
|
|
146
|
+
MC_ARENA_DEFAULT_BASE = "python:3.12-slim"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _mc_arena_base_image() -> str:
|
|
150
|
+
"""The Python base image, overridable for hosts behind a registry mirror."""
|
|
151
|
+
return (
|
|
152
|
+
os.environ.get("BENCHKIT_MC_ARENA_BASE_IMAGE") or MC_ARENA_DEFAULT_BASE
|
|
153
|
+
).strip()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def mc_arena_dockerfile() -> str:
|
|
157
|
+
return f"""\
|
|
158
|
+
FROM {_mc_arena_base_image()}
|
|
159
|
+
|
|
160
|
+
RUN pip install --no-cache-dir uv=={MC_ARENA_UV_VERSION} \\
|
|
161
|
+
&& useradd --create-home --uid 1000 runner \\
|
|
162
|
+
&& mkdir -p /work && chown runner:runner /work
|
|
163
|
+
|
|
164
|
+
USER runner
|
|
165
|
+
WORKDIR /work
|
|
166
|
+
ENV HOME=/home/runner UV_NO_PROGRESS=1 UV_OFFLINE=1
|
|
167
|
+
CMD ["sleep", "infinity"]
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class PatchEvalRuntimeRecipe:
|
|
173
|
+
"""Trusted build instructions shipped with one validated PatchEval task."""
|
|
174
|
+
|
|
175
|
+
base_image: str
|
|
176
|
+
sync_command: tuple[str, ...]
|
|
177
|
+
bootstrap_command: tuple[str, ...] = ()
|
|
178
|
+
environment: tuple[str, ...] = ()
|
|
179
|
+
schema_version: int = 1
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _docker_run_cached(command: tuple[str, ...], uv_cache_id: str) -> str:
|
|
183
|
+
"""Run a build command with the run's shared, private uv cache mounted."""
|
|
184
|
+
mount = f"--mount=type=cache,target={_UV_CACHE_DIR},id={uv_cache_id},sharing=locked"
|
|
185
|
+
return f"RUN {mount} {json.dumps(list(command), separators=(',', ':'))}\n"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def patcheval_dockerfile(recipe: PatchEvalRuntimeRecipe, uv_cache_id: str) -> str:
|
|
189
|
+
"""Generate the three-stage task runtime.
|
|
190
|
+
|
|
191
|
+
The stages are split by what actually changes between tasks:
|
|
192
|
+
|
|
193
|
+
1. ``benchkit-pi-assets`` depends only on the base image and the pinned Pi
|
|
194
|
+
assets, so the run's shared builder builds it once.
|
|
195
|
+
2. ``benchkit-runtime`` adds the parent source and the frozen sync and
|
|
196
|
+
bootstrap commands, and reuses the run's uv cache.
|
|
197
|
+
3. the final stage only adds task environment and the agent user.
|
|
198
|
+
"""
|
|
199
|
+
bootstrap = (
|
|
200
|
+
_docker_run_cached(recipe.bootstrap_command, uv_cache_id)
|
|
201
|
+
if recipe.bootstrap_command
|
|
202
|
+
else ""
|
|
203
|
+
)
|
|
204
|
+
environment = "".join(
|
|
205
|
+
f"ENV {key}={json.dumps(value)}\n"
|
|
206
|
+
for key, value in (item.split("=", 1) for item in recipe.environment)
|
|
207
|
+
)
|
|
208
|
+
return f"""\
|
|
209
|
+
FROM node:24-bookworm-slim AS benchkit-node
|
|
210
|
+
|
|
211
|
+
FROM {recipe.base_image} AS benchkit-pi-assets
|
|
212
|
+
|
|
213
|
+
USER root
|
|
214
|
+
COPY --from=benchkit-node /usr/local/ /usr/local/
|
|
215
|
+
RUN apt-get update \\
|
|
216
|
+
&& apt-get install -y --no-install-recommends \\
|
|
217
|
+
bash ca-certificates coreutils git passwd ripgrep tar \\
|
|
218
|
+
&& rm -rf /var/lib/apt/lists/* \\
|
|
219
|
+
&& (getent group node >/dev/null || groupadd --gid 1000 node) \\
|
|
220
|
+
&& (id --user node >/dev/null 2>&1 || \\
|
|
221
|
+
useradd --uid 1000 --gid node --create-home --shell /bin/bash node)
|
|
222
|
+
{_PI_INSTALL}
|
|
223
|
+
COPY inference_proxy.py /opt/benchkit/inference_proxy.py
|
|
224
|
+
COPY answer_key_guard.ts /opt/benchkit/answer_key_guard.ts
|
|
225
|
+
RUN mkdir -p /workspace /home/node/.pi/agent \\
|
|
226
|
+
&& chown -R node:node /workspace /home/node/.pi
|
|
227
|
+
|
|
228
|
+
FROM benchkit-pi-assets AS benchkit-runtime
|
|
229
|
+
|
|
230
|
+
ENV UV_PROJECT_ENVIRONMENT=/opt/venv UV_LINK_MODE=copy
|
|
231
|
+
WORKDIR /opt/project
|
|
232
|
+
COPY parent-source.tar /tmp/patcheval-parent.tar
|
|
233
|
+
RUN tar -xf /tmp/patcheval-parent.tar -C /opt/project \\
|
|
234
|
+
&& find /opt/project -exec touch -t 198001010000 {{}} + \\
|
|
235
|
+
&& rm /tmp/patcheval-parent.tar
|
|
236
|
+
{_docker_run_cached(recipe.sync_command, uv_cache_id)}{bootstrap}RUN rm -rf /opt/project \\
|
|
237
|
+
&& test -d /opt/venv \\
|
|
238
|
+
&& chmod -R a+rwX /opt/venv
|
|
239
|
+
|
|
240
|
+
FROM benchkit-runtime
|
|
241
|
+
|
|
242
|
+
{environment}USER node
|
|
243
|
+
WORKDIR /workspace
|
|
244
|
+
ENV PATH=/opt/venv/bin:$PATH HOME=/home/node USER=node LOGNAME=node \\
|
|
245
|
+
PI_OFFLINE=1 PI_TELEMETRY=0
|
|
246
|
+
CMD ["sleep", "infinity"]
|
|
247
|
+
"""
|