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,103 @@
1
+ """HumanEval benchmark - 164 code generation tasks, scored by pass@1."""
2
+
3
+ import json
4
+ import textwrap
5
+ from pathlib import Path
6
+
7
+ from benchkit.benchmarks.base import Task
8
+ from benchkit.benchmarks.utils import strip_think_tags
9
+ from benchkit.evaluation import EvaluationResult
10
+ from benchkit.executor import execute_with_feedback
11
+
12
+ DATASET = Path(__file__).parent.parent / "datasets" / "humaneval.jsonl"
13
+
14
+ SYSTEM = (
15
+ "Complete the following Python function. "
16
+ "Output ONLY the function body. "
17
+ "No markdown fences, no explanation, no examples."
18
+ )
19
+
20
+
21
+ def _extract_code(response: str) -> str:
22
+ """Strip reasoning and a single Markdown code fence."""
23
+ text = strip_think_tags(response).rstrip()
24
+
25
+ if "```python" in text:
26
+ text = text.split("```python", 1)[1].split("```", 1)[0]
27
+ elif "```" in text:
28
+ text = text.split("```", 1)[1].split("```", 1)[0]
29
+
30
+ return text.strip("\n")
31
+
32
+
33
+ def _assemble_solution(prompt: str, entry: str, code: str) -> str:
34
+ """Join a completion onto the HumanEval prompt.
35
+
36
+ Bare bodies are indented as a function body. If the first line is flush
37
+ and the rest is already indented, indenting every line over-indents the
38
+ body, so recover by indenting only that first line.
39
+ """
40
+ if f"def {entry}" in code:
41
+ imports = [
42
+ line for line in prompt.split("\n") if line.startswith(("import ", "from "))
43
+ ]
44
+ return "\n".join(imports) + "\n\n" + code if imports else code
45
+
46
+ normalized = textwrap.dedent(code)
47
+ solution = prompt + textwrap.indent(normalized, " ")
48
+ try:
49
+ compile(solution, "<benchkit-humaneval>", "exec")
50
+ return solution
51
+ except (IndentationError, TabError):
52
+ pass
53
+
54
+ lines = code.splitlines()
55
+ first_index = next(
56
+ (index for index, line in enumerate(lines) if line.strip()), None
57
+ )
58
+ if first_index is not None and not lines[first_index].startswith((" ", "\t")):
59
+ lines[first_index] = " " + lines[first_index]
60
+ normalized = textwrap.dedent("\n".join(lines))
61
+ repaired = prompt + textwrap.indent(normalized, " ")
62
+ try:
63
+ compile(repaired, "<benchkit-humaneval>", "exec")
64
+ except (IndentationError, TabError):
65
+ return solution
66
+ return repaired
67
+ return solution
68
+
69
+
70
+ class HumanEval:
71
+ name = "humaneval"
72
+ evaluation_activity = "executing code and running tests"
73
+
74
+ def load_tasks(self) -> list[Task]:
75
+ tasks = []
76
+ with open(DATASET) as f:
77
+ for line in f:
78
+ d = json.loads(line)
79
+ tasks.append(
80
+ Task(
81
+ id=d["task_id"],
82
+ prompt=d["prompt"],
83
+ metadata={
84
+ "test": d["test"],
85
+ "entry_point": d["entry_point"],
86
+ },
87
+ )
88
+ )
89
+ return tasks
90
+
91
+ def build_prompt(self, task: Task) -> str:
92
+ return f"{SYSTEM}\n\n{task.prompt}"
93
+
94
+ def evaluate(self, task: Task, response: str) -> bool:
95
+ return self.evaluate_with_feedback(task, response).passed
96
+
97
+ def evaluate_with_feedback(self, task: Task, response: str) -> EvaluationResult:
98
+ code = _extract_code(response)
99
+ entry = task.metadata["entry_point"]
100
+ fn_code = _assemble_solution(task.prompt, entry, code)
101
+ full = fn_code + "\n\n" + task.metadata["test"] + f"\ncheck({entry})\n"
102
+ result = execute_with_feedback(full)
103
+ return EvaluationResult(float(result.passed), result.feedback)
@@ -0,0 +1,56 @@
1
+ """IFEval benchmark - verifiable instruction following.
2
+
3
+ 541 prompts, each carrying one or more instructions that can be checked with
4
+ code ("write at least 300 words", "no commas", "wrap the title in <<>>").
5
+ A task counts as passed only when every instruction attached to it is
6
+ followed, which is the strict prompt-level accuracy reported in the paper.
7
+ """
8
+
9
+ import json
10
+ from pathlib import Path
11
+
12
+ from benchkit.benchmarks.base import Task
13
+ from benchkit.benchmarks.ifeval_instructions import follows_instruction
14
+ from benchkit.benchmarks.utils import strip_think_tags
15
+
16
+ DATASET = Path(__file__).parent.parent / "datasets" / "ifeval.jsonl"
17
+
18
+
19
+ class IFEval:
20
+ name = "ifeval"
21
+
22
+ def load_tasks(self) -> list[Task]:
23
+ tasks = []
24
+ with open(DATASET, encoding="utf-8") as f:
25
+ for line in f:
26
+ d = json.loads(line)
27
+ tasks.append(
28
+ Task(
29
+ id=d["task_id"],
30
+ prompt=d["prompt"],
31
+ metadata={
32
+ "instruction_id_list": d["instruction_id_list"],
33
+ "kwargs": d["kwargs"],
34
+ },
35
+ )
36
+ )
37
+ return tasks
38
+
39
+ def build_prompt(self, task: Task) -> str:
40
+ # IFEval prompts are self-contained: every constraint is spelled out in
41
+ # the prompt itself, so no extra system framing is added.
42
+ return task.prompt
43
+
44
+ def evaluate(self, task: Task, response: str) -> bool:
45
+ text = strip_think_tags(response)
46
+ instructions = task.metadata["instruction_id_list"]
47
+ kwargs_list = task.metadata["kwargs"]
48
+
49
+ for instruction_id, kwargs in zip(instructions, kwargs_list, strict=False):
50
+ args = dict(kwargs or {})
51
+ if instruction_id == "combination:repeat_prompt":
52
+ args.setdefault("prompt_to_repeat", task.prompt)
53
+ if not follows_instruction(instruction_id, text, args):
54
+ return False
55
+
56
+ return True
@@ -0,0 +1,477 @@
1
+ """Verifiable instruction checkers used by the IFEval benchmark.
2
+
3
+ Each checker mirrors the reference implementation shipped with the IFEval
4
+ paper (google-research/instruction_following_eval). The reference code leans on
5
+ NLTK for tokenization and langdetect for `language:response_language`; both are
6
+ reimplemented here with small dependency-free equivalents so BenchKit stays
7
+ installable without extra model or corpus downloads.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+ import unicodedata
15
+ from collections.abc import Callable
16
+
17
+ Checker = Callable[[str, dict], bool]
18
+
19
+ _COMPARISONS = {
20
+ "less than": lambda value, target: value < target,
21
+ "at least": lambda value, target: value >= target,
22
+ }
23
+
24
+ _CONSTRAINED_RESPONSES = (
25
+ "my answer is yes.",
26
+ "my answer is no.",
27
+ "my answer is maybe.",
28
+ )
29
+
30
+ _ABBREVIATIONS = {
31
+ "mr",
32
+ "mrs",
33
+ "ms",
34
+ "dr",
35
+ "prof",
36
+ "sr",
37
+ "jr",
38
+ "st",
39
+ "vs",
40
+ "etc",
41
+ "e.g",
42
+ "i.e",
43
+ "fig",
44
+ "no",
45
+ "inc",
46
+ "ltd",
47
+ "co",
48
+ "approx",
49
+ "u.s",
50
+ "u.k",
51
+ }
52
+
53
+
54
+ def _compare(value: int, relation: str, target: int) -> bool:
55
+ compare = _COMPARISONS.get(str(relation).strip().lower())
56
+ if compare is None:
57
+ raise ValueError(f"unsupported relation: {relation!r}")
58
+ return compare(value, target)
59
+
60
+
61
+ # Tokenization ---------------------------------------------------------
62
+
63
+
64
+ def count_words(text: str) -> int:
65
+ """Count word tokens the way the reference tokenizer (`\\w+`) does."""
66
+ return len(re.findall(r"\w+", text, flags=re.UNICODE))
67
+
68
+
69
+ def split_sentences(text: str) -> list[str]:
70
+ """Split text into sentences with a small abbreviation-aware splitter."""
71
+ sentences: list[str] = []
72
+ current: list[str] = []
73
+
74
+ for chunk in re.split(r"(?<=[.!?])[\"')\]]*\s+", text.strip()):
75
+ if not chunk:
76
+ continue
77
+ current.append(chunk)
78
+ tail = chunk.rstrip("\"')]").rstrip()
79
+ last_word = re.split(r"[\s]", tail)[-1].rstrip(".").lower()
80
+ ends_sentence = tail.endswith((".", "!", "?"))
81
+ if ends_sentence and last_word not in _ABBREVIATIONS:
82
+ sentences.append(" ".join(current))
83
+ current = []
84
+
85
+ if current:
86
+ sentences.append(" ".join(current))
87
+
88
+ return [sentence for sentence in sentences if sentence.strip()]
89
+
90
+
91
+ def count_sentences(text: str) -> int:
92
+ return len(split_sentences(text))
93
+
94
+
95
+ # Language detection ---------------------------------------------------
96
+
97
+ _SCRIPT_RANGES: list[tuple[int, int, str]] = [
98
+ (0x0590, 0x05FF, "he"),
99
+ (0x0600, 0x06FF, "arabic"),
100
+ (0x0750, 0x077F, "arabic"),
101
+ (0x0900, 0x097F, "devanagari"),
102
+ (0x0980, 0x09FF, "bn"),
103
+ (0x0A00, 0x0A7F, "pa"),
104
+ (0x0A80, 0x0AFF, "gu"),
105
+ (0x0B00, 0x0B7F, "or"),
106
+ (0x0B80, 0x0BFF, "ta"),
107
+ (0x0C00, 0x0C7F, "te"),
108
+ (0x0C80, 0x0CFF, "kn"),
109
+ (0x0D00, 0x0D7F, "ml"),
110
+ (0x0D80, 0x0DFF, "si"),
111
+ (0x0E00, 0x0E7F, "th"),
112
+ (0x0400, 0x04FF, "cyrillic"),
113
+ (0x0370, 0x03FF, "el"),
114
+ (0x1100, 0x11FF, "ko"),
115
+ (0x3130, 0x318F, "ko"),
116
+ (0xAC00, 0xD7AF, "ko"),
117
+ (0x3040, 0x30FF, "ja"),
118
+ (0x4E00, 0x9FFF, "zh"),
119
+ ]
120
+
121
+ # fmt: off
122
+ _LATIN_STOPWORDS: dict[str, set[str]] = {
123
+ "en": {
124
+ "the", "and", "that", "this", "with", "for", "you", "are", "was",
125
+ "have", "not", "but", "they", "from", "your", "will", "can", "there",
126
+ "what", "which", "about", "would", "should", "here",
127
+ },
128
+ "de": {
129
+ "der", "die", "das", "und", "ist", "nicht", "ich", "ein", "eine",
130
+ "mit", "für", "sich", "auch", "auf", "dass", "sie", "wir", "aber",
131
+ },
132
+ "it": {
133
+ "il", "la", "di", "che", "non", "per", "una", "sono", "con", "del",
134
+ "gli", "come", "più", "anche", "questo", "essere",
135
+ },
136
+ "pt": {
137
+ "de", "que", "não", "para", "uma", "com", "os", "as", "do", "da",
138
+ "mais", "por", "como", "está", "você", "são",
139
+ },
140
+ "es": {
141
+ "el", "los", "las", "por", "con", "para", "una", "pero", "más",
142
+ "está", "son", "este", "muy",
143
+ },
144
+ "fr": {
145
+ "le", "les", "des", "une", "est", "pour", "dans", "pas", "avec",
146
+ "sur", "vous", "nous", "plus", "cette",
147
+ },
148
+ "fi": {
149
+ "ja", "on", "ei", "että", "se", "olla", "joka", "kuin", "myös",
150
+ "mutta", "kun", "hän", "tai", "ovat",
151
+ },
152
+ "sw": {
153
+ "na", "ya", "kwa", "ni", "wa", "katika", "za", "la", "hii", "kuwa",
154
+ "yake", "hata", "wale", "ambayo",
155
+ },
156
+ "nl": {"het", "een", "van", "en", "is", "niet", "dat", "op", "met", "zijn"},
157
+ }
158
+ # fmt: on
159
+
160
+ _VIETNAMESE_MARKS = set("ăâđêôơưạảấầẩẫậắằẳẵặẹẻẽếềểễệỉịọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹ")
161
+
162
+ _DEVANAGARI_HINTS = {
163
+ "mr": {"आहे", "आणि", "मध्ये", "यांनी", "त्या", "करण्यात", "नाही", "होते"},
164
+ "ne": {"छ", "हो", "गर्न", "भएको", "लागि", "पनि", "गरेको", "थियो"},
165
+ "hi": {"है", "हैं", "और", "के", "में", "का", "की", "को", "यह", "नहीं"},
166
+ }
167
+
168
+ _ARABIC_HINTS = {
169
+ "ur": {"ہے", "کے", "میں", "اور", "کی", "کا", "نہیں", "ہیں"},
170
+ "fa": {"است", "را", "این", "که", "می", "برای", "با", "های"},
171
+ "ar": {"في", "من", "على", "التي", "هذا", "أن", "إلى", "مع"},
172
+ }
173
+
174
+ _CYRILLIC_HINTS = {
175
+ "bg": {"на", "се", "да", "който", "като", "със", "това", "са"},
176
+ "ru": {"это", "что", "как", "для", "они", "был", "она", "или"},
177
+ "uk": {"це", "що", "як", "для", "він", "вона", "або", "той"},
178
+ }
179
+
180
+
181
+ def _words(text: str) -> list[str]:
182
+ return re.findall(r"[^\W\d_]+", text.lower(), flags=re.UNICODE)
183
+
184
+
185
+ def _best_hint(text: str, hints: dict[str, set[str]], default: str) -> str:
186
+ words = set(_words(text))
187
+ scores = {code: len(words & markers) for code, markers in hints.items()}
188
+ best = max(scores, key=lambda code: scores[code])
189
+ return best if scores[best] else default
190
+
191
+
192
+ def _dominant_script(text: str) -> str | None:
193
+ counts: dict[str, int] = {}
194
+ latin = 0
195
+
196
+ for char in text:
197
+ if not char.isalpha():
198
+ continue
199
+ code = ord(char)
200
+ if code < 0x0250 or 0x1E00 <= code <= 0x1EFF:
201
+ latin += 1
202
+ continue
203
+ for start, end, script in _SCRIPT_RANGES:
204
+ if start <= code <= end:
205
+ counts[script] = counts.get(script, 0) + 1
206
+ break
207
+
208
+ if counts:
209
+ script = max(counts, key=lambda key: counts[key])
210
+ if counts[script] >= max(latin, 1):
211
+ return script
212
+ return "latin" if latin else None
213
+
214
+
215
+ def detect_language(text: str) -> str | None:
216
+ """Best-effort ISO 639-1 language code for a response.
217
+
218
+ Non-Latin scripts are identified by Unicode block and then narrowed with
219
+ stopword hints where a block is shared (Devanagari, Arabic, Cyrillic).
220
+ Latin-script languages are scored purely on stopword overlap.
221
+ """
222
+ text = text.strip()
223
+ if not text:
224
+ return None
225
+
226
+ script = _dominant_script(text)
227
+ if script is None:
228
+ return None
229
+ if script == "devanagari":
230
+ return _best_hint(text, _DEVANAGARI_HINTS, "hi")
231
+ if script == "arabic":
232
+ return _best_hint(text, _ARABIC_HINTS, "ar")
233
+ if script == "cyrillic":
234
+ return _best_hint(text, _CYRILLIC_HINTS, "ru")
235
+ if script != "latin":
236
+ return script
237
+
238
+ lowered = unicodedata.normalize("NFC", text.lower())
239
+ if any(char in _VIETNAMESE_MARKS for char in lowered):
240
+ return "vi"
241
+
242
+ words = set(_words(text))
243
+ scores = {code: len(words & markers) for code, markers in _LATIN_STOPWORDS.items()}
244
+ best = max(scores, key=lambda code: scores[code])
245
+ return best if scores[best] else "en"
246
+
247
+
248
+ # Instruction checkers -------------------------------------------------
249
+
250
+
251
+ def _keywords_existence(response: str, kwargs: dict) -> bool:
252
+ lowered = response.lower()
253
+ return all(keyword.lower() in lowered for keyword in kwargs["keywords"])
254
+
255
+
256
+ def _keywords_frequency(response: str, kwargs: dict) -> bool:
257
+ found = len(re.findall(re.escape(kwargs["keyword"]), response, flags=re.IGNORECASE))
258
+ return _compare(found, kwargs["relation"], int(kwargs["frequency"]))
259
+
260
+
261
+ def _keywords_forbidden(response: str, kwargs: dict) -> bool:
262
+ return not any(
263
+ re.search(rf"\b{re.escape(word)}\b", response, flags=re.IGNORECASE)
264
+ for word in kwargs["forbidden_words"]
265
+ )
266
+
267
+
268
+ def _letter_frequency(response: str, kwargs: dict) -> bool:
269
+ letter = kwargs["letter"].lower()
270
+ found = response.lower().count(letter)
271
+ return _compare(found, kwargs["let_relation"], int(kwargs["let_frequency"]))
272
+
273
+
274
+ def _response_language(response: str, kwargs: dict) -> bool:
275
+ return detect_language(response) == kwargs["language"]
276
+
277
+
278
+ def _number_sentences(response: str, kwargs: dict) -> bool:
279
+ return _compare(
280
+ count_sentences(response), kwargs["relation"], int(kwargs["num_sentences"])
281
+ )
282
+
283
+
284
+ def _number_words(response: str, kwargs: dict) -> bool:
285
+ return _compare(count_words(response), kwargs["relation"], int(kwargs["num_words"]))
286
+
287
+
288
+ def _number_paragraphs(response: str, kwargs: dict) -> bool:
289
+ paragraphs = re.split(r"\s?\*\*\*\s?", response)
290
+ num_paragraphs = int(kwargs["num_paragraphs"])
291
+
292
+ for index, paragraph in enumerate(paragraphs):
293
+ if paragraph.strip():
294
+ continue
295
+ # A leading or trailing empty chunk just means the divider sat at the
296
+ # very start or end of the response; anything else is a real miss.
297
+ if index in (0, len(paragraphs) - 1):
298
+ num_paragraphs += 1
299
+ else:
300
+ return False
301
+
302
+ return len(paragraphs) == num_paragraphs
303
+
304
+
305
+ def _nth_paragraph_first_word(response: str, kwargs: dict) -> bool:
306
+ num_paragraphs = int(kwargs["num_paragraphs"])
307
+ nth = int(kwargs["nth_paragraph"])
308
+ first_word = kwargs["first_word"].lower()
309
+
310
+ paragraphs = [
311
+ paragraph.strip()
312
+ for paragraph in re.split(r"\n\n", response)
313
+ if paragraph.strip()
314
+ ]
315
+ if len(paragraphs) != num_paragraphs or nth > len(paragraphs) or nth <= 0:
316
+ return False
317
+
318
+ paragraph = paragraphs[nth - 1].strip()
319
+ words = re.findall(r"[\w']+", paragraph)
320
+ return bool(words) and words[0].lower() == first_word
321
+
322
+
323
+ def _number_placeholders(response: str, kwargs: dict) -> bool:
324
+ placeholders = re.findall(r"\[.*?\]", response)
325
+ return len(placeholders) >= int(kwargs["num_placeholders"])
326
+
327
+
328
+ def _postscript(response: str, kwargs: dict) -> bool:
329
+ marker = kwargs["postscript_marker"].strip().lower()
330
+ if marker == "p.p.s":
331
+ pattern = r"\s*p\.\s?p\.\s?s.*$"
332
+ elif marker == "p.s.":
333
+ pattern = r"\s*p\.\s?s\..*$"
334
+ else:
335
+ pattern = r"\s*" + re.escape(marker) + r".*$"
336
+ return bool(re.search(pattern, response.lower(), flags=re.MULTILINE))
337
+
338
+
339
+ def _number_bullets(response: str, kwargs: dict) -> bool:
340
+ bullets = re.findall(r"^\s*\*[^\*].*$", response, flags=re.MULTILINE)
341
+ bullets += re.findall(r"^\s*-.*$", response, flags=re.MULTILINE)
342
+ return len(bullets) == int(kwargs["num_bullets"])
343
+
344
+
345
+ def _constrained_response(response: str, _kwargs: dict) -> bool:
346
+ lowered = response.strip().lower()
347
+ return any(option in lowered for option in _CONSTRAINED_RESPONSES)
348
+
349
+
350
+ def _number_highlighted_sections(response: str, kwargs: dict) -> bool:
351
+ count = 0
352
+ for match in re.findall(r"\*[^\n\*]*\*", response):
353
+ if match.strip("*").strip():
354
+ count += 1
355
+ for match in re.findall(r"\*\*[^\n\*]*\*\*", response):
356
+ if match.strip("*").strip():
357
+ count += 1
358
+ return count >= int(kwargs["num_highlights"])
359
+
360
+
361
+ def _multiple_sections(response: str, kwargs: dict) -> bool:
362
+ splitter = re.escape(kwargs["section_spliter"])
363
+ sections = re.split(rf"\s?{splitter}\s?\d+\s?", response)
364
+ return len(sections) - 1 >= int(kwargs["num_sections"])
365
+
366
+
367
+ def _json_format(response: str, _kwargs: dict) -> bool:
368
+ text = response.strip()
369
+ text = re.sub(r"^```(?:json|JSON)?", "", text).strip()
370
+ text = re.sub(r"```$", "", text).strip()
371
+ try:
372
+ json.loads(text)
373
+ except (ValueError, TypeError):
374
+ return False
375
+ return True
376
+
377
+
378
+ def _title(response: str, _kwargs: dict) -> bool:
379
+ return any(
380
+ match.strip("<>").strip() for match in re.findall(r"<<[^\n]+>>", response)
381
+ )
382
+
383
+
384
+ def _two_responses(response: str, _kwargs: dict) -> bool:
385
+ parts = response.split("******")
386
+ if len(parts) != 2:
387
+ return False
388
+ return all(part.strip() for part in parts)
389
+
390
+
391
+ def _repeat_prompt(response: str, kwargs: dict) -> bool:
392
+ prompt = kwargs["prompt_to_repeat"].strip().lower()
393
+ return response.strip().lower().startswith(prompt)
394
+
395
+
396
+ def _end_checker(response: str, kwargs: dict) -> bool:
397
+ return response.strip().lower().endswith(kwargs["end_phrase"].strip().lower())
398
+
399
+
400
+ def _quotation(response: str, _kwargs: dict) -> bool:
401
+ text = response.strip()
402
+ return len(text) > 1 and text.startswith('"') and text.endswith('"')
403
+
404
+
405
+ def _is_english(response: str) -> bool:
406
+ # The reference checkers pair the case rule with a language check and treat
407
+ # an undetectable response as a pass.
408
+ detected = detect_language(response)
409
+ return detected in (None, "en")
410
+
411
+
412
+ def _english_capital(response: str, _kwargs: dict) -> bool:
413
+ return response.isupper() and _is_english(response)
414
+
415
+
416
+ def _english_lowercase(response: str, _kwargs: dict) -> bool:
417
+ return response.islower() and _is_english(response)
418
+
419
+
420
+ def _capital_word_frequency(response: str, kwargs: dict) -> bool:
421
+ # Whitespace tokens with the outer punctuation trimmed, so abbreviations
422
+ # such as "P.P.S" count once rather than once per letter.
423
+ words = [token.strip(".,;:!?\"'()[]{}") for token in response.split()]
424
+ capitals = [
425
+ word
426
+ for word in words
427
+ if word and word.isupper() and any(char.isalpha() for char in word)
428
+ ]
429
+ return _compare(
430
+ len(capitals), kwargs["capital_relation"], int(kwargs["capital_frequency"])
431
+ )
432
+
433
+
434
+ def _no_comma(response: str, _kwargs: dict) -> bool:
435
+ return "," not in response and "," not in response
436
+
437
+
438
+ CHECKERS: dict[str, Checker] = {
439
+ "change_case:capital_word_frequency": _capital_word_frequency,
440
+ "change_case:english_capital": _english_capital,
441
+ "change_case:english_lowercase": _english_lowercase,
442
+ "combination:repeat_prompt": _repeat_prompt,
443
+ "combination:two_responses": _two_responses,
444
+ "detectable_content:number_placeholders": _number_placeholders,
445
+ "detectable_content:postscript": _postscript,
446
+ "detectable_format:constrained_response": _constrained_response,
447
+ "detectable_format:json_format": _json_format,
448
+ "detectable_format:multiple_sections": _multiple_sections,
449
+ "detectable_format:number_bullet_lists": _number_bullets,
450
+ "detectable_format:number_highlighted_sections": _number_highlighted_sections,
451
+ "detectable_format:title": _title,
452
+ "keywords:existence": _keywords_existence,
453
+ "keywords:forbidden_words": _keywords_forbidden,
454
+ "keywords:frequency": _keywords_frequency,
455
+ "keywords:letter_frequency": _letter_frequency,
456
+ "language:response_language": _response_language,
457
+ "length_constraints:nth_paragraph_first_word": _nth_paragraph_first_word,
458
+ "length_constraints:number_paragraphs": _number_paragraphs,
459
+ "length_constraints:number_sentences": _number_sentences,
460
+ "length_constraints:number_words": _number_words,
461
+ "punctuation:no_comma": _no_comma,
462
+ "startend:end_checker": _end_checker,
463
+ "startend:quotation": _quotation,
464
+ }
465
+
466
+
467
+ def follows_instruction(instruction_id: str, response: str, kwargs: dict) -> bool:
468
+ """Return whether a response satisfies one verifiable instruction."""
469
+ checker = CHECKERS.get(instruction_id)
470
+ if checker is None:
471
+ raise KeyError(f"unknown IFEval instruction: {instruction_id}")
472
+ if not response.strip():
473
+ return False
474
+ try:
475
+ return bool(checker(response, kwargs))
476
+ except (KeyError, ValueError, TypeError):
477
+ return False
@@ -0,0 +1,60 @@
1
+ """MBPP benchmark - 500 Python programming tasks."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from benchkit.benchmarks.base import Task
7
+ from benchkit.benchmarks.utils import strip_think_tags
8
+ from benchkit.evaluation import EvaluationResult
9
+ from benchkit.executor import execute_with_feedback
10
+
11
+ DATASET = Path(__file__).parent.parent / "datasets" / "mbpp.jsonl"
12
+
13
+ SYSTEM = (
14
+ "Write a Python function to solve the following task. "
15
+ "Output ONLY the function. "
16
+ "No markdown fences, no explanation."
17
+ )
18
+
19
+
20
+ def _extract_code(response: str) -> str:
21
+ """Strip markdown fences if the model wrapped its output."""
22
+ text = strip_think_tags(response).rstrip()
23
+
24
+ if "```python" in text:
25
+ text = text.split("```python", 1)[1].split("```", 1)[0]
26
+ elif "```" in text:
27
+ text = text.split("```", 1)[1].split("```", 1)[0]
28
+
29
+ return text.strip()
30
+
31
+
32
+ class MBPP:
33
+ name = "mbpp"
34
+ evaluation_activity = "executing code and running tests"
35
+
36
+ def load_tasks(self) -> list[Task]:
37
+ tasks = []
38
+ with open(DATASET) as f:
39
+ for line in f:
40
+ d = json.loads(line)
41
+ tasks.append(
42
+ Task(
43
+ id=d["task_id"],
44
+ prompt=d["prompt"],
45
+ metadata={"test": d["test"]},
46
+ )
47
+ )
48
+ return tasks
49
+
50
+ def build_prompt(self, task: Task) -> str:
51
+ return f"{SYSTEM}\n\n{task.prompt}"
52
+
53
+ def evaluate(self, task: Task, response: str) -> bool:
54
+ return self.evaluate_with_feedback(task, response).passed
55
+
56
+ def evaluate_with_feedback(self, task: Task, response: str) -> EvaluationResult:
57
+ code = _extract_code(response)
58
+ full = code + "\n\n" + task.metadata["test"] + "\n"
59
+ result = execute_with_feedback(full)
60
+ return EvaluationResult(float(result.passed), result.feedback)