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
|
@@ -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)
|