File size: 6,791 Bytes
df3b181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eab219c
df3b181
 
 
 
 
 
a3335a4
df3b181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from __future__ import annotations

import argparse
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Any

from dotenv import load_dotenv
from leaderboard import LeaderboardClient, build_record, latest_log_file

load_dotenv()


def run_command(cmd: list[str]) -> subprocess.CompletedProcess[str]:
    print(f"[leaderboard] running: {' '.join(cmd)}")
    return subprocess.run(cmd, capture_output=True, text=True)


def build_inspect_command(args: argparse.Namespace) -> list[str]:
    cmd = []
    cmd.extend(args.inspect_launch)
    cmd.append(args.inspect_task)

    def add_task_arg(key: str, value: Any) -> None:
        if value is None:
            return
        cmd.extend(["-T", f"{key}={value}"])

    add_task_arg("solver_name", args.solver_name)
    add_task_arg("solver_kwargs", json.dumps(args.solver_kwargs))
    add_task_arg("dataset_name", args.dataset)
    if args.limit is not None:
        add_task_arg("limit", args.limit)

    cmd.extend(["--log-dir", args.log_dir])
    if args.log_format:
        cmd.extend(["--log-format", args.log_format])

    if args.extra_inspect_args:
        cmd.extend(args.extra_inspect_args)

    return cmd


def parse_score_from_outputs(log_dir: Path) -> tuple[float, Path, list[dict[str, Any]]]:
    log_path = latest_log_file(log_dir)
    if not log_path:
        raise RuntimeError("Inspect log file not found.")

    # Sanitization
    content = log_path.read_text(encoding="utf-8")
    # Regex to match hf_ followed by 34 alphanumeric chars
    sanitized_content = re.sub(r"hf_[a-zA-Z0-9]{34}", "<REDACTED_TOKEN>", content)

    if content != sanitized_content:
        log_path.write_text(sanitized_content, encoding="utf-8")
        print(f"[leaderboard] Redacted HF tokens in {log_path}")
        content = sanitized_content

    data = json.loads(content)
    results = data.get("results", {})
    scores = results.get("scores", [])
    score_value = None
    criterion_checks: list[dict[str, Any]] = []

    for score_entry in scores:
        metrics = score_entry.get("metrics", {})
        for metric in metrics.values():
            value = metric.get("value")
            if isinstance(value, (int, float)):
                score_value = float(value)
                break
        if score_value is not None:
            break

    if score_value is None:
        raise RuntimeError("Could not find a numeric metric value in the Inspect log.")

    for sample in data.get("samples", []):
        # Grab the question from metadata (fallback to input)
        question = "Unknown Question"
        if "metadata" in sample and "question" in sample["metadata"]:
            question = sample["metadata"]["question"]
        elif "input" in sample:
            question = sample["input"]

        # Check if any scorer produced criterion_checks
        for scorer in sample.get("scores", {}).values():
            metadata = scorer.get("metadata") or {}
            checks = metadata.get("criterion_checks")

            if isinstance(checks, list) and checks:
                # Create a grouped entry for this question/sample
                grouped_entry = {"question": question, "checks": []}
                for check in checks:
                    if isinstance(check, dict):
                        grouped_entry["checks"].append(check)

                if grouped_entry["checks"]:
                    criterion_checks.append(grouped_entry)

    return score_value, log_path, criterion_checks


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Run Inspect eval and append the resulting score to a HF dataset."
    )
    parser.add_argument(
        "--hf-dataset",
        default="akseljoonas/hf-agent-leaderboard",
        help="HF dataset repo id for the leaderboard (e.g. user/leaderboard).",
    )

    parser.add_argument(
        "--solver-name",
        required=True,
        help="Solver name used in the Inspect task (e.g. hf_agent).",
    )
    parser.add_argument(
        "--solver-kwargs",
        type=json.loads,
        default="{}",
        help="JSON string with solver kwargs passed to the Inspect task.",
    )
    parser.add_argument(
        "--dataset",
        default="akseljoonas/hf-agent-rubrics@train",
        help="Dataset spec in the form author/dataset@split.",
    )
    parser.add_argument(
        "--limit",
        type=int,
        default=None,
        help="Optional sample limit passed to Inspect.",
    )
    parser.add_argument(
        "--inspect-task",
        default="eval/task.py@hf-benchmark-with-rubrics",
        help="Inspect task reference.",
    )
    parser.add_argument(
        "--inspect-launch",
        nargs="+",
        default=["uv", "run", "inspect", "eval"],
        help="Command used to invoke Inspect (default: uv run inspect eval).",
    )
    parser.add_argument(
        "--log-dir",
        default="logs/leaderboard",
        help="Directory where Inspect outputs .eval logs.",
    )
    parser.add_argument(
        "--extra-inspect-args",
        nargs="*",
        help="Additional args forwarded to Inspect after the standard task arguments.",
    )
    parser.add_argument(
        "--log-format",
        default="json",
        help="Log format passed to Inspect (default: json).",
    )

    args = parser.parse_args()

    if isinstance(args.solver_kwargs, str):
        args.solver_kwargs = json.loads(args.solver_kwargs or "{}")

    hf_token = os.getenv("HF_TOKEN")
    if not hf_token:
        print("ERROR: set HF_TOKEN in your environment.", file=sys.stderr)
        sys.exit(1)

    if "@" not in args.dataset:
        raise ValueError("Dataset must be in the format 'author/dataset@split'.")
    dataset_name, dataset_split = args.dataset.split("@", 1)

    log_dir = Path(args.log_dir)
    log_dir.mkdir(parents=True, exist_ok=True)

    inspect_cmd = build_inspect_command(args)
    result = run_command(inspect_cmd)

    if result.returncode != 0:
        print(result.stdout)
        print(result.stderr, file=sys.stderr)
        raise SystemExit(result.returncode)

    score, log_path, criterion_checks = parse_score_from_outputs(log_dir)

    client = LeaderboardClient(repo_id=args.hf_dataset, token=hf_token)
    record = build_record(
        solver_name=args.solver_name,
        solver_kwargs=args.solver_kwargs,
        dataset_name=dataset_name,
        dataset_split=dataset_split,
        limit=args.limit,
        score=score,
        command=inspect_cmd,
        log_path=log_path,
        criterion_checks=criterion_checks,
    )
    client.append_record(record)

    print(
        f"[leaderboard] recorded score {score:.3f} for solver '{args.solver_name}' to {args.hf_dataset}"
    )


if __name__ == "__main__":
    main()