@tangle-network/agent-bench 0.1.0 → 0.3.5
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.
- package/CHANGELOG.md +17 -0
- package/HARNESS.md +302 -0
- package/README.md +26 -1
- package/fixtures/aec-bench.json +18 -0
- package/fixtures/agentbench-dbbench.json +22 -0
- package/fixtures/bfcl.json +45 -0
- package/fixtures/commit0.json +72 -0
- package/fixtures/crag.json +10 -0
- package/fixtures/dabstep.json +22 -0
- package/fixtures/enterpriseops-gym.json +103 -0
- package/fixtures/finresearchbench.json +21 -0
- package/fixtures/finsearchcomp.json +66 -0
- package/fixtures/frames.json +26 -0
- package/fixtures/hotpotqa.json +182 -0
- package/fixtures/nomiracl.json +26 -0
- package/fixtures/open-rag-bench.json +16 -0
- package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
- package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
- package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
- package/fixtures/pier-agent/no-model-task/task.toml +35 -0
- package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
- package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
- package/fixtures/programbench.json +17 -0
- package/fixtures/ragbench.json +21 -0
- package/fixtures/simpleqa.json +121 -0
- package/fixtures/t2-ragbench.json +13 -0
- package/fixtures/tau2-bench.json +16 -0
- package/fixtures/tau3-banking.json +16 -0
- package/fixtures/toollm.json +28 -0
- package/fixtures/webarena-verified.json +20 -0
- package/package.json +39 -15
- package/pier_agents/__init__.py +18 -0
- package/pier_agents/candidate_contract.py +755 -0
- package/pier_agents/process_boundary.py +321 -0
- package/pier_agents/tangle_candidate.py +907 -0
- package/pier_agents/workspace_boundary.py +368 -0
- package/scripts/appworld_driver.py +359 -0
- package/scripts/cadbench_prepare.py +22 -0
- package/scripts/cadgenbench_hard_parts.py +48 -0
- package/scripts/clbench_codebase_judge.py +73 -0
- package/scripts/commit0_judge.py +170 -0
- package/scripts/dabstep_judge.py +42 -0
- package/scripts/enterpriseops_gym_judge.py +281 -0
- package/scripts/programbench_judge.py +120 -0
- package/scripts/render-gate-chart.mjs +176 -0
- package/scripts/run-package-tests.mjs +56 -0
- package/scripts/terminate-pier-trial.mts +66 -0
- package/scripts/trata-hedge/README.md +56 -0
- package/scripts/trata-hedge/run.sh +60 -0
- package/scripts/trata-hedge/solve.py +83 -0
- package/scripts/verify-packed-consumer.mjs +224 -0
- package/scripts/verify-pier-agent.mts +715 -0
- package/scripts/verify-pier-pair.mts +74 -0
- package/scripts/verify-pier-recovery.mts +139 -0
- package/src/adapters.ts +26 -0
- package/src/benchmarks/_harness.test.mts +178 -0
- package/src/benchmarks/_harness.ts +239 -16
- package/src/benchmarks/agentbench.ts +163 -0
- package/src/benchmarks/appworld.test.mts +15 -9
- package/src/benchmarks/bfcl.ts +346 -0
- package/src/benchmarks/crag.ts +137 -0
- package/src/benchmarks/dabstep.test.mts +70 -0
- package/src/benchmarks/dabstep.ts +212 -0
- package/src/benchmarks/external-adapters.test.mts +150 -0
- package/src/benchmarks/finresearchbench.ts +269 -0
- package/src/benchmarks/humaneval.ts +20 -8
- package/src/benchmarks/nomiracl.ts +180 -0
- package/src/benchmarks/open-rag-bench.ts +153 -0
- package/src/benchmarks/rag-benchmarks.test.mts +138 -0
- package/src/benchmarks/rag-shared.ts +327 -0
- package/src/benchmarks/ragbench.ts +171 -0
- package/src/benchmarks/swe-bench.test.mts +61 -0
- package/src/benchmarks/swe-bench.ts +201 -19
- package/src/benchmarks/t2-ragbench.ts +166 -0
- package/src/benchmarks/tau-bench-shared.ts +214 -0
- package/src/benchmarks/tau2-bench.ts +30 -0
- package/src/benchmarks/tau3-banking.ts +29 -0
- package/src/benchmarks/terminal-bench.test.mts +33 -0
- package/src/benchmarks/terminal-bench.ts +23 -8
- package/src/benchmarks/toollm.ts +254 -0
- package/src/benchmarks/types.ts +42 -0
- package/src/benchmarks/webarena-verified.ts +200 -0
- package/src/commit0-prereqs.sh +0 -0
- package/src/coordination-mcp-container-reach.mts +181 -0
- package/src/decoder-live.mts +1 -1
- package/src/examples/README.md +103 -39
- package/src/examples/benchmark-matrix.mts +101 -0
- package/src/examples/lean-proof-gate.README.md +77 -0
- package/src/examples/lean-proof-gate.mts +162 -0
- package/src/examples/lean-verify.ts +95 -0
- package/src/examples/lean.Dockerfile +12 -0
- package/src/examples/math-demo.mts +9 -7
- package/src/examples/strategy-demo.mts +10 -12
- package/src/gate.ts +3 -2
- package/src/hev-eval.mts +69 -0
- package/src/hev-improve.mts +169 -0
- package/src/hev-structural.mts +688 -0
- package/src/index.ts +73 -0
- package/src/mbpp-structural.mts +662 -0
- package/src/pier-agent.test-fixtures.mts +19 -0
- package/src/pier-agent.test.mts +363 -0
- package/src/pier-agent.ts +657 -0
- package/src/pier-result-grader.mjs +30 -0
- package/src/pier-result-grader.test.mts +62 -0
- package/src/pier-result-grader.ts +108 -0
- package/src/pier-task-outcome.test.mts +117 -0
- package/src/pier-task-outcome.ts +240 -0
- package/src/pier-trial-controller.test.mts +412 -0
- package/src/pier-trial-controller.ts +858 -0
- package/src/pier-trial-supervisor.mjs +352 -0
- package/src/resolve-client.ts +25 -2
- package/src/run-benchmarks-cli.mts +72 -0
- package/src/run-benchmarks-report.ts +66 -0
- package/src/run-benchmarks.test.mts +231 -0
- package/src/run-benchmarks.ts +589 -0
- package/src/smoke-structural-rollout.mts +393 -0
- package/src/swe-bench-env.test.ts +207 -0
- package/src/swe-bench-env.ts +554 -0
- package/src/swe-jail.ts +293 -0
- package/src/swe-self-improve.mts +84 -0
- package/src/swe-structural-judge-policy.test.ts +117 -0
- package/src/swe-structural-judge-policy.ts +133 -0
- package/src/swe-structural-policy.test.ts +124 -0
- package/src/swe-structural-policy.ts +132 -0
- package/src/swe-structural-provenance.test.ts +93 -0
- package/src/swe-structural-provenance.ts +138 -0
- package/src/swe-structural.mts +1260 -0
- package/src/swe-temp.ts +14 -0
- package/src/tb-container-executor.mts +234 -0
- package/src/tb-container-executor.test.mts +99 -0
- package/src/tb-supervisor-sidecar.mts +222 -0
- package/src/trata-gepa.mts +1 -1
- package/steerers/eops-itsm-population.json +1 -0
- package/tb_agents/opencode_refine_agent.py +117 -0
- package/tb_agents/opencode_router_agent.py +406 -0
- package/tb_agents/opencode_supervisor_agent.py +239 -0
- package/tb_agents/script_agent.py +66 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DABStep adapter (EnvCommons/DABStep) — data-analysis questions over synthetic
|
|
3
|
+
* payment files. Worker artifact = final answer text. Judge = the official
|
|
4
|
+
* DABStep `grade.py` normalization/matching function. No LLM judge.
|
|
5
|
+
*
|
|
6
|
+
* Live tasks require `DABSTEP_DIR` pointing at an official DABStep checkout that
|
|
7
|
+
* includes `dataset.csv`, `splits/*.txt`, `files/*`, and `grade.py`. The adapter
|
|
8
|
+
* exposes `metadata.resourceRoot` so runners can mount the benchmark files into
|
|
9
|
+
* AgentProfile.resources.files; it does not paste the dataset into prompt text.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { access, readFile, stat } from 'node:fs/promises'
|
|
14
|
+
import type { OutputAdapter } from '@tangle-network/agent-runtime/loops'
|
|
15
|
+
import { benchRoot, runVenvPython, runVenvScriptStdin } from './_harness'
|
|
16
|
+
import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
|
|
17
|
+
|
|
18
|
+
const FIXTURES = join(benchRoot, 'fixtures', 'dabstep.json')
|
|
19
|
+
const DEFAULT_SPLIT = 'easy'
|
|
20
|
+
|
|
21
|
+
interface DabstepFixtureRow {
|
|
22
|
+
task_id: number
|
|
23
|
+
instructions: string
|
|
24
|
+
all_golds_by_task: Array<Record<string, unknown>>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface DabstepMeta {
|
|
28
|
+
taskId: number
|
|
29
|
+
split: string
|
|
30
|
+
golds: Array<Record<string, unknown>>
|
|
31
|
+
resourceRoot?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const dabstepDir = (): string | undefined => process.env.DABSTEP_DIR
|
|
35
|
+
const gradeFile = (dir: string): string => join(dir, 'grade.py')
|
|
36
|
+
const resourceRoot = (dir: string): string => join(dir, 'files')
|
|
37
|
+
|
|
38
|
+
async function assertFile(path: string, label: string): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
await access(path)
|
|
41
|
+
} catch (err) {
|
|
42
|
+
throw new Error(`DABStep: missing ${label} at ${path} (${err instanceof Error ? err.message : err})`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function assertOfficialFiles(dir: string, split: string): Promise<void> {
|
|
47
|
+
await assertFile(join(dir, 'dataset.csv'), 'released dataset.csv')
|
|
48
|
+
await assertFile(join(dir, 'splits', `${split}.txt`), `${split} split file`)
|
|
49
|
+
await assertFile(gradeFile(dir), 'official grade.py')
|
|
50
|
+
const files = resourceRoot(dir)
|
|
51
|
+
try {
|
|
52
|
+
const s = await stat(files)
|
|
53
|
+
if (!s.isDirectory()) throw new Error('not a directory')
|
|
54
|
+
} catch (err) {
|
|
55
|
+
throw new Error(`DABStep: missing benchmark files directory at ${files} (${err instanceof Error ? err.message : err})`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const dabstepAnswerOutput: OutputAdapter<string> = {
|
|
60
|
+
parse(events) {
|
|
61
|
+
let text = ''
|
|
62
|
+
for (const ev of events) {
|
|
63
|
+
const d = (ev as { data?: Record<string, unknown> })?.data
|
|
64
|
+
const t = d?.finalText ?? d?.text ?? d?.result
|
|
65
|
+
if (typeof t === 'string' && t.length > 0) text = t
|
|
66
|
+
}
|
|
67
|
+
const fences = [...text.matchAll(/```(?:text|answer)?\s*\n([\s\S]*?)```/g)]
|
|
68
|
+
return (fences.at(-1)?.[1] ?? text).trim()
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function rowToTask(row: DabstepFixtureRow, split: string, dir?: string): BenchTask {
|
|
73
|
+
const meta: DabstepMeta = {
|
|
74
|
+
taskId: row.task_id,
|
|
75
|
+
split,
|
|
76
|
+
golds: row.all_golds_by_task,
|
|
77
|
+
...(dir ? { resourceRoot: resourceRoot(dir) } : {}),
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
id: String(row.task_id),
|
|
81
|
+
split,
|
|
82
|
+
prompt: [
|
|
83
|
+
'Solve this DABStep data-analysis task using the mounted payment files.',
|
|
84
|
+
'Use code or shell commands as needed, then return only the final answer.',
|
|
85
|
+
'',
|
|
86
|
+
row.instructions,
|
|
87
|
+
].join('\n'),
|
|
88
|
+
metadata: meta as unknown as Record<string, unknown>,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readMeta(task: BenchTask): DabstepMeta {
|
|
93
|
+
const md = task.metadata
|
|
94
|
+
if (!md || typeof md.taskId !== 'number' || !Array.isArray(md.golds)) {
|
|
95
|
+
throw new Error(`dabstep task ${task.id} missing metadata — loadTasks did not populate it`)
|
|
96
|
+
}
|
|
97
|
+
return md as unknown as DabstepMeta
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function selectRows(rows: DabstepFixtureRow[], opts: LoadOptions, split: string, dir?: string): BenchTask[] {
|
|
101
|
+
let tasks = rows.map((row) => rowToTask(row, split, dir))
|
|
102
|
+
if (opts.ids) {
|
|
103
|
+
const want = new Set(opts.ids)
|
|
104
|
+
tasks = tasks.filter((task) => want.has(task.id))
|
|
105
|
+
} else if (opts.limit !== undefined) {
|
|
106
|
+
tasks = tasks.slice(0, opts.limit)
|
|
107
|
+
}
|
|
108
|
+
if (tasks.length === 0) throw new Error(`DABStep: no tasks matched ${JSON.stringify(opts)} for split=${split}`)
|
|
109
|
+
return tasks
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function loadFixtures(opts: LoadOptions, split: string): Promise<BenchTask[]> {
|
|
113
|
+
const rows = JSON.parse(await readFile(FIXTURES, 'utf8')) as DabstepFixtureRow[]
|
|
114
|
+
console.warn(`[dabstep] DABSTEP_FIXTURES=1 — loading ${rows.length} adapter fixtures from ${FIXTURES}`)
|
|
115
|
+
return selectRows(rows, opts, split)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function loadOfficialTasks(dir: string, opts: LoadOptions, split: string): Promise<BenchTask[]> {
|
|
119
|
+
const script = `
|
|
120
|
+
import ast, csv, json, sys
|
|
121
|
+
from pathlib import Path
|
|
122
|
+
|
|
123
|
+
root = Path(sys.argv[1])
|
|
124
|
+
split = sys.argv[2]
|
|
125
|
+
limit = None if sys.argv[3] == "" else int(sys.argv[3])
|
|
126
|
+
ids = set(json.loads(sys.argv[4]))
|
|
127
|
+
dataset = root / "dataset.csv"
|
|
128
|
+
split_file = root / "splits" / f"{split}.txt"
|
|
129
|
+
if not dataset.exists():
|
|
130
|
+
raise SystemExit(f"missing official DABStep dataset.csv at {dataset}")
|
|
131
|
+
if not split_file.exists():
|
|
132
|
+
raise SystemExit(f"missing official DABStep split file at {split_file}")
|
|
133
|
+
split_ids = {int(line.strip()) for line in split_file.read_text().splitlines() if line.strip()}
|
|
134
|
+
out = []
|
|
135
|
+
with dataset.open(newline="") as f:
|
|
136
|
+
for row in csv.DictReader(f):
|
|
137
|
+
task_id = int(row["task_id"])
|
|
138
|
+
if task_id not in split_ids:
|
|
139
|
+
continue
|
|
140
|
+
if ids and str(task_id) not in ids:
|
|
141
|
+
continue
|
|
142
|
+
out.append({
|
|
143
|
+
"task_id": task_id,
|
|
144
|
+
"instructions": f"{row['question']}\\n{row['guidelines']}",
|
|
145
|
+
"all_golds_by_task": ast.literal_eval(str(row["all_golds_by_task"])),
|
|
146
|
+
})
|
|
147
|
+
if limit is not None and len(out) >= limit:
|
|
148
|
+
break
|
|
149
|
+
if not out:
|
|
150
|
+
raise SystemExit(f"no DABStep rows matched split={split} ids={sorted(ids)} limit={limit}")
|
|
151
|
+
print(json.dumps(out))
|
|
152
|
+
`
|
|
153
|
+
const stdout = await runVenvPython(script, [dir, split, opts.limit === undefined ? '' : String(opts.limit), JSON.stringify(opts.ids ?? [])])
|
|
154
|
+
return selectRows(JSON.parse(stdout) as DabstepFixtureRow[], opts, split, dir)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function createDabstepAdapter(): BenchmarkAdapter {
|
|
158
|
+
const fixturesMode = process.env.DABSTEP_FIXTURES === '1'
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
name: 'dabstep',
|
|
162
|
+
output: dabstepAnswerOutput,
|
|
163
|
+
|
|
164
|
+
async preflight() {
|
|
165
|
+
if (fixturesMode) return
|
|
166
|
+
const dir = dabstepDir()
|
|
167
|
+
if (!dir) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
'DABSTEP_DIR is required. Fix: clone https://github.com/EnvCommons/DABStep, add the released dataset.csv under that checkout, then set DABSTEP_DIR=/path/to/DABStep.',
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
await assertOfficialFiles(dir, DEFAULT_SPLIT)
|
|
173
|
+
await loadOfficialTasks(dir, { limit: 1 }, DEFAULT_SPLIT)
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async loadTasks(opts: LoadOptions = {}) {
|
|
177
|
+
const split = opts.split ?? DEFAULT_SPLIT
|
|
178
|
+
if (fixturesMode) return loadFixtures(opts, split)
|
|
179
|
+
const dir = dabstepDir()
|
|
180
|
+
if (!dir) throw new Error('DABSTEP_DIR is required to load official DABStep tasks')
|
|
181
|
+
return loadOfficialTasks(dir, opts, split)
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async goldArtifact(task: BenchTask) {
|
|
185
|
+
const meta = readMeta(task)
|
|
186
|
+
const first = meta.golds[0]
|
|
187
|
+
if (!first) return undefined
|
|
188
|
+
const value = first.value
|
|
189
|
+
return value === undefined ? undefined : String(value)
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
|
|
193
|
+
const meta = readMeta(task)
|
|
194
|
+
const dir = dabstepDir()
|
|
195
|
+
if (!dir) throw new Error('DABSTEP_DIR is required to judge DABStep tasks with the official grade.py')
|
|
196
|
+
const stdout = await runVenvScriptStdin(
|
|
197
|
+
join(benchRoot, 'scripts', 'dabstep_judge.py'),
|
|
198
|
+
['--grade-file', gradeFile(dir)],
|
|
199
|
+
JSON.stringify({ prediction: artifact, golds: meta.golds }),
|
|
200
|
+
{ cwd: benchRoot },
|
|
201
|
+
)
|
|
202
|
+
const report = JSON.parse(stdout.trim().split('\n').at(-1) ?? '{}') as { correct?: boolean; score?: number; error?: string }
|
|
203
|
+
if (report.error) throw new Error(`DABStep judge error for ${task.id}: ${report.error}`)
|
|
204
|
+
const score = typeof report.score === 'number' ? report.score : 0
|
|
205
|
+
return {
|
|
206
|
+
resolved: report.correct === true,
|
|
207
|
+
score,
|
|
208
|
+
detail: JSON.stringify({ taskId: meta.taskId, split: meta.split, correct: report.correct }),
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { createAgentBenchAdapter } from './agentbench'
|
|
7
|
+
import { createBfclAdapter } from './bfcl'
|
|
8
|
+
import { createFinResearchBenchAdapter } from './finresearchbench'
|
|
9
|
+
import { createTau2BenchAdapter } from './tau2-bench'
|
|
10
|
+
import { createTau3BankingAdapter } from './tau3-banking'
|
|
11
|
+
import { createToolLlmAdapter } from './toollm'
|
|
12
|
+
import { createWebArenaVerifiedAdapter } from './webarena-verified'
|
|
13
|
+
|
|
14
|
+
async function withEnv<T>(patch: Record<string, string | undefined>, fn: () => Promise<T>): Promise<T> {
|
|
15
|
+
const old: Record<string, string | undefined> = {}
|
|
16
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
17
|
+
old[key] = process.env[key]
|
|
18
|
+
if (value === undefined) delete process.env[key]
|
|
19
|
+
else process.env[key] = value
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return await fn()
|
|
23
|
+
} finally {
|
|
24
|
+
for (const [key, value] of Object.entries(old)) {
|
|
25
|
+
if (value === undefined) delete process.env[key]
|
|
26
|
+
else process.env[key] = value
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('WebArena-Verified fixture mode loads tasks and live mode fails loud without checkout', async () => {
|
|
32
|
+
await withEnv({ WEBARENA_VERIFIED_FIXTURES: '1', WEBARENA_VERIFIED_DIR: undefined }, async () => {
|
|
33
|
+
const adapter = createWebArenaVerifiedAdapter()
|
|
34
|
+
await adapter.preflight()
|
|
35
|
+
const tasks = await adapter.loadTasks({ limit: 1 })
|
|
36
|
+
assert.equal(tasks.length, 1)
|
|
37
|
+
assert.equal(tasks[0].id, '0')
|
|
38
|
+
assert.match(tasks[0].prompt, /WebArena-Verified/)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
await withEnv({ WEBARENA_VERIFIED_FIXTURES: undefined, WEBARENA_VERIFIED_DIR: undefined }, async () => {
|
|
42
|
+
await assert.rejects(() => createWebArenaVerifiedAdapter().preflight(), /WEBARENA_VERIFIED_DIR is required/)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('tau2-bench fixture mode loads tasks and live mode fails loud without checkout', async () => {
|
|
47
|
+
await withEnv({ TAU2_FIXTURES: '1', TAU2_BENCH_DIR: undefined }, async () => {
|
|
48
|
+
const adapter = createTau2BenchAdapter()
|
|
49
|
+
await adapter.preflight()
|
|
50
|
+
const tasks = await adapter.loadTasks({ limit: 1 })
|
|
51
|
+
assert.equal(tasks.length, 1)
|
|
52
|
+
assert.equal(tasks[0].id, 'retail-fixture-0')
|
|
53
|
+
assert.match(tasks[0].prompt, /tau2 task/)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
await withEnv({ TAU2_FIXTURES: undefined, TAU2_BENCH_DIR: undefined }, async () => {
|
|
57
|
+
await assert.rejects(() => createTau2BenchAdapter().preflight(), /TAU2_BENCH_DIR is required/)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('tau3-banking fixture mode loads tasks and live mode fails loud without checkout', async () => {
|
|
62
|
+
await withEnv({ TAU3_FIXTURES: '1', TAU3_BENCH_DIR: undefined }, async () => {
|
|
63
|
+
const adapter = createTau3BankingAdapter()
|
|
64
|
+
await adapter.preflight()
|
|
65
|
+
const tasks = await adapter.loadTasks({ limit: 1 })
|
|
66
|
+
assert.equal(tasks.length, 1)
|
|
67
|
+
assert.equal(tasks[0].id, 'banking-knowledge-fixture-0')
|
|
68
|
+
assert.match(tasks[0].prompt, /tau3 banking task/)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
await withEnv({ TAU3_FIXTURES: undefined, TAU3_BENCH_DIR: undefined }, async () => {
|
|
72
|
+
await assert.rejects(() => createTau3BankingAdapter().preflight(), /TAU3_BENCH_DIR is required/)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('AgentBench DBBench fixture mode loads and exact-label judge scores deterministically', async () => {
|
|
77
|
+
await withEnv({ AGENTBENCH_FIXTURES: '1', AGENTBENCH_DIR: undefined }, async () => {
|
|
78
|
+
const adapter = createAgentBenchAdapter()
|
|
79
|
+
await adapter.preflight()
|
|
80
|
+
const [task] = await adapter.loadTasks({ limit: 1 })
|
|
81
|
+
assert.equal(await adapter.goldArtifact(task), 'Women +60kg Bronze')
|
|
82
|
+
assert.equal((await adapter.judge(task, 'women +60kg bronze')).score, 1)
|
|
83
|
+
assert.equal((await adapter.judge(task, 'wrong')).score, 0)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
await withEnv({ AGENTBENCH_FIXTURES: undefined, AGENTBENCH_DIR: undefined }, async () => {
|
|
87
|
+
await assert.rejects(() => createAgentBenchAdapter().preflight(), /AGENTBENCH_DIR is required/)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('BFCL fixture mode loads official-shaped rows and scores function calls', async () => {
|
|
92
|
+
await withEnv({ BFCL_FIXTURES: '1', BFCL_DIR: undefined }, async () => {
|
|
93
|
+
const adapter = createBfclAdapter()
|
|
94
|
+
await adapter.preflight()
|
|
95
|
+
const [task] = await adapter.loadTasks({ limit: 1 })
|
|
96
|
+
assert.equal(task.id, 'simple_python_fixture_0')
|
|
97
|
+
const gold = await adapter.goldArtifact(task)
|
|
98
|
+
assert.match(gold ?? '', /calculate_triangle_area/)
|
|
99
|
+
assert.equal((await adapter.judge(task, gold ?? '')).score, 1)
|
|
100
|
+
assert.equal((await adapter.judge(task, '{"function_calls":[{"name":"wrong","arguments":{}}]}')).score, 0)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
await withEnv({ BFCL_FIXTURES: undefined, BFCL_DIR: undefined }, async () => {
|
|
104
|
+
await assert.rejects(() => createBfclAdapter().preflight(), /BFCL_DIR is required/)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('ToolLLM scores only deterministic API-selection labels and rejects unlabeled rows', async () => {
|
|
109
|
+
await withEnv({ TOOLLM_FIXTURES: '1', TOOLBENCH_DIR: undefined }, async () => {
|
|
110
|
+
const adapter = createToolLlmAdapter()
|
|
111
|
+
await adapter.preflight()
|
|
112
|
+
const [task] = await adapter.loadTasks({ limit: 1 })
|
|
113
|
+
assert.equal(task.id, '1')
|
|
114
|
+
const gold = await adapter.goldArtifact(task)
|
|
115
|
+
assert.match(gold ?? '', /Checkhealth/)
|
|
116
|
+
assert.equal((await adapter.judge(task, '')).score, 0)
|
|
117
|
+
assert.equal((await adapter.judge(task, gold ?? '')).score, 1)
|
|
118
|
+
assert.equal((await adapter.judge(task, '{"api_calls":[{"tool_name":"SQUAKE","api_name":"Checkhealth"}]}')).score, 0.5)
|
|
119
|
+
const freeText = await adapter.judge(task, 'Use SQUAKE Checkhealth, then call SQUAKE Projects.')
|
|
120
|
+
assert.equal(freeText.score, 1)
|
|
121
|
+
assert.equal(freeText.resolved, false)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const dir = await mkdtemp(join(tmpdir(), 'toollm-'))
|
|
125
|
+
try {
|
|
126
|
+
const query = join(dir, 'queries.json')
|
|
127
|
+
await writeFile(query, JSON.stringify([{ query_id: 1, query: 'Call a tool.', api_list: [] }]))
|
|
128
|
+
await withEnv({ TOOLLM_FIXTURES: undefined, TOOLBENCH_DIR: dir, TOOLLM_QUERY_FILE: query }, async () => {
|
|
129
|
+
await assert.rejects(() => createToolLlmAdapter().preflight(), /deterministic API-selection labels missing/)
|
|
130
|
+
})
|
|
131
|
+
} finally {
|
|
132
|
+
await rm(dir, { recursive: true, force: true })
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('FinResearchBench fixture mode is explicit and live mode requires exported judge rows', async () => {
|
|
137
|
+
await withEnv({ FINRESEARCHBENCH_FIXTURES: '1', FINRESEARCHBENCH_DATA_FILE: undefined }, async () => {
|
|
138
|
+
const adapter = createFinResearchBenchAdapter()
|
|
139
|
+
await adapter.preflight()
|
|
140
|
+
const [task] = await adapter.loadTasks({ limit: 1 })
|
|
141
|
+
const gold = await adapter.goldArtifact(task)
|
|
142
|
+
assert.match(gold ?? '', /Margin expansion/)
|
|
143
|
+
assert.equal((await adapter.judge(task, gold ?? '')).score, 1)
|
|
144
|
+
assert.equal((await adapter.judge(task, 'unrelated answer')).score, 0)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
await withEnv({ FINRESEARCHBENCH_FIXTURES: undefined, FINRESEARCHBENCH_DATA_FILE: undefined }, async () => {
|
|
148
|
+
await assert.rejects(() => createFinResearchBenchAdapter().preflight(), /FINRESEARCHBENCH_DATA_FILE is required/)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FinResearchBench-compatible adapter.
|
|
3
|
+
*
|
|
4
|
+
* The paper defines a logic-tree Agent-as-a-Judge benchmark for financial
|
|
5
|
+
* research reports, but there is no stable public scorer package wired here.
|
|
6
|
+
* Live mode therefore requires a local data export whose rows carry the official
|
|
7
|
+
* judge prompt/template/logic tree. The adapter refuses to invent a judge.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFile, stat } from 'node:fs/promises'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import { benchRoot } from './_harness'
|
|
13
|
+
import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
|
|
14
|
+
|
|
15
|
+
const FIXTURES = join(benchRoot, 'fixtures', 'finresearchbench.json')
|
|
16
|
+
|
|
17
|
+
interface FinResearchRecord {
|
|
18
|
+
id: string
|
|
19
|
+
question: string
|
|
20
|
+
category?: string
|
|
21
|
+
reference_answer?: string
|
|
22
|
+
reference_report?: string
|
|
23
|
+
logic_tree?: unknown
|
|
24
|
+
rubric?: unknown
|
|
25
|
+
judge_system_prompt?: string
|
|
26
|
+
judge_prompt_template?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface FinResearchMeta {
|
|
30
|
+
id: string
|
|
31
|
+
category: string
|
|
32
|
+
question: string
|
|
33
|
+
referenceAnswer: string
|
|
34
|
+
referenceReport: string
|
|
35
|
+
logicTree: unknown
|
|
36
|
+
rubric: unknown
|
|
37
|
+
judgeSystemPrompt?: string
|
|
38
|
+
judgePromptTemplate?: string
|
|
39
|
+
scoring: 'official-logic-tree-judge' | 'fixture-exact-reference'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const dataFile = (): string | undefined => process.env.FINRESEARCHBENCH_DATA_FILE
|
|
43
|
+
|
|
44
|
+
function routerConfig(): { baseUrl: string; key: string; model: string } {
|
|
45
|
+
const key = process.env.TANGLE_API_KEY
|
|
46
|
+
if (!key) throw new Error('TANGLE_API_KEY is required for FinResearchBench live LLM judging')
|
|
47
|
+
return {
|
|
48
|
+
baseUrl: process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1',
|
|
49
|
+
key,
|
|
50
|
+
model: process.env.FINRESEARCHBENCH_JUDGE_MODEL ?? process.env.JUDGE_MODEL ?? 'deepseek-v4-flash',
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function assertReadable(path: string, label: string): Promise<void> {
|
|
55
|
+
try {
|
|
56
|
+
await stat(path)
|
|
57
|
+
} catch (err) {
|
|
58
|
+
throw new Error(`FinResearchBench: missing ${label} at ${path} (${err instanceof Error ? err.message : err})`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readRecords(raw: string): FinResearchRecord[] {
|
|
63
|
+
const trimmed = raw.trim()
|
|
64
|
+
if (trimmed.startsWith('[')) return JSON.parse(trimmed) as FinResearchRecord[]
|
|
65
|
+
return trimmed
|
|
66
|
+
.split(/\r?\n/)
|
|
67
|
+
.map((line) => line.trim())
|
|
68
|
+
.filter((line) => line.length > 0)
|
|
69
|
+
.map((line) => JSON.parse(line) as FinResearchRecord)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assertLiveJudgeFields(records: readonly FinResearchRecord[], source: string): void {
|
|
73
|
+
const missing = records
|
|
74
|
+
.filter((row) => !row.judge_system_prompt || !row.judge_prompt_template)
|
|
75
|
+
.map((row) => row.id)
|
|
76
|
+
if (missing.length > 0) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`FinResearchBench live rows from ${source} missing official judge prompts for ${missing.length}/${records.length} row(s): ${missing.slice(0, 5).join(', ')}. ` +
|
|
79
|
+
'Use a benchmark export with judge_system_prompt and judge_prompt_template, or do not score this benchmark live.',
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rowToTask(row: FinResearchRecord, fixturesMode: boolean): BenchTask {
|
|
85
|
+
const referenceAnswer = row.reference_answer ?? ''
|
|
86
|
+
const referenceReport = row.reference_report ?? referenceAnswer
|
|
87
|
+
const meta: FinResearchMeta = {
|
|
88
|
+
id: row.id,
|
|
89
|
+
category: row.category ?? 'unknown',
|
|
90
|
+
question: row.question,
|
|
91
|
+
referenceAnswer,
|
|
92
|
+
referenceReport,
|
|
93
|
+
logicTree: row.logic_tree ?? null,
|
|
94
|
+
rubric: row.rubric ?? null,
|
|
95
|
+
judgeSystemPrompt: row.judge_system_prompt,
|
|
96
|
+
judgePromptTemplate: row.judge_prompt_template,
|
|
97
|
+
scoring: fixturesMode ? 'fixture-exact-reference' : 'official-logic-tree-judge',
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
id: row.id,
|
|
101
|
+
split: row.category,
|
|
102
|
+
prompt: [
|
|
103
|
+
'Complete this FinResearchBench financial research task.',
|
|
104
|
+
'Produce a decision-grade research answer with explicit reasoning, evidence, and final conclusion.',
|
|
105
|
+
'',
|
|
106
|
+
row.question,
|
|
107
|
+
].join('\n'),
|
|
108
|
+
metadata: meta as unknown as Record<string, unknown>,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readMeta(task: BenchTask): FinResearchMeta {
|
|
113
|
+
const md = task.metadata
|
|
114
|
+
if (!md || typeof md.question !== 'string') {
|
|
115
|
+
throw new Error(`FinResearchBench task ${task.id} missing metadata — loadTasks did not populate it`)
|
|
116
|
+
}
|
|
117
|
+
return md as unknown as FinResearchMeta
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function selectRows(rows: FinResearchRecord[], opts: LoadOptions, fixturesMode: boolean): BenchTask[] {
|
|
121
|
+
let tasks = rows.map((row) => rowToTask(row, fixturesMode))
|
|
122
|
+
if (opts.split) tasks = tasks.filter((task) => task.split === opts.split)
|
|
123
|
+
if (opts.ids) {
|
|
124
|
+
const want = new Set(opts.ids)
|
|
125
|
+
tasks = tasks.filter((task) => want.has(task.id))
|
|
126
|
+
} else if (opts.limit !== undefined) {
|
|
127
|
+
tasks = tasks.slice(0, opts.limit)
|
|
128
|
+
}
|
|
129
|
+
if (tasks.length === 0) throw new Error(`FinResearchBench: no tasks matched ${JSON.stringify(opts)}`)
|
|
130
|
+
return tasks
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function loadFixtures(opts: LoadOptions): Promise<BenchTask[]> {
|
|
134
|
+
const records = readRecords(await readFile(FIXTURES, 'utf8'))
|
|
135
|
+
console.warn(`[finresearchbench] FINRESEARCHBENCH_FIXTURES=1 — loading ${records.length} adapter fixtures`)
|
|
136
|
+
return selectRows(records, opts, true)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function loadOfficialTasks(path: string, opts: LoadOptions): Promise<BenchTask[]> {
|
|
140
|
+
const records = readRecords(await readFile(path, 'utf8'))
|
|
141
|
+
assertLiveJudgeFields(records, path)
|
|
142
|
+
return selectRows(records, opts, false)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function fillTemplate(meta: FinResearchMeta, response: string): string {
|
|
146
|
+
const template = meta.judgePromptTemplate
|
|
147
|
+
if (!template) throw new Error(`FinResearchBench task ${meta.id} missing judge_prompt_template`)
|
|
148
|
+
return template
|
|
149
|
+
.replaceAll('{question}', meta.question)
|
|
150
|
+
.replaceAll('{response}', response)
|
|
151
|
+
.replaceAll('{reference_answer}', meta.referenceAnswer)
|
|
152
|
+
.replaceAll('{reference_report}', meta.referenceReport)
|
|
153
|
+
.replaceAll('{logic_tree}', JSON.stringify(meta.logicTree, null, 2))
|
|
154
|
+
.replaceAll('{rubric}', JSON.stringify(meta.rubric, null, 2))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseJudgeScore(content: string): { score: number; raw: unknown } {
|
|
158
|
+
const candidates: string[] = []
|
|
159
|
+
for (const m of content.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)) candidates.push(m[1].trim())
|
|
160
|
+
for (const m of content.matchAll(/\{[\s\S]*?\}/g)) candidates.push(m[0])
|
|
161
|
+
candidates.push(content.trim())
|
|
162
|
+
for (const candidate of candidates) {
|
|
163
|
+
let parsed: Record<string, unknown>
|
|
164
|
+
try {
|
|
165
|
+
parsed = JSON.parse(candidate) as Record<string, unknown>
|
|
166
|
+
} catch {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
const raw = parsed.score ?? parsed.overall_score ?? parsed.total_score ?? parsed.answer_score
|
|
170
|
+
const n = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number(raw) : NaN
|
|
171
|
+
if (!Number.isFinite(n)) continue
|
|
172
|
+
if (n < 0) throw new Error(`FinResearchBench judge score is negative: ${JSON.stringify(parsed)}`)
|
|
173
|
+
const score = n <= 1 ? n : n <= 10 ? n / 10 : n / 100
|
|
174
|
+
if (score > 1) throw new Error(`FinResearchBench judge score outside supported range: ${JSON.stringify(parsed)}`)
|
|
175
|
+
return { score, raw: parsed }
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`FinResearchBench judge produced no parseable JSON score: ${content.slice(0, 400)}`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function runOfficialJudge(meta: FinResearchMeta, response: string): Promise<BenchScore> {
|
|
181
|
+
if (!meta.judgeSystemPrompt) throw new Error(`FinResearchBench task ${meta.id} missing judge_system_prompt`)
|
|
182
|
+
const router = routerConfig()
|
|
183
|
+
const res = await fetch(`${router.baseUrl}/chat/completions`, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${router.key}` },
|
|
186
|
+
body: JSON.stringify({
|
|
187
|
+
model: router.model,
|
|
188
|
+
temperature: 0,
|
|
189
|
+
messages: [
|
|
190
|
+
{ role: 'system', content: meta.judgeSystemPrompt },
|
|
191
|
+
{ role: 'user', content: fillTemplate(meta, response) },
|
|
192
|
+
],
|
|
193
|
+
}),
|
|
194
|
+
})
|
|
195
|
+
if (!res.ok) throw new Error(`FinResearchBench judge HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`)
|
|
196
|
+
const body = (await res.json()) as { choices?: Array<{ message?: { content?: string } }> }
|
|
197
|
+
const content = body.choices?.[0]?.message?.content
|
|
198
|
+
if (typeof content !== 'string') throw new Error(`FinResearchBench judge returned no message content: ${JSON.stringify(body).slice(0, 300)}`)
|
|
199
|
+
const { score, raw } = parseJudgeScore(content)
|
|
200
|
+
return {
|
|
201
|
+
resolved: score >= Number(process.env.FINRESEARCHBENCH_PASS_THRESHOLD ?? 0.8),
|
|
202
|
+
score,
|
|
203
|
+
detail: JSON.stringify({ scoring: meta.scoring, category: meta.category, judgeModel: router.model, raw }),
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeText(value: string): string {
|
|
208
|
+
return value.toLowerCase().replace(/\s+/g, ' ').trim()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function scoreFixture(meta: FinResearchMeta, artifact: string): BenchScore {
|
|
212
|
+
const answer = normalizeText(meta.referenceAnswer || meta.referenceReport)
|
|
213
|
+
const response = normalizeText(artifact)
|
|
214
|
+
const score = answer.length > 0 && response.includes(answer) ? 1 : 0
|
|
215
|
+
return {
|
|
216
|
+
resolved: score === 1,
|
|
217
|
+
score,
|
|
218
|
+
detail: JSON.stringify({ scoring: meta.scoring, category: meta.category }),
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function createFinResearchBenchAdapter(): BenchmarkAdapter {
|
|
223
|
+
const fixturesMode = process.env.FINRESEARCHBENCH_FIXTURES === '1'
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
name: 'finresearchbench',
|
|
227
|
+
|
|
228
|
+
async preflight() {
|
|
229
|
+
if (fixturesMode) {
|
|
230
|
+
await assertReadable(FIXTURES, 'fixture file')
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
const file = dataFile()
|
|
234
|
+
if (!file) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
'FINRESEARCHBENCH_DATA_FILE is required. Fix: export the official FinResearchBench rows as JSON/JSONL with judge_system_prompt and judge_prompt_template fields, then set FINRESEARCHBENCH_DATA_FILE=/path/to/export.jsonl.',
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
routerConfig()
|
|
240
|
+
await assertReadable(file, 'data file')
|
|
241
|
+
await loadOfficialTasks(file, { limit: 1 })
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
async loadTasks(opts: LoadOptions = {}) {
|
|
245
|
+
if (fixturesMode) return loadFixtures(opts)
|
|
246
|
+
const file = dataFile()
|
|
247
|
+
if (!file) throw new Error('FINRESEARCHBENCH_DATA_FILE is required to load FinResearchBench rows')
|
|
248
|
+
return loadOfficialTasks(file, opts)
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
async goldArtifact(task: BenchTask) {
|
|
252
|
+
const meta = readMeta(task)
|
|
253
|
+
return meta.referenceReport || meta.referenceAnswer || undefined
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
|
|
257
|
+
const meta = readMeta(task)
|
|
258
|
+
if (fixturesMode) return scoreFixture(meta, artifact)
|
|
259
|
+
if (artifact.trim().length === 0) {
|
|
260
|
+
return {
|
|
261
|
+
resolved: false,
|
|
262
|
+
score: 0,
|
|
263
|
+
detail: JSON.stringify({ scoring: meta.scoring, category: meta.category, reason: 'empty answer' }),
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return runOfficialJudge(meta, artifact)
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { execFile } from 'node:child_process'
|
|
20
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
20
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
21
21
|
import { tmpdir } from 'node:os'
|
|
22
22
|
import { join } from 'node:path'
|
|
23
23
|
import { gunzipSync } from 'node:zlib'
|
|
@@ -41,9 +41,17 @@ export interface HumanEvalTask {
|
|
|
41
41
|
* selects a deeper slice (the later tasks are harder) so the worker has a
|
|
42
42
|
* correctable middle band rather than a saturated easy prefix. */
|
|
43
43
|
export async function loadHumanEval(limit: number, offset = 0): Promise<HumanEvalTask[]> {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
44
|
+
// Prefer a locally-cached .jsonl.gz (HUMANEVAL_GZ) — the GitHub raw URL rate-limits
|
|
45
|
+
// (429) under repeated runs. Fall back to the network fetch when unset.
|
|
46
|
+
const localGz = process.env.HUMANEVAL_GZ
|
|
47
|
+
let gz: Buffer
|
|
48
|
+
if (localGz) {
|
|
49
|
+
gz = readFileSync(localGz)
|
|
50
|
+
} else {
|
|
51
|
+
const res = await fetch(humanevalUrl)
|
|
52
|
+
if (!res.ok) throw new Error(`HumanEval fetch HTTP ${res.status}: ${humanevalUrl}`)
|
|
53
|
+
gz = Buffer.from(await res.arrayBuffer())
|
|
54
|
+
}
|
|
47
55
|
const text = gunzipSync(gz).toString('utf8')
|
|
48
56
|
const tasks: HumanEvalTask[] = []
|
|
49
57
|
for (const line of text.split('\n')) {
|
|
@@ -242,10 +250,14 @@ export function createHumanEvalAdapter(): BenchmarkAdapter {
|
|
|
242
250
|
return { resolved: pass === 1, score: pass, detail: pass === 1 ? 'tests passed' : 'tests failed' }
|
|
243
251
|
},
|
|
244
252
|
async goldArtifact(task: BenchTask) {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
|
|
253
|
+
const m = task.metadata as HumanEvalMeta | undefined
|
|
254
|
+
const sol = m?.canonicalSolution
|
|
255
|
+
// Return the COMPLETE function (signature header + canonical body), i.e. what a
|
|
256
|
+
// real worker emits — NOT the body alone. The judge runs `extractCode`, whose
|
|
257
|
+
// unfenced fallback is `reply.trim()`; trimming a body-only string strips its
|
|
258
|
+
// leading indent and breaks it, so a body-only gold fails its own judge. A full
|
|
259
|
+
// def starts at column 0, trims safely, and self-verifies.
|
|
260
|
+
return sol ? `${m!.promptHeader}${sol}` : undefined
|
|
249
261
|
},
|
|
250
262
|
}
|
|
251
263
|
}
|