Zhengyang commited on
Commit
038337f
·
1 Parent(s): dfc6628

Deploy AISE-Bench leaderboard

Browse files
Files changed (8) hide show
  1. .gitignore +3 -0
  2. README.md +6 -4
  3. app.py +350 -0
  4. data/leaderboard.json +1 -0
  5. publish_result.py +71 -0
  6. requirements.txt +2 -0
  7. schema.py +101 -0
  8. storage.py +142 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ runtime_submissions/
2
+ __pycache__/
3
+ *.log
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: AISE Bench Leaderboard
3
- emoji: 📊
4
- colorFrom: indigo
5
  colorTo: purple
6
  sdk: static
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
+ title: AISE-Bench Leaderboard
3
+ emoji: compass
4
+ colorFrom: blue
5
  colorTo: purple
6
  sdk: static
7
  pinned: false
8
  ---
9
 
10
+ # AISE-Bench Leaderboard
11
+
12
+ Static leaderboard page for AISE-Bench.
app.py ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import html
4
+
5
+ import gradio as gr
6
+
7
+ from storage import load_leaderboard
8
+
9
+
10
+ APP_CSS = """
11
+ footer, .api-docs, .show-api, .built-with, [data-testid="api-info"] { display: none !important; }
12
+ .gradio-container {
13
+ max-width: 100% !important;
14
+ padding: 18px 24px 24px !important;
15
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
16
+ font-size: 14px !important;
17
+ }
18
+ .hero { text-align: center; margin-bottom: 8px; }
19
+ .hero h1 { font-size: 32px !important; line-height: 1.2 !important; margin: 8px 0 6px !important; }
20
+ .hero p { color: #4b5563; font-size: 14px !important; line-height: 1.6 !important; margin: 0 !important; }
21
+ .hero-links { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; margin-top: 8px; }
22
+ .hero-links a { color: #2563eb; text-decoration: none; }
23
+ .hero-links a:hover { text-decoration: underline; }
24
+ .summary-grid { display: grid; grid-template-columns: repeat(3, minmax(180px, 1fr)); gap: 12px; margin: 14px 0 18px; }
25
+ .summary-card { border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px 16px; background: #ffffff; }
26
+ .summary-card strong { display: block; color: #111827; font-size: 18px; margin-bottom: 3px; }
27
+ .summary-card span { color: #6b7280; font-size: 13px; }
28
+ .submission-guide { max-width: 1100px; font-size: 14px !important; line-height: 1.65 !important; margin: 8px auto 22px; }
29
+ .submission-guide h2 { font-size: 21px !important; margin: 22px 0 8px !important; }
30
+ .submission-guide h3 { font-size: 17px !important; margin: 18px 0 8px !important; }
31
+ .submission-guide p, .submission-guide li { font-size: 14px !important; line-height: 1.65 !important; }
32
+ .submission-guide code { font-size: 13px !important; }
33
+ .submission-guide pre code { font-size: 13px !important; line-height: 1.5 !important; }
34
+ .leaderboard-title h2 { font-size: 24px !important; text-align: center; margin: 18px 0 4px !important; }
35
+ .leaderboard-title p { color: #6b7280; font-size: 14px !important; text-align: center; margin: 0 0 12px !important; }
36
+ .leaderboard-toolbar { display: flex; align-items: center; justify-content: flex-end; margin: 8px 0 10px; }
37
+ .table-meta { color: #6b7280; display: flex; gap: 12px; font-size: 13px; }
38
+ .aise-table-wrap { height: min(68vh, 760px); min-height: 460px; overflow: auto; border: 1px solid #e5e7eb; border-radius: 10px; background: #ffffff; }
39
+ .aise-table { border-collapse: separate; border-spacing: 0; width: 100%; min-width: 1320px; color: #263238; font-size: 13px; }
40
+ .aise-table th, .aise-table td { border-bottom: 1px solid #edf0f3; border-right: 1px solid #edf0f3; padding: 9px 10px; text-align: center; white-space: nowrap; }
41
+ .aise-table th { position: sticky; z-index: 2; background: #f8fafc; color: #111827; font-weight: 650; user-select: none; }
42
+ .aise-table thead tr:first-child th { top: 0; background: #e5e7eb; font-size: 13px; }
43
+ .aise-table thead tr:nth-child(2) th { top: 38px; cursor: pointer; }
44
+ .aise-table th[rowspan="2"] { top: 0; cursor: pointer; }
45
+ .aise-table tbody tr:nth-child(even) { background: #fafafa; }
46
+ .aise-table tbody tr:hover { background: #eef5ff; }
47
+ .aise-table .rank { width: 48px; color: #4b5563; font-variant-numeric: tabular-nums; }
48
+ .aise-table .model { min-width: 190px; text-align: left; white-space: normal; }
49
+ .aise-table .model a, .aise-table .model-name { color: #2563eb; font-size: 14px; font-weight: 650; text-decoration: none; }
50
+ .aise-table .model a:hover { text-decoration: underline; }
51
+ .aise-table .org { display: block; color: #8a8f98; font-size: 12px; margin-top: 3px; }
52
+ .aise-table .num { font-variant-numeric: tabular-nums; }
53
+ .verified-badge { display: inline-flex; align-items: center; border-radius: 999px; font-size: 11px; font-weight: 700; padding: 3px 8px; }
54
+ .verified-yes { background: #dcfce7; color: #166534; }
55
+ .verified-no { background: #f3f4f6; color: #6b7280; }
56
+ .aise-table .empty { padding: 28px; color: #7b8088; }
57
+ .submit-actions button { min-height: 42px !important; font-size: 14px !important; font-weight: 600 !important; }
58
+ .section-title h2 { font-size: 21px !important; line-height: 1.3 !important; margin: 22px 0 8px !important; }
59
+ label, .label-wrap, input, textarea { font-size: 14px !important; }
60
+ @media (max-width: 760px) {
61
+ .gradio-container { padding: 12px !important; }
62
+ .summary-grid { grid-template-columns: 1fr; }
63
+ .table-meta span:last-child { display: none; }
64
+ }
65
+ """
66
+
67
+
68
+ TABLE_JS = """
69
+ () => {
70
+ const table = document.getElementById("aise-leaderboard-table");
71
+ if (!table) return;
72
+
73
+ const tbody = table.querySelector("tbody");
74
+ const rows = Array.from(tbody.querySelectorAll("tr[data-entry]"));
75
+ const headers = table.querySelectorAll("th[data-sort]");
76
+ const countElement = document.getElementById("aise-table-count");
77
+ const numericColumns = new Set([
78
+ "Rank", "Precision", "Recall", "Format", "Edit Dist.", "Para. Acc.",
79
+ "Success", "Correct.", "Complete.", "Faithful.", "F1-LM"
80
+ ]);
81
+ let sortColumn = "F1-LM";
82
+ let sortDirection = "desc";
83
+
84
+ function parseValue(row, column) {
85
+ const cell = row.querySelector(`[data-col="${column}"]`);
86
+ if (!cell) return "";
87
+ const raw = cell.dataset.value || cell.textContent || "";
88
+ if (numericColumns.has(column)) {
89
+ const value = Number.parseFloat(raw);
90
+ return Number.isNaN(value) ? -Infinity : value;
91
+ }
92
+ return raw.toLowerCase();
93
+ }
94
+
95
+ function applyTableState() {
96
+ const sortedRows = rows.slice().sort((left, right) => {
97
+ const leftValue = parseValue(left, sortColumn);
98
+ const rightValue = parseValue(right, sortColumn);
99
+ if (leftValue < rightValue) return sortDirection === "asc" ? -1 : 1;
100
+ if (leftValue > rightValue) return sortDirection === "asc" ? 1 : -1;
101
+ return 0;
102
+ });
103
+
104
+ sortedRows.forEach((row, index) => {
105
+ tbody.appendChild(row);
106
+ const rank = index + 1;
107
+ const rankCell = row.querySelector('[data-col="Rank"]');
108
+ rankCell.textContent = String(rank);
109
+ rankCell.dataset.value = String(rank);
110
+ });
111
+
112
+ countElement.textContent = `${sortedRows.length} entries`;
113
+ headers.forEach(header => {
114
+ header.classList.remove("sort-asc", "sort-desc");
115
+ if (header.dataset.sort === sortColumn) {
116
+ header.classList.add(sortDirection === "asc" ? "sort-asc" : "sort-desc");
117
+ }
118
+ });
119
+ }
120
+
121
+ headers.forEach(header => {
122
+ header.addEventListener("click", () => {
123
+ const column = header.dataset.sort;
124
+ if (sortColumn === column) {
125
+ sortDirection = sortDirection === "asc" ? "desc" : "asc";
126
+ } else {
127
+ sortColumn = column;
128
+ sortDirection = numericColumns.has(column) ? "desc" : "asc";
129
+ }
130
+ applyTableState();
131
+ });
132
+ });
133
+
134
+ applyTableState();
135
+ }
136
+ """
137
+
138
+
139
+ METRIC_COLUMNS = [
140
+ ("Precision", "precision", None),
141
+ ("Recall", "recall", None),
142
+ ("Format", "format", "clarity"),
143
+ ("Edit Dist.", "edit_distance", None),
144
+ ("Para. Acc.", "para_acc", "planning"),
145
+ ("Success", "success", "execution"),
146
+ ("Correct.", "correct", "correctness"),
147
+ ("Complete.", "complete", "completeness"),
148
+ ("Faithful.", "faithful", "faithfulness"),
149
+ ("F1-LM", "f1_lm", "overall"),
150
+ ]
151
+
152
+
153
+ def _metric(record: dict, primary: str, fallback: str | None = None) -> object:
154
+ value = record.get(primary)
155
+ if value is None and fallback:
156
+ value = record.get(fallback)
157
+ return value
158
+
159
+
160
+ def _format_metric(value: object) -> str:
161
+ if value is None or value == "":
162
+ return "/"
163
+ if isinstance(value, str):
164
+ return html.escape(value)
165
+ try:
166
+ return f"{float(value):.4f}".rstrip("0").rstrip(".")
167
+ except (TypeError, ValueError):
168
+ return html.escape(str(value))
169
+
170
+
171
+ def _metric_value(value: object) -> str:
172
+ try:
173
+ return str(float(value))
174
+ except (TypeError, ValueError):
175
+ return ""
176
+
177
+
178
+ def _leaderboard_html() -> str:
179
+ rows = []
180
+ for rank, record in enumerate(load_leaderboard(), start=1):
181
+ model = html.escape(str(record.get("model", "")))
182
+ organization = html.escape(str(record.get("organization", "")))
183
+ model_url = str(record.get("model_url", ""))
184
+ if _valid_url(model_url):
185
+ model_cell = f'<a href="{html.escape(model_url, quote=True)}" target="_blank">{model}</a>'
186
+ else:
187
+ model_cell = f'<span class="model-name">{model}</span>'
188
+ model_cell += f'<span class="org">{organization}</span>'
189
+ metric_cells = []
190
+ for label, primary, fallback in METRIC_COLUMNS:
191
+ metric = _metric(record, primary, fallback)
192
+ metric_cells.append(
193
+ f'<td class="num" data-col="{label}" data-value="{_metric_value(metric)}">'
194
+ f'{_format_metric(metric)}</td>'
195
+ )
196
+ verified = bool(record.get("verified"))
197
+ verified_label = "Verified" if verified else "Pending"
198
+ verified_class = "verified-yes" if verified else "verified-no"
199
+ rows.append(
200
+ f"""
201
+ <tr data-entry="true">
202
+ <td class="rank" data-col="Rank" data-value="{rank}">{rank}</td>
203
+ <td class="model" data-col="Model" data-value="{model}">{model_cell}</td>
204
+ {''.join(metric_cells)}
205
+ <td data-col="Verified" data-value="{verified_label}"><span class="verified-badge {verified_class}">{verified_label}</span></td>
206
+ </tr>
207
+ """
208
+ )
209
+
210
+ empty = '<tr><td class="empty" colspan="13">No verified submissions yet.</td></tr>'
211
+ return f"""
212
+ <div class="leaderboard-toolbar">
213
+ <div class="table-meta">
214
+ <span id="aise-table-count">{len(rows)} entries</span>
215
+ <span>Click a column header to sort</span>
216
+ </div>
217
+ </div>
218
+ <div class="aise-table-wrap">
219
+ <table id="aise-leaderboard-table" class="aise-table">
220
+ <thead>
221
+ <tr>
222
+ <th rowspan="2" data-sort="Rank">#</th>
223
+ <th rowspan="2" data-sort="Model">Model</th>
224
+ <th colspan="3">References and Formatting</th>
225
+ <th colspan="3">API-based Judge</th>
226
+ <th colspan="4">Answer Content</th>
227
+ <th rowspan="2" data-sort="Verified">Status</th>
228
+ </tr>
229
+ <tr>
230
+ <th data-sort="Precision">Precision</th><th data-sort="Recall">Recall</th><th data-sort="Format">Format</th>
231
+ <th data-sort="Edit Dist.">Edit Dist.</th><th data-sort="Para. Acc.">Para. Acc.</th><th data-sort="Success">Success</th>
232
+ <th data-sort="Correct.">Correct.</th><th data-sort="Complete.">Complete.</th><th data-sort="Faithful.">Faithful.</th><th data-sort="F1-LM">F1-LM</th>
233
+ </tr>
234
+ </thead>
235
+ <tbody>{''.join(rows) or empty}</tbody>
236
+ </table>
237
+ </div>
238
+ """
239
+
240
+
241
+ with gr.Blocks(
242
+ title="AISE-Bench Leaderboard",
243
+ analytics_enabled=False,
244
+ ) as demo:
245
+ gr.Markdown(
246
+ """
247
+ # AISE-Bench Leaderboard
248
+
249
+ Benchmarking full-cycle academic information-seeking agents on planning, execution,
250
+ reference grounding, and answer quality.
251
+
252
+ <div class="hero-links">
253
+ <a href="https://aise-bench.github.io/" target="_blank" rel="noopener noreferrer">Project Page</a>
254
+ <span aria-hidden="true">&middot;</span>
255
+ <a href="https://arxiv.org/abs/2607.20498" target="_blank" rel="noopener noreferrer">Paper</a>
256
+ <span aria-hidden="true">&middot;</span>
257
+ <a href="https://huggingface.co/datasets/zhengyang6666/AISE-Bench" target="_blank" rel="noopener noreferrer">Dataset</a>
258
+ <span aria-hidden="true">&middot;</span>
259
+ <a href="https://github.com/zai-org/AISE-Bench" target="_blank" rel="noopener noreferrer">GitHub</a>
260
+ </div>
261
+ """,
262
+ elem_classes=["hero"],
263
+ )
264
+
265
+ gr.HTML(
266
+ """
267
+ <div class="summary-grid">
268
+ <div class="summary-card"><strong>500</strong><span>official test questions</span></div>
269
+ <div class="summary-card"><strong>10 metrics</strong><span>process and answer evaluation</span></div>
270
+ <div class="summary-card"><strong>Verified only</strong><span>scores produced by the official evaluator</span></div>
271
+ </div>
272
+ """
273
+ )
274
+
275
+ gr.Markdown(
276
+ """
277
+ ## How to Submit
278
+
279
+ 1. Fork [this repository](https://huggingface.co/datasets/zhengyang6666/AISE-Bench).
280
+ 2. Create a new branch for your submission.
281
+ 3. Add your submission folder under
282
+ `submissions/<organization>__<model>__<input_config>/`.
283
+ 4. Open a Pull Request with the new submission folder.
284
+
285
+ ### Submission Directory Requirements
286
+
287
+ Each submission directory must contain the metadata and predictions for one
288
+ model/input configuration pair:
289
+
290
+ ```text
291
+ <organization>__<model>__<input_config>/
292
+ metadata.yaml
293
+ predictions.jsonl
294
+ generation_config.json # optional, recommended
295
+ artifacts/ # optional logs or prompt notes
296
+ ```
297
+
298
+ Use URL-safe directory names. Replace spaces, slashes, and special characters
299
+ with hyphens; keep `input_config` as `TEXT`.
300
+
301
+ ### `metadata.yaml`
302
+
303
+ ```yaml
304
+ model_name: "My Model"
305
+ model_url: https://...
306
+ ```
307
+
308
+ ### `predictions.jsonl`
309
+
310
+ Each line must be one JSON object:
311
+
312
+ - `index`: one-based sequential position; it must be exactly `1, 2, ..., N` in file order
313
+ - `qid`: stable question identifier from the dataset
314
+ - `question`: original real academic search query submitted by AMiner users
315
+ - `planning_text`: model-produced multi-step API planning sequence
316
+ - `api_input`: standardized input parameters for each API call
317
+ - `api_output`: AMiner academic KG API results, entity IDs, and execution status
318
+ - `result_edit`: model-produced final answer grounded by reference citations `[1]`
319
+
320
+ ```json
321
+ {
322
+ "index": 1,
323
+ "qid": 3,
324
+ "question": "question text",
325
+ "planning_text": [],
326
+ "api_input": {},
327
+ "api_output": {},
328
+ "result_edit": "{\"answer\":\"...\",\"reference\":{\"[1]\":\"...\"}}"
329
+ }
330
+ ```
331
+
332
+ The file must contain exactly one line for every item in `test.json`.
333
+ `qid` must match the corresponding item in `test.json`.
334
+ """,
335
+ elem_classes=["submission-guide"],
336
+ )
337
+
338
+ gr.Markdown(
339
+ """
340
+ ## Leaderboard
341
+
342
+ Official results on the 500-question test set are sorted by **F1-LM** by default. The table
343
+ is intentionally empty until the first submission completes official evaluation and review.
344
+ """,
345
+ elem_classes=["leaderboard-title"],
346
+ )
347
+ leaderboard = gr.HTML(value=_leaderboard_html())
348
+
349
+ if __name__ == "__main__":
350
+ demo.launch(css=APP_CSS, js=TABLE_JS)
data/leaderboard.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
publish_result.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+
8
+ from storage import load_pending_submission, publish_result
9
+
10
+
11
+ REQUIRED_METRICS = {
12
+ "precision",
13
+ "recall",
14
+ "clarity",
15
+ "correctness",
16
+ "integrality",
17
+ "completeness",
18
+ "faithfulness",
19
+ "success",
20
+ "edit_distance",
21
+ }
22
+
23
+
24
+ def parse_args() -> argparse.Namespace:
25
+ parser = argparse.ArgumentParser(description="Publish an evaluated AISE-Bench submission.")
26
+ parser.add_argument("submission_id")
27
+ parser.add_argument("metrics_json", type=Path)
28
+ parser.add_argument("--grounding", type=float, required=True)
29
+ parser.add_argument("--overall", type=float, required=True)
30
+ return parser.parse_args()
31
+
32
+
33
+ def main() -> None:
34
+ args = parse_args()
35
+ metrics = json.loads(args.metrics_json.read_text(encoding="utf-8-sig"))
36
+ missing = REQUIRED_METRICS - metrics.keys()
37
+ if missing:
38
+ raise SystemExit(f"Metrics file is missing: {', '.join(sorted(missing))}")
39
+
40
+ metadata = load_pending_submission(args.submission_id)
41
+ result = {
42
+ **metadata,
43
+ "status": "accepted",
44
+ "evaluated_at": datetime.now(timezone.utc).isoformat(),
45
+ "format": float(metrics["clarity"]),
46
+ "edit_distance": float(metrics["edit_distance"]),
47
+ "para_acc": metrics.get("para_acc", metrics.get("parameter_accuracy")),
48
+ "success": float(metrics["success"]),
49
+ "correct": float(metrics["correctness"]),
50
+ "complete": float(metrics["completeness"]),
51
+ "faithful": float(metrics["faithfulness"]),
52
+ "f1_lm": metrics.get("f1_lm"),
53
+ "planning": float(metrics["edit_distance"]),
54
+ "execution": float(metrics["success"]),
55
+ "precision": float(metrics["precision"]),
56
+ "recall": float(metrics["recall"]),
57
+ "clarity": float(metrics["clarity"]),
58
+ "correctness": float(metrics["correctness"]),
59
+ "integrality": float(metrics["integrality"]),
60
+ "completeness": float(metrics["completeness"]),
61
+ "faithfulness": float(metrics["faithfulness"]),
62
+ "grounding": args.grounding,
63
+ "overall": args.overall,
64
+ "verified": True,
65
+ }
66
+ publish_result(result)
67
+ print(f"Published {args.submission_id}")
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=6.0,<7
2
+ huggingface_hub>=0.28
schema.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ MAX_SUBMISSION_BYTES = 100 * 1024 * 1024
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class Track:
15
+ name: str
16
+ expected_samples: int
17
+
18
+
19
+ TRACKS = {
20
+ "test-500": Track("Official Test 500", 500),
21
+ }
22
+
23
+ REQUIRED_PREDICTION_FIELDS = {
24
+ "index",
25
+ "qid",
26
+ "question",
27
+ "planning_text",
28
+ "api_input",
29
+ "api_output",
30
+ "result_edit",
31
+ }
32
+
33
+
34
+ def load_and_validate_submission(path: str | Path, track_id: str) -> tuple[list[dict[str, Any]], str]:
35
+ submission_path = Path(path)
36
+ if not submission_path.is_file():
37
+ raise ValueError("Submission file does not exist.")
38
+ if submission_path.stat().st_size > MAX_SUBMISSION_BYTES:
39
+ raise ValueError("Submission file exceeds the 100 MB limit.")
40
+ if track_id not in TRACKS:
41
+ raise ValueError(f"Unknown track: {track_id}")
42
+
43
+ try:
44
+ payload = json.loads(submission_path.read_text(encoding="utf-8"))
45
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
46
+ raise ValueError(f"Submission must be valid UTF-8 JSON: {exc}") from exc
47
+
48
+ predictions = payload.get("predictions") if isinstance(payload, dict) else payload
49
+ if not isinstance(predictions, list):
50
+ raise ValueError('Submission must be a JSON list or {"predictions": [...]}.')
51
+
52
+ expected_samples = TRACKS[track_id].expected_samples
53
+ if len(predictions) != expected_samples:
54
+ raise ValueError(
55
+ f"Track {track_id} requires {expected_samples} predictions; received {len(predictions)}."
56
+ )
57
+
58
+ seen_ids: set[str] = set()
59
+ for position, prediction in enumerate(predictions, start=1):
60
+ if not isinstance(prediction, dict):
61
+ raise ValueError(f"Prediction #{position} must be a JSON object.")
62
+ missing_fields = REQUIRED_PREDICTION_FIELDS - prediction.keys()
63
+ if missing_fields:
64
+ missing = ", ".join(sorted(missing_fields))
65
+ raise ValueError(f"Prediction #{position} is missing required fields: {missing}.")
66
+ if "id" in prediction:
67
+ raise ValueError(f"Prediction #{position} must use 'qid'; field 'id' is not accepted.")
68
+
69
+ sequence_index = prediction["index"]
70
+ if isinstance(sequence_index, bool) or not isinstance(sequence_index, int):
71
+ raise ValueError(f"Prediction #{position} field 'index' must be an integer.")
72
+ if sequence_index != position:
73
+ raise ValueError(
74
+ f"Prediction #{position} must have index {position}; received {sequence_index}."
75
+ )
76
+
77
+ qid = prediction["qid"]
78
+ if isinstance(qid, bool) or not isinstance(qid, (int, str)):
79
+ raise ValueError(f"Prediction #{position} field 'qid' must be an integer or string.")
80
+ prediction_id = str(qid).strip()
81
+ if not prediction_id:
82
+ raise ValueError(f"Prediction #{position} field 'qid' must not be empty.")
83
+ if prediction_id in seen_ids:
84
+ raise ValueError(f"Duplicate prediction qid: {prediction_id}")
85
+ seen_ids.add(prediction_id)
86
+
87
+ if not isinstance(prediction["question"], str):
88
+ raise ValueError(f"Prediction {prediction_id} field 'question' must be a string.")
89
+ if not isinstance(prediction["planning_text"], (str, list)):
90
+ raise ValueError(
91
+ f"Prediction {prediction_id} field 'planning_text' must be a string or list."
92
+ )
93
+ if not isinstance(prediction["api_input"], (list, dict)):
94
+ raise ValueError(f"Prediction {prediction_id} field 'api_input' must be a list or object.")
95
+ if not isinstance(prediction["api_output"], (list, dict)):
96
+ raise ValueError(f"Prediction {prediction_id} field 'api_output' must be a list or object.")
97
+ if not isinstance(prediction["result_edit"], str):
98
+ raise ValueError(f"Prediction {prediction_id} field 'result_edit' must be a string.")
99
+
100
+ digest = hashlib.sha256(submission_path.read_bytes()).hexdigest()
101
+ return predictions, digest
storage.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from datetime import datetime, timezone
6
+ from io import BytesIO
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from huggingface_hub import HfApi, hf_hub_download, list_repo_files
11
+
12
+
13
+ BASE_DIR = Path(__file__).resolve().parent
14
+ SEED_PATH = BASE_DIR / "data" / "leaderboard.json"
15
+ LOCAL_RUNTIME_DIR = BASE_DIR / "runtime_submissions"
16
+
17
+
18
+ def _dataset_config() -> tuple[str, str] | None:
19
+ repo_id = os.getenv("LEADERBOARD_DATASET_REPO", "").strip()
20
+ token = os.getenv("HF_TOKEN", "").strip()
21
+ return (repo_id, token) if repo_id and token else None
22
+
23
+
24
+ def _read_json(path: str | Path) -> Any:
25
+ return json.loads(Path(path).read_text(encoding="utf-8"))
26
+
27
+
28
+ def load_leaderboard() -> list[dict[str, Any]]:
29
+ records = list(_read_json(SEED_PATH))
30
+ config = _dataset_config()
31
+
32
+ if config:
33
+ repo_id, token = config
34
+ try:
35
+ files = list_repo_files(repo_id, repo_type="dataset", token=token)
36
+ for filename in files:
37
+ if not filename.startswith("results/") or not filename.endswith(".json"):
38
+ continue
39
+ local_path = hf_hub_download(
40
+ repo_id,
41
+ filename,
42
+ repo_type="dataset",
43
+ token=token,
44
+ )
45
+ record = _read_json(local_path)
46
+ if record.get("status") == "accepted":
47
+ records.append(record)
48
+ except Exception:
49
+ pass
50
+ elif LOCAL_RUNTIME_DIR.exists():
51
+ for path in LOCAL_RUNTIME_DIR.glob("results/*.json"):
52
+ record = _read_json(path)
53
+ if record.get("status") == "accepted":
54
+ records.append(record)
55
+
56
+ def score(item: dict[str, Any]) -> float:
57
+ value = item.get("f1_lm", item.get("overall"))
58
+ try:
59
+ return float(value)
60
+ except (TypeError, ValueError):
61
+ return float("-inf")
62
+
63
+ return sorted(
64
+ records,
65
+ key=lambda item: (
66
+ -score(item),
67
+ str(item.get("model", "")).lower(),
68
+ ),
69
+ )
70
+
71
+
72
+ def queue_submission(metadata: dict[str, Any], prediction_path: str | Path) -> None:
73
+ metadata = {
74
+ **metadata,
75
+ "status": "pending",
76
+ "submitted_at": datetime.now(timezone.utc).isoformat(),
77
+ }
78
+ submission_id = metadata["submission_id"]
79
+ prediction_target = f"predictions/{submission_id}.json"
80
+ metadata_target = f"pending/{submission_id}.json"
81
+ config = _dataset_config()
82
+
83
+ if config:
84
+ repo_id, token = config
85
+ api = HfApi(token=token)
86
+ api.upload_file(
87
+ path_or_fileobj=str(prediction_path),
88
+ path_in_repo=prediction_target,
89
+ repo_id=repo_id,
90
+ repo_type="dataset",
91
+ commit_message=f"Add predictions for {submission_id}",
92
+ )
93
+ api.upload_file(
94
+ path_or_fileobj=BytesIO(
95
+ json.dumps(metadata, ensure_ascii=False, indent=2).encode("utf-8")
96
+ ),
97
+ path_in_repo=metadata_target,
98
+ repo_id=repo_id,
99
+ repo_type="dataset",
100
+ commit_message=f"Queue submission {submission_id}",
101
+ )
102
+ return
103
+
104
+ (LOCAL_RUNTIME_DIR / "predictions").mkdir(parents=True, exist_ok=True)
105
+ (LOCAL_RUNTIME_DIR / "pending").mkdir(parents=True, exist_ok=True)
106
+ destination = LOCAL_RUNTIME_DIR / prediction_target
107
+ destination.write_bytes(Path(prediction_path).read_bytes())
108
+ (LOCAL_RUNTIME_DIR / metadata_target).write_text(
109
+ json.dumps(metadata, ensure_ascii=False, indent=2),
110
+ encoding="utf-8",
111
+ )
112
+
113
+
114
+ def load_pending_submission(submission_id: str) -> dict[str, Any]:
115
+ filename = f"pending/{submission_id}.json"
116
+ config = _dataset_config()
117
+ if config:
118
+ repo_id, token = config
119
+ local_path = hf_hub_download(repo_id, filename, repo_type="dataset", token=token)
120
+ return _read_json(local_path)
121
+ return _read_json(LOCAL_RUNTIME_DIR / filename)
122
+
123
+
124
+ def publish_result(record: dict[str, Any]) -> None:
125
+ submission_id = record["submission_id"]
126
+ filename = f"results/{submission_id}.json"
127
+ payload = json.dumps(record, ensure_ascii=False, indent=2).encode("utf-8")
128
+ config = _dataset_config()
129
+ if config:
130
+ repo_id, token = config
131
+ HfApi(token=token).upload_file(
132
+ path_or_fileobj=BytesIO(payload),
133
+ path_in_repo=filename,
134
+ repo_id=repo_id,
135
+ repo_type="dataset",
136
+ commit_message=f"Publish result {submission_id}",
137
+ )
138
+ return
139
+
140
+ destination = LOCAL_RUNTIME_DIR / filename
141
+ destination.parent.mkdir(parents=True, exist_ok=True)
142
+ destination.write_bytes(payload)