File size: 11,339 Bytes
224d30c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
"""
eval_set entry point for L2-Bench evaluation.

Usage:
    uv run python -m l2_bench_eval.eval \
        --model bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0 \
        --log-dir logs/run-001

    # Smoke test with 2 samples
    uv run python -m l2_bench_eval.eval \
        --model bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0 \
        --log-dir logs/smoke-test \
        --sample-limit 2

    # Run with custom solver config and scorer
    uv run python -m l2_bench_eval.eval \
        --model bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0 \
        --log-dir logs/run-001 \
        --epochs 2 --sample-limit 10 \
        --solver-max-tokens 8192 --solver-temperature 0.5 \
        --scorer-model bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0 \
        --csv-path path/to/data.csv --resources-dir path/to/resources

Version | Date       | Author    | Change comment
--------|------------|-----------|---------------
1.0.0   | 2026-07-29 | M. Ku     | Initial open-source release
"""

import argparse
from pathlib import Path

from dotenv import load_dotenv
from inspect_ai import eval_set
from inspect_ai.model import GenerateConfig, get_model
from pydantic import BaseModel

from l2_bench_eval import config
from l2_bench_eval.bedrock_patch import patch_bedrock_timeout
from l2_bench_eval.score import ScorerSetting
from l2_bench_eval.task import create_l2_bench_eval_task


class EvalRunParams(BaseModel):
    """Parameters for a single evaluation run.

    Attributes
    ----------
    solver_model_name : str
        Model identifier passed to ``get_model`` (e.g. ``bedrock/...``).
    solver_model_base_url : str or None
        Optional base URL override for the solver model API.
    solver_model_config : GenerateConfig
        Generation configuration for the solver model.
    log_dir : str
        Directory where eval logs are written.
    epochs : int
        Number of evaluation epochs.
    retry_on_error : int or None
        Number of retries on transient errors (``None`` disables retries).
    continue_on_fail : bool
        If ``True``, keep running remaining samples after a failure.
    scorer_setting : ScorerSetting or None
        Optional scorer model and generation configuration.
    csv_path : Path or None
        Path to the tasks CSV file. Uses the repo default when ``None``.
    resources_dir : Path or None
        Path to the task resources directory. Uses the repo default when ``None``.
    first_n_samples : int or None
        Limit evaluation to the first *n* samples.
    sample_range : tuple of (int, int) or None
        Slice range ``(start, end)`` applied to the dataset. Overrides
        ``first_n_samples`` when set.
    """

    solver_model_name: str
    solver_model_base_url: str | None = None
    solver_model_config: GenerateConfig = GenerateConfig(max_tokens=4096, temperature=0.0)
    log_dir: str
    epochs: int = 1
    retry_on_error: int | None = 1
    continue_on_fail: bool = True
    scorer_setting: ScorerSetting | None = None
    csv_path: Path | None = None
    resources_dir: Path | None = None
    first_n_samples: int | None = None
    sample_range: tuple[int, int] | None = None # will override first_n_samples
    task_ids: list[int] | None = None # will override sample_range


def run_eval(params: EvalRunParams):
    """Execute an L2-Bench evaluation run.

    Parameters
    ----------
    params : EvalRunParams
        Fully-populated run parameters including model, scorer, and dataset
        settings.
    """
    patch_bedrock_timeout(read_timeout=600)

    solver_model = get_model(
        model=params.solver_model_name,
        base_url=params.solver_model_base_url,
        config=params.solver_model_config,
    )

    task = create_l2_bench_eval_task(
        scorer_setting=params.scorer_setting,
        csv_path=params.csv_path,
        resources_dir=params.resources_dir,
        first_n_samples=params.first_n_samples,
        sample_range=params.sample_range,
        task_ids=params.task_ids
    )

    eval_set(
        tasks=[task],
        model=solver_model,
        log_dir=params.log_dir,
        epochs=params.epochs,
        retry_on_error=params.retry_on_error,
        continue_on_fail=params.continue_on_fail
    )


def main():
    """CLI entry point for L2-Bench evaluation."""
    parser = argparse.ArgumentParser(description="Run L2-Bench eval_set")
    parser.add_argument("--model", required=True, help="Solver model name")
    parser.add_argument("--log-dir", required=True, help="Log directory")
    parser.add_argument("--epochs", type=int, default=1)
    parser.add_argument("--sample-limit", type=int, default=0, help="0 = no limit")
    parser.add_argument("--env-file", type=Path, default=Path.cwd() / ".env", help="Path to .env file")

    parser.add_argument("--solver-max-tokens", type=int, default=4096, help="Solver max output tokens")
    parser.add_argument("--solver-temperature", type=float, default=0.0, help="Solver sampling temperature")
    parser.add_argument("--solver-top-p", type=float, default=None, help="Solver top-p (nucleus sampling)")
    parser.add_argument("--solver-top-k", type=int, default=None, help="Solver top-k sampling")
    parser.add_argument("--solver-frequency-penalty", type=float, default=None, help="Solver frequency penalty")
    parser.add_argument("--solver-presence-penalty", type=float, default=None, help="Solver presence penalty")
    parser.add_argument("--solver-seed", type=int, default=None, help="Solver random seed")
    parser.add_argument("--solver-stop-seqs", nargs="*", default=None, help="Solver stop sequences")
    parser.add_argument("--solver-num-choices", type=int, default=None, help="Solver number of choices")
    parser.add_argument("--solver-best-of", type=int, default=None, help="Solver best-of sampling count")
    parser.add_argument("--solver-max-retries", type=int, default=None, help="Solver max retries")
    parser.add_argument("--solver-timeout", type=int, default=None, help="Solver timeout in seconds")
    parser.add_argument("--solver-max-connections", type=int, default=None, help="Solver max connections")
    parser.add_argument("--solver-reasoning-tokens", type=int, default=None, help="Solver reasoning/thinking token budget")
    parser.add_argument("--solver-reasoning-effort", choices=["none", "minimal", "low", "medium", "high", "xhigh"], default=None, help="Solver reasoning effort level")

    parser.add_argument("--scorer-model", default=config.DEFAULT_JUDGE_MODEL, help="Judge model name")
    parser.add_argument("--scorer-max-tokens", type=int, default=None, help="Scorer max output tokens")
    parser.add_argument("--scorer-temperature", type=float, default=None, help="Judge sampling temperature. Leave unset when a reasoning budget is in use")
    parser.add_argument("--scorer-top-p", type=float, default=None, help="Scorer top-p (nucleus sampling)")
    parser.add_argument("--scorer-top-k", type=int, default=None, help="Scorer top-k sampling")
    parser.add_argument("--scorer-frequency-penalty", type=float, default=None, help="Scorer frequency penalty")
    parser.add_argument("--scorer-presence-penalty", type=float, default=None, help="Scorer presence penalty")
    parser.add_argument("--scorer-seed", type=int, default=None, help="Scorer random seed")
    parser.add_argument("--scorer-stop-seqs", nargs="*", default=None, help="Scorer stop sequences")
    parser.add_argument("--scorer-num-choices", type=int, default=None, help="Scorer number of choices")
    parser.add_argument("--scorer-best-of", type=int, default=None, help="Scorer best-of sampling count")
    parser.add_argument("--scorer-max-retries", type=int, default=None, help="Scorer max API retries")
    parser.add_argument("--scorer-timeout", type=int, default=None, help="Scorer timeout in seconds")
    parser.add_argument("--scorer-max-connections", type=int, default=None, help="Scorer max connections")
    parser.add_argument("--scorer-reasoning-tokens", type=int, default=config.DEFAULT_JUDGE_REASONING_TOKENS, help="Judge reasoning/thinking token budget")
    parser.add_argument("--scorer-reasoning-effort", choices=["none", "minimal", "low", "medium", "high", "xhigh"], default=None, help="Scorer reasoning effort level")

    parser.add_argument(
        "--task-ids", nargs="*", type=int, default=None,
        help="List of task IDs to evaluate",
    )
    parser.add_argument("--continue-on-fail", action=argparse.BooleanOptionalAction, default=True)
    parser.add_argument("--csv-path", type=Path, default=None)
    parser.add_argument("--resources-dir", type=Path, default=None)
    parser.add_argument(
        "--prompt-version", default=config.DEFAULT_JUDGE_PROMPT_VERSION,
        help="Judge prompt version. v1 is the production judge; v2-v4 are the paper's ablations",
    )
    parser.add_argument(
        "--judge-verdict-retries", type=int, default=config.DEFAULT_JUDGE_MAX_RETRIES,
        help="Times to re-prompt the judge when it returns an unparseable verdict",
    )
    args = parser.parse_args()

    load_dotenv(args.env_file)

    params = EvalRunParams(
        solver_model_name=args.model,
        solver_model_config=GenerateConfig(
            max_tokens=args.solver_max_tokens,
            temperature=args.solver_temperature,
            top_p=args.solver_top_p,
            top_k=args.solver_top_k,
            frequency_penalty=args.solver_frequency_penalty,
            presence_penalty=args.solver_presence_penalty,
            seed=args.solver_seed,
            stop_seqs=args.solver_stop_seqs,
            num_choices=args.solver_num_choices,
            best_of=args.solver_best_of,
            max_retries=args.solver_max_retries,
            timeout=args.solver_timeout,
            max_connections=args.solver_max_connections,
            reasoning_tokens=args.solver_reasoning_tokens,
            reasoning_effort=args.solver_reasoning_effort,
        ),
        log_dir=args.log_dir,
        epochs=args.epochs,
        continue_on_fail=args.continue_on_fail,
        scorer_setting=ScorerSetting(
            model=args.scorer_model,
            max_retries=args.judge_verdict_retries,
            scorer_model_config=GenerateConfig(
                max_tokens=args.scorer_max_tokens,
                temperature=args.scorer_temperature,
                top_p=args.scorer_top_p,
                top_k=args.scorer_top_k,
                frequency_penalty=args.scorer_frequency_penalty,
                presence_penalty=args.scorer_presence_penalty,
                seed=args.scorer_seed,
                stop_seqs=args.scorer_stop_seqs,
                num_choices=args.scorer_num_choices,
                best_of=args.scorer_best_of,
                max_retries=args.scorer_max_retries,
                timeout=args.scorer_timeout,
                max_connections=args.scorer_max_connections,
                reasoning_tokens=args.scorer_reasoning_tokens,
                reasoning_effort=args.scorer_reasoning_effort,
            ),
            prompt_version=args.prompt_version,
        ),
        csv_path=args.csv_path,
        resources_dir=args.resources_dir,
        first_n_samples=args.sample_limit if args.sample_limit > 0 else None,
        task_ids=args.task_ids,
    )

    run_eval(params)


if __name__ == "__main__":
    main()