agentinputlab 0.1.0a1__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.
Files changed (469) hide show
  1. agentinputlab/__init__.py +28 -0
  2. agentinputlab/algorithms/__init__.py +6 -0
  3. agentinputlab/algorithms/acquire/__init__.py +145 -0
  4. agentinputlab/algorithms/acquire/adaptive_rag/__init__.py +43 -0
  5. agentinputlab/algorithms/acquire/adaptive_rag/replay.py +367 -0
  6. agentinputlab/algorithms/acquire/adaptive_rag/runtime.py +1202 -0
  7. agentinputlab/algorithms/acquire/adaptive_rag_common_support.py +542 -0
  8. agentinputlab/algorithms/acquire/hyde.py +296 -0
  9. agentinputlab/algorithms/acquire/hyde_controlled.py +200 -0
  10. agentinputlab/algorithms/acquire/hyde_external.py +148 -0
  11. agentinputlab/algorithms/acquire/ircot.py +28 -0
  12. agentinputlab/algorithms/acquire/ircot_common_support.py +478 -0
  13. agentinputlab/algorithms/acquire/ircot_native.py +355 -0
  14. agentinputlab/algorithms/acquire/ircot_replay.py +327 -0
  15. agentinputlab/algorithms/acquire/qrag/__init__.py +79 -0
  16. agentinputlab/algorithms/acquire/qrag/bridge.py +828 -0
  17. agentinputlab/algorithms/acquire/qrag/common_support.py +891 -0
  18. agentinputlab/algorithms/acquire/qrag/native_axis.py +71 -0
  19. agentinputlab/algorithms/acquire/qrag/reader_adapter.py +139 -0
  20. agentinputlab/algorithms/acquire/r2ag/__init__.py +57 -0
  21. agentinputlab/algorithms/acquire/r2ag/bridge.py +1312 -0
  22. agentinputlab/algorithms/acquire/r2ag/common_support.py +1029 -0
  23. agentinputlab/algorithms/acquire/r2ag/socket_runtime.py +255 -0
  24. agentinputlab/algorithms/clarify/__init__.py +61 -0
  25. agentinputlab/algorithms/clarify/_structured.py +64 -0
  26. agentinputlab/algorithms/clarify/active_task_disambiguation.py +371 -0
  27. agentinputlab/algorithms/clarify/ask_or_assume/__init__.py +17 -0
  28. agentinputlab/algorithms/clarify/ask_or_assume/monitor.py +410 -0
  29. agentinputlab/algorithms/clarify/future_turns.py +379 -0
  30. agentinputlab/algorithms/clarify/udcs.py +255 -0
  31. agentinputlab/algorithms/clarify/udcs_deterministic.py +196 -0
  32. agentinputlab/algorithms/curate/__init__.py +65 -0
  33. agentinputlab/algorithms/curate/acon/__init__.py +27 -0
  34. agentinputlab/algorithms/curate/acon/common.py +284 -0
  35. agentinputlab/algorithms/curate/acon/common_support.py +136 -0
  36. agentinputlab/algorithms/curate/acon/history_bridge.py +309 -0
  37. agentinputlab/algorithms/curate/acon/observation_bridge.py +290 -0
  38. agentinputlab/algorithms/curate/llmlingua/__init__.py +30 -0
  39. agentinputlab/algorithms/curate/llmlingua/adapters.py +537 -0
  40. agentinputlab/algorithms/curate/llmlingua/budget.py +48 -0
  41. agentinputlab/algorithms/curate/llmlingua/controlled.py +175 -0
  42. agentinputlab/algorithms/curate/llmlingua/multilingual.py +105 -0
  43. agentinputlab/algorithms/curate/llmlingua/span_map.py +167 -0
  44. agentinputlab/algorithms/curate/recomp/__init__.py +47 -0
  45. agentinputlab/algorithms/curate/recomp/external.py +1129 -0
  46. agentinputlab/algorithms/curate/recomp/selective.py +304 -0
  47. agentinputlab/algorithms/decompose/__init__.py +107 -0
  48. agentinputlab/algorithms/decompose/decomp/__init__.py +53 -0
  49. agentinputlab/algorithms/decompose/decomp/common_support.py +478 -0
  50. agentinputlab/algorithms/decompose/decomp/planner_component.py +199 -0
  51. agentinputlab/algorithms/decompose/decomp/worker_bridge.py +416 -0
  52. agentinputlab/algorithms/decompose/llm_compiler/__init__.py +35 -0
  53. agentinputlab/algorithms/decompose/llm_compiler/bridge.py +520 -0
  54. agentinputlab/algorithms/decompose/llm_compiler/common_support.py +504 -0
  55. agentinputlab/algorithms/decompose/plan_and_budget/__init__.py +43 -0
  56. agentinputlab/algorithms/decompose/plan_and_budget/cached_soft_budget.py +362 -0
  57. agentinputlab/algorithms/decompose/plan_and_budget/decomposer_component.py +107 -0
  58. agentinputlab/algorithms/decompose/plan_and_budget/full.py +205 -0
  59. agentinputlab/algorithms/decompose/plan_and_budget/hard_cap.py +246 -0
  60. agentinputlab/algorithms/decompose/plan_and_budget/regenerated_soft_budget.py +448 -0
  61. agentinputlab/algorithms/decompose/plan_and_solve/__init__.py +33 -0
  62. agentinputlab/algorithms/decompose/plan_and_solve/plan_component.py +192 -0
  63. agentinputlab/algorithms/decompose/plan_and_solve/ps_prompt.py +127 -0
  64. agentinputlab/algorithms/decompose/self_ask/__init__.py +27 -0
  65. agentinputlab/algorithms/decompose/self_ask/loop.py +362 -0
  66. agentinputlab/algorithms/decompose/self_ask/planner_component.py +117 -0
  67. agentinputlab/algorithms/reformulate/__init__.py +85 -0
  68. agentinputlab/algorithms/reformulate/infocqr/__init__.py +40 -0
  69. agentinputlab/algorithms/reformulate/infocqr/cache_replay.py +362 -0
  70. agentinputlab/algorithms/reformulate/infocqr/external.py +675 -0
  71. agentinputlab/algorithms/reformulate/infocqr/student.py +189 -0
  72. agentinputlab/algorithms/reformulate/query2doc/__init__.py +42 -0
  73. agentinputlab/algorithms/reformulate/query2doc/controlled_live.py +190 -0
  74. agentinputlab/algorithms/reformulate/query2doc/models.py +95 -0
  75. agentinputlab/algorithms/reformulate/query2doc/official_cached.py +335 -0
  76. agentinputlab/algorithms/reformulate/query2doc/querygym_bridge.py +223 -0
  77. agentinputlab/algorithms/reformulate/query2doc/querygym_runtime.py +326 -0
  78. agentinputlab/algorithms/reformulate/reformer/__init__.py +23 -0
  79. agentinputlab/algorithms/reformulate/reformer/external.py +417 -0
  80. agentinputlab/algorithms/reformulate/rrr.py +696 -0
  81. agentinputlab/annotation/__init__.py +5 -0
  82. agentinputlab/annotation/independent.py +428 -0
  83. agentinputlab/annotation/server.py +233 -0
  84. agentinputlab/annotation/static/app.js +121 -0
  85. agentinputlab/annotation/static/index.html +60 -0
  86. agentinputlab/annotation/static/review.css +2 -0
  87. agentinputlab/annotation/static/review.html +10 -0
  88. agentinputlab/annotation/static/review.js +11 -0
  89. agentinputlab/annotation/static/styles.css +55 -0
  90. agentinputlab/annotation/store.py +444 -0
  91. agentinputlab/api.py +368 -0
  92. agentinputlab/artifacts/__init__.py +15 -0
  93. agentinputlab/artifacts/json_file.py +104 -0
  94. agentinputlab/artifacts/manifest.py +98 -0
  95. agentinputlab/artifacts/store.py +157 -0
  96. agentinputlab/backends/__init__.py +98 -0
  97. agentinputlab/backends/embedding/__init__.py +25 -0
  98. agentinputlab/backends/embedding/base.py +145 -0
  99. agentinputlab/backends/embedding/callable.py +75 -0
  100. agentinputlab/backends/embedding/replay.py +129 -0
  101. agentinputlab/backends/llm/__init__.py +31 -0
  102. agentinputlab/backends/llm/base.py +149 -0
  103. agentinputlab/backends/llm/callable.py +108 -0
  104. agentinputlab/backends/llm/decomp_completion.py +88 -0
  105. agentinputlab/backends/llm/final_content_stop.py +72 -0
  106. agentinputlab/backends/llm/gpt_oss_final_channel.py +84 -0
  107. agentinputlab/backends/llm/openai_compatible.py +332 -0
  108. agentinputlab/backends/llm/transformers.py +171 -0
  109. agentinputlab/backends/retrieval/__init__.py +35 -0
  110. agentinputlab/backends/retrieval/base.py +50 -0
  111. agentinputlab/backends/retrieval/dense.py +777 -0
  112. agentinputlab/backends/retrieval/memory.py +119 -0
  113. agentinputlab/backends/retrieval/sqlite_fts.py +155 -0
  114. agentinputlab/backends/user/__init__.py +15 -0
  115. agentinputlab/backends/user/base.py +66 -0
  116. agentinputlab/backends/user/callable.py +54 -0
  117. agentinputlab/backends/user/replay.py +89 -0
  118. agentinputlab/backends/vector.py +38 -0
  119. agentinputlab/benchmarks/__init__.py +246 -0
  120. agentinputlab/benchmarks/acquisition.py +106 -0
  121. agentinputlab/benchmarks/ale.py +540 -0
  122. agentinputlab/benchmarks/appworld_ul.py +669 -0
  123. agentinputlab/benchmarks/assets.py +226 -0
  124. agentinputlab/benchmarks/base.py +68 -0
  125. agentinputlab/benchmarks/beir_archive.py +235 -0
  126. agentinputlab/benchmarks/beir_sample.py +269 -0
  127. agentinputlab/benchmarks/bfcl_native.py +245 -0
  128. agentinputlab/benchmarks/bfcl_v3.py +607 -0
  129. agentinputlab/benchmarks/cache.py +218 -0
  130. agentinputlab/benchmarks/clariq.py +203 -0
  131. agentinputlab/benchmarks/data/registry.json +321 -0
  132. agentinputlab/benchmarks/data/smoke_assets.json +511 -0
  133. agentinputlab/benchmarks/data/statebench_v0_8_1_task_ids.json +475 -0
  134. agentinputlab/benchmarks/data/toolsandbox_165848b_scenario_names.json +1 -0
  135. agentinputlab/benchmarks/execution.py +1111 -0
  136. agentinputlab/benchmarks/interactive.py +257 -0
  137. agentinputlab/benchmarks/ir_query.py +204 -0
  138. agentinputlab/benchmarks/longbench.py +203 -0
  139. agentinputlab/benchmarks/paired_replay.py +479 -0
  140. agentinputlab/benchmarks/preprocess.py +2313 -0
  141. agentinputlab/benchmarks/production.py +236 -0
  142. agentinputlab/benchmarks/qa.py +963 -0
  143. agentinputlab/benchmarks/registry.py +110 -0
  144. agentinputlab/benchmarks/retrieval.py +459 -0
  145. agentinputlab/benchmarks/runtime.py +509 -0
  146. agentinputlab/benchmarks/statebench.py +938 -0
  147. agentinputlab/benchmarks/tau3.py +725 -0
  148. agentinputlab/benchmarks/toolsandbox.py +626 -0
  149. agentinputlab/benchmarks/trec_dl_sample.py +263 -0
  150. agentinputlab/campaign.py +654 -0
  151. agentinputlab/campaign_runner.py +953 -0
  152. agentinputlab/cli/__init__.py +6 -0
  153. agentinputlab/cli/main.py +933 -0
  154. agentinputlab/connectors/__init__.py +46 -0
  155. agentinputlab/connectors/knowledge/__init__.py +4 -0
  156. agentinputlab/connectors/tools/__init__.py +30 -0
  157. agentinputlab/connectors/tools/base.py +57 -0
  158. agentinputlab/connectors/tools/mcp.py +222 -0
  159. agentinputlab/connectors/tools/memory.py +41 -0
  160. agentinputlab/connectors/tools/openapi.py +695 -0
  161. agentinputlab/connectors/tools/schema.py +102 -0
  162. agentinputlab/contracts/__init__.py +91 -0
  163. agentinputlab/contracts/availability.py +24 -0
  164. agentinputlab/contracts/dependency.py +58 -0
  165. agentinputlab/contracts/enums.py +82 -0
  166. agentinputlab/contracts/errors.py +63 -0
  167. agentinputlab/contracts/evidence.py +43 -0
  168. agentinputlab/contracts/intent.py +110 -0
  169. agentinputlab/contracts/lifecycle.py +164 -0
  170. agentinputlab/contracts/patch.py +167 -0
  171. agentinputlab/contracts/serialization.py +93 -0
  172. agentinputlab/contracts/state.py +127 -0
  173. agentinputlab/contracts/trace.py +59 -0
  174. agentinputlab/contracts/worker.py +42 -0
  175. agentinputlab/core/__init__.py +7 -0
  176. agentinputlab/data/__init__.py +32 -0
  177. agentinputlab/data/checksums.py +9 -0
  178. agentinputlab/data/downloader.py +235 -0
  179. agentinputlab/data/http.py +171 -0
  180. agentinputlab/data/index.py +21 -0
  181. agentinputlab/data/overlap.py +28 -0
  182. agentinputlab/data/preprocess.py +32 -0
  183. agentinputlab/data/storage.py +57 -0
  184. agentinputlab/data/trec_sample.py +427 -0
  185. agentinputlab/evaluation/__init__.py +5 -0
  186. agentinputlab/evaluation/outcomes.py +32 -0
  187. agentinputlab/experiments/__init__.py +278 -0
  188. agentinputlab/experiments/analysis.py +981 -0
  189. agentinputlab/experiments/artifacts.py +687 -0
  190. agentinputlab/experiments/automatic_measurement.py +678 -0
  191. agentinputlab/experiments/auxiliary_matrix.py +203 -0
  192. agentinputlab/experiments/bindings.py +430 -0
  193. agentinputlab/experiments/blueprints.py +147 -0
  194. agentinputlab/experiments/builders/__init__.py +45 -0
  195. agentinputlab/experiments/builders/executable_external_payload.py +423 -0
  196. agentinputlab/experiments/builders/external_process_arena.py +1018 -0
  197. agentinputlab/experiments/builders/registry_intervention.py +463 -0
  198. agentinputlab/experiments/cli.py +158 -0
  199. agentinputlab/experiments/controls.py +340 -0
  200. agentinputlab/experiments/driver_guard.py +205 -0
  201. agentinputlab/experiments/drivers/__init__.py +89 -0
  202. agentinputlab/experiments/drivers/external_arena.py +2908 -0
  203. agentinputlab/experiments/drivers/method_process.py +662 -0
  204. agentinputlab/experiments/drivers/parity_process.py +1484 -0
  205. agentinputlab/experiments/drivers/static_production.py +1574 -0
  206. agentinputlab/experiments/executor.py +448 -0
  207. agentinputlab/experiments/loader.py +605 -0
  208. agentinputlab/experiments/measurement.py +675 -0
  209. agentinputlab/experiments/paper_analysis.py +1818 -0
  210. agentinputlab/experiments/parity_identity.py +605 -0
  211. agentinputlab/experiments/plan.py +277 -0
  212. agentinputlab/experiments/production_builders.py +3739 -0
  213. agentinputlab/experiments/readiness.py +432 -0
  214. agentinputlab/experiments/schema.py +514 -0
  215. agentinputlab/experiments/secrets.py +177 -0
  216. agentinputlab/experiments/stateful_analysis.py +1738 -0
  217. agentinputlab/experiments/transfer_analysis.py +1316 -0
  218. agentinputlab/experiments/unseen_analysis.py +1025 -0
  219. agentinputlab/integrations/__init__.py +1 -0
  220. agentinputlab/integrations/action_space/__init__.py +1 -0
  221. agentinputlab/integrations/external_workers/__init__.py +76 -0
  222. agentinputlab/integrations/external_workers/ace.py +431 -0
  223. agentinputlab/integrations/external_workers/base.py +181 -0
  224. agentinputlab/integrations/external_workers/decomp_live.py +249 -0
  225. agentinputlab/integrations/external_workers/decomp_planner.py +30 -0
  226. agentinputlab/integrations/external_workers/infocqr_student.py +280 -0
  227. agentinputlab/integrations/external_workers/ircot.py +576 -0
  228. agentinputlab/integrations/external_workers/llmcompiler_live.py +360 -0
  229. agentinputlab/integrations/external_workers/longllmlingua.py +162 -0
  230. agentinputlab/integrations/external_workers/method.py +314 -0
  231. agentinputlab/integrations/external_workers/process.py +678 -0
  232. agentinputlab/integrations/external_workers/supervisor.py +1071 -0
  233. agentinputlab/integrations/full_architecture/__init__.py +1 -0
  234. agentinputlab/integrations/full_architecture/hyde.py +212 -0
  235. agentinputlab/integrations/full_architecture/plan_and_budget.py +188 -0
  236. agentinputlab/integrations/live_history.py +222 -0
  237. agentinputlab/integrations/prompt_optimization/__init__.py +1 -0
  238. agentinputlab/integrations/training/__init__.py +1 -0
  239. agentinputlab/metrics/__init__.py +50 -0
  240. agentinputlab/metrics/catalog.py +313 -0
  241. agentinputlab/metrics/harm.py +95 -0
  242. agentinputlab/metrics/paired.py +40 -0
  243. agentinputlab/metrics/qa.py +76 -0
  244. agentinputlab/metrics/text.py +33 -0
  245. agentinputlab/metrics/third_party.py +161 -0
  246. agentinputlab/models/__init__.py +4 -0
  247. agentinputlab/observability/__init__.py +56 -0
  248. agentinputlab/observability/budget.py +316 -0
  249. agentinputlab/observability/instrumentation.py +754 -0
  250. agentinputlab/observability/models.py +403 -0
  251. agentinputlab/observability/monitor.py +380 -0
  252. agentinputlab/optimizers/__init__.py +1 -0
  253. agentinputlab/optimizers/clarify/__init__.py +12 -0
  254. agentinputlab/optimizers/context/__init__.py +22 -0
  255. agentinputlab/optimizers/decompose/__init__.py +64 -0
  256. agentinputlab/optimizers/evidence/__init__.py +22 -0
  257. agentinputlab/optimizers/rewrite/__init__.py +30 -0
  258. agentinputlab/parity/__init__.py +5 -0
  259. agentinputlab/parity/base.py +206 -0
  260. agentinputlab/production_campaign.py +763 -0
  261. agentinputlab/py.typed +1 -0
  262. agentinputlab/recipes/__init__.py +333 -0
  263. agentinputlab/recipes/ace.py +418 -0
  264. agentinputlab/recipes/ace_bfcl.py +273 -0
  265. agentinputlab/recipes/ace_common_support.py +169 -0
  266. agentinputlab/recipes/ace_native_plan.py +322 -0
  267. agentinputlab/recipes/ace_online.py +711 -0
  268. agentinputlab/recipes/ace_retrieval.py +330 -0
  269. agentinputlab/recipes/ace_statebench.py +382 -0
  270. agentinputlab/recipes/ace_tau.py +463 -0
  271. agentinputlab/recipes/ace_toolsandbox.py +432 -0
  272. agentinputlab/recipes/acon_guideline.py +22 -0
  273. agentinputlab/recipes/adaptive_rag.py +822 -0
  274. agentinputlab/recipes/llmlingua2_inference.py +146 -0
  275. agentinputlab/recipes/llmlingua2_training.py +796 -0
  276. agentinputlab/recipes/mce.py +1486 -0
  277. agentinputlab/recipes/mce_final.py +335 -0
  278. agentinputlab/recipes/mce_text_fit.py +499 -0
  279. agentinputlab/recipes/mce_text_only.py +298 -0
  280. agentinputlab/recipes/qrag.py +729 -0
  281. agentinputlab/recipes/qrag_inference.py +364 -0
  282. agentinputlab/recipes/r2ag_features.py +236 -0
  283. agentinputlab/recipes/r2ag_inference.py +503 -0
  284. agentinputlab/recipes/r2ag_training.py +948 -0
  285. agentinputlab/recipes/recomp_abstractive.py +592 -0
  286. agentinputlab/recipes/recomp_extractive.py +525 -0
  287. agentinputlab/recipes/reformer.py +17 -0
  288. agentinputlab/recipes/rqrag.py +1516 -0
  289. agentinputlab/recipes/rqrag_common_support.py +193 -0
  290. agentinputlab/recipes/rqrag_completion.py +147 -0
  291. agentinputlab/recipes/skillopt.py +462 -0
  292. agentinputlab/recipes/skillopt_common_support.py +154 -0
  293. agentinputlab/recipes/skillopt_environment.py +165 -0
  294. agentinputlab/recipes/skillopt_environment_cli.py +5 -0
  295. agentinputlab/recipes/skillopt_retrieval.py +188 -0
  296. agentinputlab/recipes/smartsearch.py +897 -0
  297. agentinputlab/recipes/smartsearch_curriculum.py +283 -0
  298. agentinputlab/recipes/smartsearch_grpo_contracts.py +58 -0
  299. agentinputlab/recipes/smartsearch_grpo_data.py +314 -0
  300. agentinputlab/recipes/smartsearch_grpo_meter.py +324 -0
  301. agentinputlab/recipes/smartsearch_grpo_prefix.py +144 -0
  302. agentinputlab/recipes/smartsearch_grpo_rollout.py +119 -0
  303. agentinputlab/recipes/smartsearch_grpo_tools.py +364 -0
  304. agentinputlab/recipes/smartsearch_grpo_training.py +305 -0
  305. agentinputlab/recipes/smartsearch_grpo_transport.py +248 -0
  306. agentinputlab/recipes/smartsearch_grpo_worker.py +364 -0
  307. agentinputlab/recipes/smartsearch_inference.py +524 -0
  308. agentinputlab/recipes/smartsearch_native_data.py +172 -0
  309. agentinputlab/recipes/smartsearch_policy.py +616 -0
  310. agentinputlab/recipes/smartsearch_policy_worker.py +9 -0
  311. agentinputlab/recipes/smartsearch_sampling.py +232 -0
  312. agentinputlab/recipes/smartsearch_teacher.py +203 -0
  313. agentinputlab/recipes/smartsearch_training.py +265 -0
  314. agentinputlab/registry/__init__.py +46 -0
  315. agentinputlab/registry/beta.py +727 -0
  316. agentinputlab/registry/data/algorithms.json +1959 -0
  317. agentinputlab/registry/data/auxiliary_method_profiles.json +34 -0
  318. agentinputlab/registry/loader.py +100 -0
  319. agentinputlab/registry/result_manifest.py +184 -0
  320. agentinputlab/registry/runtime.py +195 -0
  321. agentinputlab/registry/schema.py +449 -0
  322. agentinputlab/runner/__init__.py +30 -0
  323. agentinputlab/runner/balance.py +248 -0
  324. agentinputlab/runner/cache.py +135 -0
  325. agentinputlab/runner/composite.py +130 -0
  326. agentinputlab/runner/engine.py +285 -0
  327. agentinputlab/runner/runtime_guard.py +268 -0
  328. agentinputlab/runner/worker.py +80 -0
  329. agentinputlab/runtime_bootstrap.py +288 -0
  330. agentinputlab/selectors/__init__.py +196 -0
  331. agentinputlab/selectors/advanced.py +1514 -0
  332. agentinputlab/selectors/artifacts.py +1087 -0
  333. agentinputlab/selectors/base.py +112 -0
  334. agentinputlab/selectors/campaigns.py +1893 -0
  335. agentinputlab/selectors/catalog.py +86 -0
  336. agentinputlab/selectors/discovery.py +1860 -0
  337. agentinputlab/selectors/evaluation.py +1348 -0
  338. agentinputlab/selectors/features.py +383 -0
  339. agentinputlab/selectors/learned.py +502 -0
  340. agentinputlab/selectors/no_op.py +28 -0
  341. agentinputlab/selectors/outcome_table.py +475 -0
  342. agentinputlab/selectors/producer.py +853 -0
  343. agentinputlab/selectors/rules.py +109 -0
  344. agentinputlab/selectors/runtime.py +420 -0
  345. agentinputlab/studies/__init__.py +16 -0
  346. agentinputlab/studies/design.py +165 -0
  347. agentinputlab/studies/paired.py +322 -0
  348. agentinputlab/studio/__init__.py +6 -0
  349. agentinputlab/studio/campaigns.py +819 -0
  350. agentinputlab/studio/engine.py +1408 -0
  351. agentinputlab/studio/model_backends.py +425 -0
  352. agentinputlab/studio/prepared.py +410 -0
  353. agentinputlab/studio/server.py +613 -0
  354. agentinputlab/studio/static/app.js +397 -0
  355. agentinputlab/studio/static/campaigns.html +175 -0
  356. agentinputlab/studio/static/campaigns.js +426 -0
  357. agentinputlab/studio/static/index.html +192 -0
  358. agentinputlab/studio/static/result.html +103 -0
  359. agentinputlab/studio/static/result.js +273 -0
  360. agentinputlab/studio/static/styles.css +467 -0
  361. agentinputlab-0.1.0a1.data/data/share/agentinputlab/CITATION.cff +22 -0
  362. agentinputlab-0.1.0a1.dist-info/METADATA +392 -0
  363. agentinputlab-0.1.0a1.dist-info/RECORD +469 -0
  364. agentinputlab-0.1.0a1.dist-info/WHEEL +5 -0
  365. agentinputlab-0.1.0a1.dist-info/entry_points.txt +6 -0
  366. agentinputlab-0.1.0a1.dist-info/licenses/LICENSE +21 -0
  367. agentinputlab-0.1.0a1.dist-info/licenses/NOTICE +31 -0
  368. agentinputlab-0.1.0a1.dist-info/licenses/THIRD_PARTY_NOTICES.md +94 -0
  369. agentinputlab-0.1.0a1.dist-info/top_level.txt +2 -0
  370. workers/README.md +94 -0
  371. workers/ace/README.md +261 -0
  372. workers/ace/__init__.py +1 -0
  373. workers/ace/compatibility_manifest.json +39 -0
  374. workers/ace/driver.py +1682 -0
  375. workers/acon/README.md +215 -0
  376. workers/acon/__init__.py +5 -0
  377. workers/acon/compatibility_manifest.json +93 -0
  378. workers/acon/driver.py +1859 -0
  379. workers/acon/fitted.py +395 -0
  380. workers/acon/guideline.py +470 -0
  381. workers/acon/guideline_source_manifest.json +20 -0
  382. workers/adaptive_rag/README.md +96 -0
  383. workers/adaptive_rag/__init__.py +15 -0
  384. workers/adaptive_rag/compatibility_manifest.json +61 -0
  385. workers/adaptive_rag/driver.py +1470 -0
  386. workers/adaptive_rag/fixture_runtime.py +163 -0
  387. workers/adaptive_rag/runtime.py +252 -0
  388. workers/ask_or_assume/README.md +124 -0
  389. workers/ask_or_assume/__init__.py +1 -0
  390. workers/ask_or_assume/compatibility_manifest.json +36 -0
  391. workers/ask_or_assume/driver.py +1227 -0
  392. workers/decomp/README.md +133 -0
  393. workers/decomp/UPSTREAM_LICENSE.Apache-2.0.txt +201 -0
  394. workers/decomp/__init__.py +5 -0
  395. workers/decomp/compatibility_manifest.json +205 -0
  396. workers/decomp/driver.py +1015 -0
  397. workers/decomp/live.py +137 -0
  398. workers/decomp/runtime.py +231 -0
  399. workers/infocqr/README.md +106 -0
  400. workers/infocqr/__init__.py +1 -0
  401. workers/infocqr/compatibility_manifest.json +84 -0
  402. workers/infocqr/driver.py +1766 -0
  403. workers/ircot/README.md +167 -0
  404. workers/ircot/__init__.py +7 -0
  405. workers/ircot/compatibility_manifest.json +121 -0
  406. workers/ircot/controlled.py +457 -0
  407. workers/ircot/driver.py +1373 -0
  408. workers/ircot/fixture_bindings.json +39 -0
  409. workers/ircot/fixture_config.json +48 -0
  410. workers/ircot/fixture_prompt.txt +2 -0
  411. workers/ircot/native_qa.py +398 -0
  412. workers/ircot/qa_compatibility_manifest.json +57 -0
  413. workers/ircot/runtime.py +97 -0
  414. workers/llmcompiler/README.md +160 -0
  415. workers/llmcompiler/__init__.py +1 -0
  416. workers/llmcompiler/compatibility_manifest.json +45 -0
  417. workers/llmcompiler/driver.py +1514 -0
  418. workers/llmcompiler/export_prompts.py +74 -0
  419. workers/llmcompiler/live.py +174 -0
  420. workers/llmcompiler/patch_manifest.json +147 -0
  421. workers/mce/README.md +198 -0
  422. workers/mce/__init__.py +29 -0
  423. workers/mce/compatibility_manifest.json +115 -0
  424. workers/mce/driver.py +2585 -0
  425. workers/mce/final_eval.py +173 -0
  426. workers/official_arenas/README.md +25 -0
  427. workers/official_arenas/__init__.py +1 -0
  428. workers/official_arenas/capture_records.py +110 -0
  429. workers/official_arenas/driver.py +619 -0
  430. workers/official_arenas/protocol.py +334 -0
  431. workers/official_arenas/providers/__init__.py +1 -0
  432. workers/official_arenas/providers/_live_history.py +228 -0
  433. workers/official_arenas/providers/_message_compat.py +77 -0
  434. workers/official_arenas/providers/appworld_ul.py +196 -0
  435. workers/official_arenas/providers/fixture.py +112 -0
  436. workers/official_arenas/providers/statebench.py +960 -0
  437. workers/official_arenas/providers/tau3.py +1672 -0
  438. workers/official_arenas/providers/tau_retail.py +586 -0
  439. workers/official_arenas/providers/toolsandbox.py +1321 -0
  440. workers/official_arenas/providers/tooltalk.py +699 -0
  441. workers/official_arenas/retail_capture.py +188 -0
  442. workers/official_arenas/retail_driver.py +275 -0
  443. workers/qrag/README.md +172 -0
  444. workers/qrag/__init__.py +1 -0
  445. workers/qrag/compatibility_manifest.json +64 -0
  446. workers/qrag/driver.py +673 -0
  447. workers/r2ag/README.md +154 -0
  448. workers/r2ag/__init__.py +5 -0
  449. workers/r2ag/compatibility_manifest.json +55 -0
  450. workers/r2ag/driver.py +1287 -0
  451. workers/r2ag/feature_schema.json +8 -0
  452. workers/r2ag/runtime.py +589 -0
  453. workers/recomp/README.md +120 -0
  454. workers/recomp/__init__.py +1 -0
  455. workers/recomp/compatibility_manifest.json +43 -0
  456. workers/recomp/driver.py +1653 -0
  457. workers/reformer/README.md +125 -0
  458. workers/reformer/__init__.py +1 -0
  459. workers/reformer/compatibility_manifest.json +68 -0
  460. workers/reformer/driver.py +1459 -0
  461. workers/reformer/induction.py +457 -0
  462. workers/rrr/README.md +126 -0
  463. workers/rrr/__init__.py +1 -0
  464. workers/rrr/compatibility_manifest.json +57 -0
  465. workers/rrr/driver.py +1133 -0
  466. workers/smartsearch/README.md +331 -0
  467. workers/smartsearch/__init__.py +1 -0
  468. workers/smartsearch/compatibility_manifest.json +41 -0
  469. workers/smartsearch/driver.py +1643 -0
@@ -0,0 +1,2313 @@
1
+ """Deterministic, gold-separated preprocessing for audited smoke assets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import hashlib
7
+ import io
8
+ import json
9
+ import os
10
+ import re
11
+ import shutil
12
+ import stat
13
+ import tarfile
14
+ import tempfile
15
+ import zipfile
16
+ from collections import defaultdict
17
+ from collections.abc import Iterable, Mapping
18
+ from dataclasses import dataclass
19
+ from pathlib import Path, PurePosixPath
20
+ from types import MappingProxyType
21
+ from typing import Any
22
+
23
+ from agentinputlab.artifacts import sha256_file
24
+ from agentinputlab.contracts.errors import ArtifactError
25
+ from agentinputlab.contracts.serialization import canonical_json
26
+ from agentinputlab.data.storage import (
27
+ DEFAULT_PREPARED_LIMIT,
28
+ positive_byte_limit,
29
+ require_storage_capacity,
30
+ )
31
+
32
+ from .assets import BenchmarkSmokeAsset
33
+
34
+ _PROCESSOR_REVISIONS = {
35
+ "beir_zip_v1": "1",
36
+ "bfcl_v3_simple_tar_v1": "1",
37
+ "bright_corpus_parquet_v1": "1",
38
+ "bright_examples_parquet_v1": "1",
39
+ "clariq_tsv_v1": "1",
40
+ "hotpotqa_parquet_v1": "1",
41
+ "musique_zip_v1": "1",
42
+ "qasper_tgz_v1": "1",
43
+ "tau3_banking_tar_v1": "1",
44
+ "two_wiki_zip_v1": "1",
45
+ }
46
+ _PROCESSOR_FILES = {
47
+ "beir_zip_v1": {"inputs.jsonl", "gold.jsonl", "corpus.jsonl"},
48
+ "bfcl_v3_simple_tar_v1": {"inputs.jsonl", "gold.jsonl", "audit.jsonl"},
49
+ "bright_corpus_parquet_v1": {"corpus.jsonl"},
50
+ "bright_examples_parquet_v1": {"inputs.jsonl", "gold.jsonl"},
51
+ "clariq_tsv_v1": {"inputs.jsonl", "gold.jsonl"},
52
+ "hotpotqa_parquet_v1": {"inputs.jsonl", "gold.jsonl"},
53
+ "musique_zip_v1": {"inputs.jsonl", "gold.jsonl", "audit.jsonl"},
54
+ "qasper_tgz_v1": {"inputs.jsonl", "gold.jsonl"},
55
+ "tau3_banking_tar_v1": {
56
+ "inputs.jsonl",
57
+ "gold.jsonl",
58
+ "corpus.jsonl",
59
+ "audit.jsonl",
60
+ },
61
+ "two_wiki_zip_v1": {"inputs.jsonl", "gold.jsonl", "audit.jsonl"},
62
+ }
63
+ _BEIR_MEMBERS = {
64
+ "arguana/corpus.jsonl",
65
+ "arguana/queries.jsonl",
66
+ "arguana/qrels/test.tsv",
67
+ }
68
+ _CLARIQ_FIELDS = (
69
+ "topic_id",
70
+ "initial_request",
71
+ "topic_desc",
72
+ "clarification_need",
73
+ "facet_id",
74
+ "facet_desc",
75
+ "question_id",
76
+ "question",
77
+ "answer",
78
+ )
79
+ _MAX_ARCHIVE_MEMBERS = 16
80
+ _MAX_UNCOMPRESSED_BYTES = 64 * 1024 * 1024
81
+ _MAX_MEMBER_BYTES = 32 * 1024 * 1024
82
+ _MAX_COMPRESSION_RATIO = 200.0
83
+ _MUSIQUE_MEMBERS = {
84
+ "data/.DS_Store",
85
+ "data/dev_test_singlehop_questions_v1.0.json",
86
+ "data/musique_ans_v1.0_dev.jsonl",
87
+ "data/musique_ans_v1.0_test.jsonl",
88
+ "data/musique_ans_v1.0_train.jsonl",
89
+ "data/musique_full_v1.0_dev.jsonl",
90
+ "data/musique_full_v1.0_test.jsonl",
91
+ "data/musique_full_v1.0_train.jsonl",
92
+ }
93
+ _QASPER_MEMBERS = {
94
+ "README.md",
95
+ "qasper-dev-v0.3.json",
96
+ "qasper-train-v0.3.json",
97
+ }
98
+ _TWO_WIKI_MEMBERS = {
99
+ "dev.json",
100
+ "id_aliases.json",
101
+ "test.json",
102
+ "train.json",
103
+ }
104
+ _TAU3_BANKING_CELL = "tau3.banking_knowledge"
105
+ _TAU3_BANKING_DATA_ROOT = "data/tau2/domains/banking_knowledge"
106
+ _TAU3_BANKING_SOURCE_ROOT = "src/tau2/domains/banking_knowledge"
107
+ _TAU3_RUNTIME_SOURCE_PREFIXES = (
108
+ "src/tau2/agent/base/",
109
+ "src/tau2/data_model/",
110
+ "src/tau2/domains/banking_knowledge/",
111
+ "src/tau2/environment/",
112
+ "src/tau2/evaluator/",
113
+ "src/tau2/knowledge/",
114
+ "src/tau2/orchestrator/",
115
+ "src/tau2/runner/",
116
+ "src/tau2/user/",
117
+ )
118
+ _TAU3_TASK_FIELDS = {
119
+ "annotations",
120
+ "description",
121
+ "evaluation_criteria",
122
+ "id",
123
+ "initial_state",
124
+ "required_documents",
125
+ "user_scenario",
126
+ "user_tools",
127
+ }
128
+ _TAU3_DOCUMENT_FIELDS = {"content", "id", "title"}
129
+ _TAU3_MAX_ARCHIVE_MEMBERS = 5_000
130
+ _TAU3_MAX_SELECTED_FILES = 2_048
131
+ _TAU3_MAX_SELECTED_MEMBER_BYTES = 8 * 1024 * 1024
132
+ _TAU3_MAX_SELECTED_BYTES = 64 * 1024 * 1024
133
+ _TAU3_VERSION_PATTERN = re.compile(
134
+ r'^version\s*=\s*["\'](?P<version>[^"\']+)["\']\s*$', re.MULTILINE
135
+ )
136
+ _BFCL_V3_CELL = "bfcl.v3.simple_python"
137
+ _BFCL_V3_ARCHIVE_ROOT = "gorilla-1.3"
138
+ _BFCL_V3_SOURCE_ROOT = "berkeley-function-call-leaderboard"
139
+ _BFCL_V3_PROMPT_PATH = f"{_BFCL_V3_SOURCE_ROOT}/bfcl_eval/data/BFCL_v3_simple.json"
140
+ _BFCL_V3_GOLD_PATH = f"{_BFCL_V3_SOURCE_ROOT}/bfcl_eval/data/possible_answer/BFCL_v3_simple.json"
141
+ _BFCL_V3_REQUIRED_FILES = {
142
+ "LICENSE",
143
+ f"{_BFCL_V3_SOURCE_ROOT}/pyproject.toml",
144
+ _BFCL_V3_PROMPT_PATH,
145
+ _BFCL_V3_GOLD_PATH,
146
+ f"{_BFCL_V3_SOURCE_ROOT}/bfcl_eval/constants/category_mapping.py",
147
+ f"{_BFCL_V3_SOURCE_ROOT}/bfcl_eval/eval_checker/ast_eval/ast_checker.py",
148
+ f"{_BFCL_V3_SOURCE_ROOT}/bfcl_eval/eval_checker/eval_runner.py",
149
+ f"{_BFCL_V3_SOURCE_ROOT}/bfcl_eval/model_handler/base_handler.py",
150
+ }
151
+ _BFCL_V3_MAX_ARCHIVE_MEMBERS = 1_000
152
+ _BFCL_V3_MAX_SELECTED_MEMBER_BYTES = 2 * 1024 * 1024
153
+ _BFCL_V3_MAX_SELECTED_BYTES = 8 * 1024 * 1024
154
+
155
+
156
+ @dataclass(frozen=True, slots=True)
157
+ class PreparedBenchmark:
158
+ asset_id: str
159
+ processor: str
160
+ processor_revision: str
161
+ source_sha256: str
162
+ root: Path
163
+ counts: Mapping[str, int]
164
+ file_sha256: Mapping[str, str]
165
+ cell_id: str = ""
166
+ source_revision: str = ""
167
+ split: str = ""
168
+
169
+ def __post_init__(self) -> None:
170
+ object.__setattr__(self, "counts", MappingProxyType(dict(self.counts)))
171
+ object.__setattr__(self, "file_sha256", MappingProxyType(dict(self.file_sha256)))
172
+
173
+ @property
174
+ def inputs_path(self) -> Path:
175
+ return self.root / "inputs.jsonl"
176
+
177
+ @property
178
+ def gold_path(self) -> Path:
179
+ return self.root / "gold.jsonl"
180
+
181
+ @property
182
+ def corpus_path(self) -> Path | None:
183
+ path = self.root / "corpus.jsonl"
184
+ return path if path.is_file() else None
185
+
186
+ @property
187
+ def audit_path(self) -> Path | None:
188
+ path = self.root / "audit.jsonl"
189
+ return path if path.is_file() else None
190
+
191
+
192
+ def prepare_smoke_asset(
193
+ asset: BenchmarkSmokeAsset,
194
+ source: str | Path,
195
+ output_root: str | Path,
196
+ *,
197
+ max_prepared_bytes: int = DEFAULT_PREPARED_LIMIT,
198
+ ) -> PreparedBenchmark:
199
+ """Verify and normalize one complete source artifact into an immutable cache.
200
+
201
+ The normalized cache always stores optimizer-visible inputs separately from
202
+ evaluator-only gold. Existing caches are verified before reuse and never
203
+ silently overwritten.
204
+ """
205
+
206
+ positive_byte_limit(max_prepared_bytes, name="max_prepared_bytes")
207
+ source_path = Path(source).expanduser().resolve()
208
+ if not source_path.is_file():
209
+ raise ArtifactError(f"benchmark source is missing: {source_path}")
210
+ if source_path.stat().st_size != asset.size_bytes:
211
+ raise ArtifactError(f"benchmark source size mismatch: {asset.asset_id}")
212
+ if sha256_file(source_path) != asset.sha256:
213
+ raise ArtifactError(f"benchmark source checksum mismatch: {asset.asset_id}")
214
+
215
+ revision = _PROCESSOR_REVISIONS.get(asset.processor)
216
+ if revision is None:
217
+ raise ArtifactError(f"unsupported benchmark processor: {asset.processor}")
218
+ root = Path(output_root).expanduser().resolve()
219
+ root.mkdir(parents=True, exist_ok=True)
220
+ destination = _prepared_path(asset, root)
221
+ if destination.exists():
222
+ return _load_prepared(destination, expected_asset=asset)
223
+ require_storage_capacity([(root, max_prepared_bytes)])
224
+
225
+ if asset.processor == "beir_zip_v1":
226
+ normalized = _normalize_beir_zip(source_path, asset)
227
+ elif asset.processor == "bfcl_v3_simple_tar_v1":
228
+ normalized = _normalize_bfcl_v3_simple_tar(source_path, asset)
229
+ elif asset.processor == "bright_examples_parquet_v1":
230
+ normalized = _normalize_bright_examples(source_path, asset)
231
+ elif asset.processor == "bright_corpus_parquet_v1":
232
+ normalized = _normalize_bright_corpus(source_path, asset)
233
+ elif asset.processor == "clariq_tsv_v1":
234
+ normalized = _normalize_clariq_tsv(source_path, asset)
235
+ elif asset.processor == "musique_zip_v1":
236
+ normalized = _normalize_musique_zip(source_path, asset)
237
+ elif asset.processor == "two_wiki_zip_v1":
238
+ normalized = _normalize_two_wiki_zip(source_path, asset)
239
+ elif asset.processor == "hotpotqa_parquet_v1":
240
+ normalized = _normalize_hotpotqa_parquet(source_path, asset)
241
+ elif asset.processor == "qasper_tgz_v1":
242
+ normalized = _normalize_qasper_tgz(source_path, asset)
243
+ elif asset.processor == "tau3_banking_tar_v1":
244
+ normalized = _normalize_tau3_banking_tar(source_path, asset)
245
+ else: # pragma: no cover - guarded above and by the asset registry
246
+ raise ArtifactError(f"unsupported benchmark processor: {asset.processor}")
247
+
248
+ parent = destination.parent
249
+ parent.mkdir(parents=True, exist_ok=True)
250
+ temporary = Path(tempfile.mkdtemp(prefix=".prepare-", dir=parent))
251
+ try:
252
+ file_sha256: dict[str, str] = {}
253
+ written_bytes = 0
254
+
255
+ def write_bounded(path: Path, content: bytes) -> None:
256
+ nonlocal written_bytes
257
+ if written_bytes + len(content) > max_prepared_bytes:
258
+ raise ArtifactError("normalized benchmark exceeds max_prepared_bytes")
259
+ require_storage_capacity([(temporary, len(content))])
260
+ _write_bytes(path, content)
261
+ written_bytes += len(content)
262
+
263
+ for name in ("inputs.jsonl", "gold.jsonl", "corpus.jsonl", "audit.jsonl"):
264
+ records = normalized.get(name)
265
+ if records is None:
266
+ continue
267
+ content = _jsonl(records)
268
+ path = temporary / name
269
+ write_bounded(path, content)
270
+ file_sha256[name] = hashlib.sha256(content).hexdigest()
271
+ if asset.prepared_sha256 and dict(asset.prepared_sha256) != file_sha256:
272
+ raise ArtifactError(
273
+ f"prepared content hash mismatch for {asset.asset_id}: "
274
+ "the processor output differs from the audited lock"
275
+ )
276
+ counts = dict(normalized["counts"])
277
+ _require_expected_counts(asset, counts)
278
+ metadata = {
279
+ "schema_version": 1,
280
+ "asset_id": asset.asset_id,
281
+ "cell_id": asset.cell_id,
282
+ "source_revision": asset.source_revision,
283
+ "source_sha256": asset.sha256,
284
+ "source_size_bytes": asset.size_bytes,
285
+ "processor": asset.processor,
286
+ "processor_revision": revision,
287
+ "gold_separated": True,
288
+ "counts": counts,
289
+ "files": file_sha256,
290
+ }
291
+ if asset.processor not in {"beir_zip_v1", "clariq_tsv_v1"}:
292
+ metadata["split"] = _processor_split(asset.processor)
293
+ write_bounded(
294
+ temporary / "metadata.json",
295
+ (canonical_json(metadata) + "\n").encode("utf-8"),
296
+ )
297
+ os.replace(temporary, destination)
298
+ finally:
299
+ if temporary.exists():
300
+ shutil.rmtree(temporary)
301
+ return _load_prepared(destination, expected_asset=asset)
302
+
303
+
304
+ def load_prepared_benchmark(
305
+ asset: BenchmarkSmokeAsset,
306
+ prepared_root: str | Path,
307
+ ) -> PreparedBenchmark:
308
+ """Load and reverify a deterministic prepared cache without source bytes."""
309
+
310
+ root = Path(prepared_root).expanduser().resolve()
311
+ destination = _prepared_path(asset, root)
312
+ if not destination.is_dir():
313
+ raise ArtifactError(f"prepared benchmark cache is missing: {asset.asset_id}")
314
+ return _load_prepared(destination, expected_asset=asset)
315
+
316
+
317
+ def _prepared_path(asset: BenchmarkSmokeAsset, root: Path) -> Path:
318
+ revision = _PROCESSOR_REVISIONS.get(asset.processor)
319
+ if revision is None:
320
+ raise ArtifactError(f"unsupported benchmark processor: {asset.processor}")
321
+ cache_key = hashlib.sha256(f"{asset.sha256}:{asset.processor}:{revision}".encode()).hexdigest()[
322
+ :20
323
+ ]
324
+ return root / asset.asset_id / cache_key
325
+
326
+
327
+ def _normalize_beir_zip(
328
+ source: Path,
329
+ asset: BenchmarkSmokeAsset,
330
+ ) -> dict[str, Any]:
331
+ try:
332
+ archive = zipfile.ZipFile(source)
333
+ except zipfile.BadZipFile as exc:
334
+ raise ArtifactError("BEIR source is not a valid ZIP archive") from exc
335
+ with archive:
336
+ _validate_zip(archive)
337
+ corpus = _read_jsonl_member(archive, "arguana/corpus.jsonl")
338
+ queries = _read_jsonl_member(archive, "arguana/queries.jsonl")
339
+ qrel_text = _read_text_member(archive, "arguana/qrels/test.tsv")
340
+
341
+ corpus_by_id = _records_by_id(corpus, label="BEIR corpus")
342
+ query_by_id = _records_by_id(queries, label="BEIR queries")
343
+ qrels: dict[str, dict[str, int]] = defaultdict(dict)
344
+ reader = csv.DictReader(io.StringIO(qrel_text), delimiter="\t")
345
+ if tuple(reader.fieldnames or ()) != ("query-id", "corpus-id", "score"):
346
+ raise ArtifactError("BEIR qrels schema does not match query-id/corpus-id/score")
347
+ qrel_count = 0
348
+ for row in reader:
349
+ query_id = _required_text(row, "query-id", label="BEIR qrels")
350
+ corpus_id = _required_text(row, "corpus-id", label="BEIR qrels")
351
+ try:
352
+ score = int(_required_text(row, "score", label="BEIR qrels"))
353
+ except ValueError as exc:
354
+ raise ArtifactError("BEIR qrels score must be an integer") from exc
355
+ if score < 0:
356
+ raise ArtifactError("BEIR qrels score must be non-negative")
357
+ if corpus_id in qrels[query_id]:
358
+ raise ArtifactError("BEIR qrels contain a duplicate query/document pair")
359
+ qrels[query_id][corpus_id] = score
360
+ qrel_count += 1
361
+ if set(qrels) != set(query_by_id):
362
+ raise ArtifactError("BEIR qrels and query IDs do not match")
363
+ orphan_queries = {
364
+ query_id for query_id, judgments in qrels.items() if set(judgments) - set(corpus_by_id)
365
+ }
366
+
367
+ inputs = []
368
+ gold = []
369
+ for query_id in sorted(query_by_id):
370
+ query = query_by_id[query_id]
371
+ text = _required_text(query, "text", label="BEIR query")
372
+ inputs.append(
373
+ {
374
+ "id": query_id,
375
+ "query": text,
376
+ "instruction": "Retrieve the most relevant counterargument.",
377
+ "visible_metadata": {
378
+ "benchmark_cell": asset.cell_id,
379
+ "source_query_id": query_id,
380
+ },
381
+ }
382
+ )
383
+ gold.append(
384
+ {
385
+ "id": query_id,
386
+ "relevance": {
387
+ doc_id: qrels[query_id][doc_id] for doc_id in sorted(qrels[query_id])
388
+ },
389
+ "orphan_relevant_document_ids": sorted(set(qrels[query_id]) - set(corpus_by_id)),
390
+ }
391
+ )
392
+ normalized_corpus = []
393
+ for document_id in sorted(corpus_by_id):
394
+ document = corpus_by_id[document_id]
395
+ normalized_corpus.append(
396
+ {
397
+ "id": document_id,
398
+ "title": _optional_text(document, "title", label="BEIR corpus"),
399
+ "text": _required_text(document, "text", label="BEIR corpus"),
400
+ }
401
+ )
402
+ return {
403
+ "inputs.jsonl": inputs,
404
+ "gold.jsonl": gold,
405
+ "corpus.jsonl": normalized_corpus,
406
+ "counts": {
407
+ "inputs": len(inputs),
408
+ "gold": len(gold),
409
+ "corpus": len(normalized_corpus),
410
+ "qrels": qrel_count,
411
+ "orphan_qrels": sum(
412
+ doc_id not in corpus_by_id for judgments in qrels.values() for doc_id in judgments
413
+ ),
414
+ "orphan_queries": len(orphan_queries),
415
+ },
416
+ }
417
+
418
+
419
+ def _normalize_bright_examples(
420
+ source: Path,
421
+ asset: BenchmarkSmokeAsset,
422
+ ) -> dict[str, Any]:
423
+ rows = _read_parquet_rows(
424
+ source,
425
+ expected_columns={
426
+ "excluded_ids",
427
+ "gold_answer",
428
+ "gold_ids",
429
+ "gold_ids_long",
430
+ "id",
431
+ "query",
432
+ "reasoning",
433
+ },
434
+ label="BRIGHT examples",
435
+ )
436
+ instructions = {
437
+ "bright.earth_science": (
438
+ "Retrieve short documents that provide the evidence needed to answer this "
439
+ "reasoning-intensive Earth Science query."
440
+ ),
441
+ "bright.economics": (
442
+ "Retrieve short documents that provide the evidence needed to answer this "
443
+ "reasoning-intensive Economics query."
444
+ ),
445
+ "bright.robotics": (
446
+ "Retrieve short documents that provide the evidence needed to answer this "
447
+ "reasoning-intensive Robotics query."
448
+ ),
449
+ "bright.stackoverflow": (
450
+ "Retrieve short documents that provide the evidence needed to answer this "
451
+ "reasoning-intensive Stack Overflow query."
452
+ ),
453
+ }
454
+ try:
455
+ instruction = instructions[asset.cell_id]
456
+ except KeyError as exc:
457
+ raise ArtifactError(f"unsupported BRIGHT examples cell: {asset.cell_id}") from exc
458
+ normalized: list[tuple[str, dict[str, Any], dict[str, Any]]] = []
459
+ qrel_count = 0
460
+ excluded_count = 0
461
+ for row in rows:
462
+ example_id = _required_text(row, "id", label="BRIGHT example")
463
+ query = _required_text(row, "query", label="BRIGHT example")
464
+ gold_ids = _string_list(row.get("gold_ids"), label="BRIGHT gold_ids", non_empty=True)
465
+ excluded_ids = _string_list(
466
+ row.get("excluded_ids"), label="BRIGHT excluded_ids", non_empty=False
467
+ )
468
+ relevance = {document_id: 1 for document_id in sorted(set(gold_ids))}
469
+ if len(relevance) != len(gold_ids):
470
+ raise ArtifactError("BRIGHT gold_ids contain duplicates")
471
+ qrel_count += len(relevance)
472
+ excluded = sorted({item for item in excluded_ids if item != "N/A"})
473
+ excluded_count += len(excluded)
474
+ visible = {
475
+ "id": example_id,
476
+ "query": query,
477
+ "instruction": instruction,
478
+ "visible_metadata": {
479
+ "benchmark_cell": asset.cell_id,
480
+ "source_split": "test",
481
+ "source_query_id": example_id,
482
+ },
483
+ }
484
+ evaluator = {
485
+ "id": example_id,
486
+ "relevance": relevance,
487
+ "evaluator_metadata": {
488
+ "excluded_document_ids": excluded,
489
+ "gold_answer": _optional_text(row, "gold_answer", label="BRIGHT example"),
490
+ "gold_ids_long": _string_list(
491
+ row.get("gold_ids_long"),
492
+ label="BRIGHT gold_ids_long",
493
+ non_empty=False,
494
+ ),
495
+ "reasoning": _optional_text(row, "reasoning", label="BRIGHT example"),
496
+ },
497
+ }
498
+ normalized.append((example_id, visible, evaluator))
499
+ normalized.sort(key=lambda item: item[0])
500
+ _require_unique_ids((item[0] for item in normalized), label="BRIGHT examples")
501
+ return {
502
+ "inputs.jsonl": [item[1] for item in normalized],
503
+ "gold.jsonl": [item[2] for item in normalized],
504
+ "counts": {
505
+ "excluded_documents": excluded_count,
506
+ "gold": len(normalized),
507
+ "inputs": len(normalized),
508
+ "qrels": qrel_count,
509
+ },
510
+ }
511
+
512
+
513
+ def _normalize_bright_corpus(
514
+ source: Path,
515
+ asset: BenchmarkSmokeAsset,
516
+ ) -> dict[str, Any]:
517
+ if asset.cell_id not in {
518
+ "bright.earth_science",
519
+ "bright.economics",
520
+ "bright.robotics",
521
+ "bright.stackoverflow",
522
+ }:
523
+ raise ArtifactError(f"unsupported BRIGHT corpus cell: {asset.cell_id}")
524
+ rows = _read_parquet_rows(
525
+ source,
526
+ expected_columns={"content", "id"},
527
+ label="BRIGHT corpus",
528
+ )
529
+ corpus: list[dict[str, str]] = []
530
+ ids: list[str] = []
531
+ for row in rows:
532
+ document_id = _required_text(row, "id", label="BRIGHT corpus")
533
+ ids.append(document_id)
534
+ corpus.append(
535
+ {
536
+ "id": document_id,
537
+ "title": "",
538
+ "text": _required_text(row, "content", label="BRIGHT corpus"),
539
+ }
540
+ )
541
+ _require_unique_ids(ids, label="BRIGHT corpus")
542
+ corpus.sort(key=lambda item: item["id"])
543
+ return {
544
+ "corpus.jsonl": corpus,
545
+ "counts": {"corpus": len(corpus)},
546
+ }
547
+
548
+
549
+ def _normalize_musique_zip(
550
+ source: Path,
551
+ asset: BenchmarkSmokeAsset,
552
+ ) -> dict[str, Any]:
553
+ if asset.cell_id != "musique.answerable":
554
+ raise ArtifactError(f"unsupported MuSiQue cell: {asset.cell_id}")
555
+ try:
556
+ archive = zipfile.ZipFile(source)
557
+ except zipfile.BadZipFile as exc:
558
+ raise ArtifactError("MuSiQue source is not a valid ZIP archive") from exc
559
+ with archive:
560
+ _validate_named_zip(
561
+ archive,
562
+ allowed_files=_MUSIQUE_MEMBERS,
563
+ required_files={
564
+ "data/dev_test_singlehop_questions_v1.0.json",
565
+ "data/musique_ans_v1.0_dev.jsonl",
566
+ },
567
+ max_members=16,
568
+ max_member_bytes=512 * 1024 * 1024,
569
+ max_uncompressed_bytes=1024 * 1024 * 1024,
570
+ label="MuSiQue",
571
+ )
572
+ rows = _read_jsonl_member(archive, "data/musique_ans_v1.0_dev.jsonl")
573
+ leakage_payload = _read_json_member(
574
+ archive,
575
+ "data/dev_test_singlehop_questions_v1.0.json",
576
+ label="MuSiQue leakage audit",
577
+ )
578
+ audit_rows = _normalize_musique_leakage_audit(leakage_payload)
579
+ inputs: list[dict[str, Any]] = []
580
+ gold: list[dict[str, Any]] = []
581
+ context_count = 0
582
+ support_count = 0
583
+ for row in rows:
584
+ example_id = _required_text(row, "id", label="MuSiQue example")
585
+ if row.get("answerable") is not True:
586
+ raise ArtifactError("MuSiQue answerable dev contains a non-answerable row")
587
+ paragraphs = row.get("paragraphs")
588
+ if not isinstance(paragraphs, list) or not paragraphs:
589
+ raise ArtifactError("MuSiQue example requires a non-empty paragraphs list")
590
+ blocks: list[dict[str, Any]] = []
591
+ support_ids: list[str] = []
592
+ paragraph_indices: list[int] = []
593
+ for paragraph in paragraphs:
594
+ if not isinstance(paragraph, Mapping):
595
+ raise ArtifactError("MuSiQue paragraph must be an object")
596
+ index = paragraph.get("idx")
597
+ if isinstance(index, bool) or not isinstance(index, int) or index < 0:
598
+ raise ArtifactError("MuSiQue paragraph idx must be a non-negative integer")
599
+ paragraph_indices.append(index)
600
+ block_id = f"paragraph:{index}"
601
+ title = _required_text(paragraph, "title", label="MuSiQue paragraph")
602
+ blocks.append(
603
+ {
604
+ "block_id": block_id,
605
+ "text": _required_text(
606
+ paragraph,
607
+ "paragraph_text",
608
+ label="MuSiQue paragraph",
609
+ ),
610
+ "source": title,
611
+ "metadata": {"paragraph_index": index, "title": title},
612
+ }
613
+ )
614
+ is_supporting = paragraph.get("is_supporting")
615
+ if not isinstance(is_supporting, bool):
616
+ raise ArtifactError("MuSiQue is_supporting must be boolean")
617
+ if is_supporting:
618
+ support_ids.append(block_id)
619
+ _require_unique_ids(
620
+ (str(index) for index in paragraph_indices),
621
+ label="MuSiQue paragraph indices",
622
+ )
623
+ if not support_ids:
624
+ raise ArtifactError("MuSiQue answerable row requires supporting paragraphs")
625
+ answer = _required_text(row, "answer", label="MuSiQue example")
626
+ aliases = _string_list(
627
+ row.get("answer_aliases"), label="MuSiQue answer_aliases", non_empty=False
628
+ )
629
+ answers = _ordered_unique((answer, *aliases))
630
+ decomposition = row.get("question_decomposition")
631
+ if not isinstance(decomposition, list):
632
+ raise ArtifactError("MuSiQue question_decomposition must be a list")
633
+ inputs.append(
634
+ {
635
+ "id": example_id,
636
+ "query": _required_text(row, "question", label="MuSiQue example"),
637
+ "instruction": (
638
+ "Answer the multi-hop question using the provided paragraphs and identify "
639
+ "the supporting paragraphs."
640
+ ),
641
+ "visible_context": blocks,
642
+ "visible_metadata": {
643
+ "benchmark_cell": asset.cell_id,
644
+ "source_split": "dev",
645
+ },
646
+ }
647
+ )
648
+ gold.append(
649
+ {
650
+ "id": example_id,
651
+ "answers": answers,
652
+ "support_ids": sorted(support_ids),
653
+ "evaluator_metadata": {"question_decomposition": decomposition},
654
+ }
655
+ )
656
+ context_count += len(blocks)
657
+ support_count += len(support_ids)
658
+ inputs.sort(key=lambda item: item["id"])
659
+ gold.sort(key=lambda item: item["id"])
660
+ _require_unique_ids((item["id"] for item in inputs), label="MuSiQue examples")
661
+ return {
662
+ "audit.jsonl": audit_rows,
663
+ "inputs.jsonl": inputs,
664
+ "gold.jsonl": gold,
665
+ "counts": {
666
+ "context_blocks": context_count,
667
+ "gold": len(gold),
668
+ "inputs": len(inputs),
669
+ "leakage_audit_records": len(audit_rows),
670
+ "leakage_audit_sources": len(leakage_payload),
671
+ "support_labels": support_count,
672
+ },
673
+ }
674
+
675
+
676
+ def _normalize_two_wiki_zip(
677
+ source: Path,
678
+ asset: BenchmarkSmokeAsset,
679
+ ) -> dict[str, Any]:
680
+ if asset.cell_id != "2wiki.multihop":
681
+ raise ArtifactError(f"unsupported 2Wiki cell: {asset.cell_id}")
682
+ try:
683
+ archive = zipfile.ZipFile(source)
684
+ except zipfile.BadZipFile as exc:
685
+ raise ArtifactError("2WikiMultiHopQA source is not a valid ZIP archive") from exc
686
+ with archive:
687
+ _validate_named_zip(
688
+ archive,
689
+ allowed_files=_TWO_WIKI_MEMBERS,
690
+ required_files={"dev.json", "id_aliases.json"},
691
+ max_members=8,
692
+ max_member_bytes=768 * 1024 * 1024,
693
+ max_uncompressed_bytes=900 * 1024 * 1024,
694
+ label="2WikiMultiHopQA",
695
+ )
696
+ rows = _read_json_member(archive, "dev.json", label="2WikiMultiHopQA dev")
697
+ alias_rows = _read_jsonl_member(archive, "id_aliases.json")
698
+ if not isinstance(rows, list) or not rows or not all(isinstance(row, dict) for row in rows):
699
+ raise ArtifactError("2WikiMultiHopQA dev must be a non-empty JSON array of objects")
700
+ expected_fields = {
701
+ "_id",
702
+ "answer",
703
+ "answer_id",
704
+ "context",
705
+ "entity_ids",
706
+ "evidences",
707
+ "evidences_id",
708
+ "question",
709
+ "supporting_facts",
710
+ "type",
711
+ }
712
+ for row in rows:
713
+ if set(row) != expected_fields:
714
+ raise ArtifactError("2WikiMultiHopQA dev row schema differs from the audited release")
715
+ aliases, alias_audit, alias_count = _normalize_two_wiki_aliases(alias_rows)
716
+ normalized = _normalize_hotpot_style_rows(
717
+ rows,
718
+ asset=asset,
719
+ source_split="dev",
720
+ two_wiki=True,
721
+ two_wiki_aliases=aliases,
722
+ )
723
+ normalized["audit.jsonl"] = alias_audit
724
+ normalized["counts"]["alias_entities"] = len(alias_audit)
725
+ normalized["counts"]["alias_strings"] = alias_count
726
+ return normalized
727
+
728
+
729
+ def _normalize_hotpotqa_parquet(
730
+ source: Path,
731
+ asset: BenchmarkSmokeAsset,
732
+ ) -> dict[str, Any]:
733
+ if asset.cell_id != "hotpotqa.distractor":
734
+ raise ArtifactError(f"unsupported HotpotQA cell: {asset.cell_id}")
735
+ rows = _read_parquet_rows(
736
+ source,
737
+ expected_columns={
738
+ "answer",
739
+ "context",
740
+ "id",
741
+ "level",
742
+ "question",
743
+ "supporting_facts",
744
+ "type",
745
+ },
746
+ label="HotpotQA distractor validation",
747
+ )
748
+ return _normalize_hotpot_style_rows(
749
+ rows,
750
+ asset=asset,
751
+ source_split="validation",
752
+ two_wiki=False,
753
+ )
754
+
755
+
756
+ def _normalize_hotpot_style_rows(
757
+ rows: list[dict[str, Any]],
758
+ *,
759
+ asset: BenchmarkSmokeAsset,
760
+ source_split: str,
761
+ two_wiki: bool,
762
+ two_wiki_aliases: Mapping[str, tuple[str, ...]] | None = None,
763
+ ) -> dict[str, Any]:
764
+ inputs: list[dict[str, Any]] = []
765
+ gold: list[dict[str, Any]] = []
766
+ context_count = 0
767
+ support_count = 0
768
+ orphan_support_count = 0
769
+ duplicate_context_count = 0
770
+ empty_context_count = 0
771
+ evidence_count = 0
772
+ ids: list[str] = []
773
+ label = "2WikiMultiHopQA" if two_wiki else "HotpotQA"
774
+ for row in rows:
775
+ id_key = "_id" if two_wiki else "id"
776
+ example_id = _required_text(row, id_key, label=f"{label} example")
777
+ ids.append(example_id)
778
+ raw_context = (
779
+ row.get("context")
780
+ if two_wiki
781
+ else _hotpot_struct_context(row.get("context"), label=label)
782
+ )
783
+ raw_support = (
784
+ row.get("supporting_facts")
785
+ if two_wiki
786
+ else _hotpot_struct_support(row.get("supporting_facts"), label=label)
787
+ )
788
+ if not isinstance(raw_context, list) or not raw_context:
789
+ raise ArtifactError(f"{label} context must be a non-empty list")
790
+ blocks: list[dict[str, Any]] = []
791
+ available_supports: set[str] = set()
792
+ visible_sentence_text: dict[str, str] = {}
793
+ for paragraph_index, item in enumerate(raw_context):
794
+ if not isinstance(item, list) or len(item) != 2:
795
+ raise ArtifactError(f"{label} context entries must be [title, sentences]")
796
+ title, sentences = item
797
+ if not isinstance(title, str) or not title.strip():
798
+ raise ArtifactError(f"{label} context title must be non-empty")
799
+ if not isinstance(sentences, list) or not sentences:
800
+ raise ArtifactError(f"{label} context sentences must be a non-empty list")
801
+ if not all(isinstance(sentence, str) for sentence in sentences):
802
+ raise ArtifactError(f"{label} context sentences must be strings")
803
+ for sentence_index, sentence in enumerate(sentences):
804
+ support_id = _support_identifier(title, sentence_index)
805
+ if not sentence.strip():
806
+ empty_context_count += 1
807
+ continue
808
+ if support_id in visible_sentence_text:
809
+ if visible_sentence_text[support_id] != sentence:
810
+ raise ArtifactError(f"{label} duplicate sentence ID has conflicting text")
811
+ duplicate_context_count += 1
812
+ else:
813
+ visible_sentence_text[support_id] = sentence
814
+ available_supports.add(support_id)
815
+ blocks.append(
816
+ {
817
+ "block_id": f"paragraph:{paragraph_index}:sentence:{sentence_index}",
818
+ "text": sentence,
819
+ "source": title,
820
+ "metadata": {
821
+ "official_support_id": support_id,
822
+ "paragraph_index": paragraph_index,
823
+ "sentence_index": sentence_index,
824
+ "title": title,
825
+ },
826
+ }
827
+ )
828
+ if not isinstance(raw_support, list) or not raw_support:
829
+ raise ArtifactError(f"{label} supporting facts must be a non-empty list")
830
+ support_ids: list[str] = []
831
+ orphan_support_ids: list[str] = []
832
+ for item in raw_support:
833
+ if not isinstance(item, list) or len(item) != 2:
834
+ raise ArtifactError(f"{label} supporting facts must contain [title, sent_id]")
835
+ title, sentence_index = item
836
+ if not isinstance(title, str) or not title.strip():
837
+ raise ArtifactError(f"{label} supporting-fact title must be non-empty")
838
+ if (
839
+ isinstance(sentence_index, bool)
840
+ or not isinstance(sentence_index, int)
841
+ or sentence_index < 0
842
+ ):
843
+ raise ArtifactError(f"{label} supporting-fact index must be non-negative")
844
+ support_id = _support_identifier(title, sentence_index)
845
+ if support_id not in available_supports:
846
+ # The official HotpotQA validation release contains a small
847
+ # number of source annotation defects (for example sentence
848
+ # index 902 in a five-sentence paragraph). Preserve these
849
+ # labels for scorer parity and record them explicitly instead
850
+ # of silently dropping or repairing upstream gold.
851
+ orphan_support_ids.append(support_id)
852
+ support_ids.append(support_id)
853
+ if len(set(support_ids)) != len(support_ids):
854
+ raise ArtifactError(f"{label} supporting facts contain duplicates")
855
+ evaluator_metadata: dict[str, Any] = {
856
+ "question_type": _required_text(row, "type", label=f"{label} example")
857
+ }
858
+ if not two_wiki:
859
+ evaluator_metadata["difficulty"] = _required_text(
860
+ row,
861
+ "level",
862
+ label=f"{label} example",
863
+ )
864
+ if orphan_support_ids:
865
+ evaluator_metadata["orphan_support_ids"] = sorted(orphan_support_ids)
866
+ if two_wiki:
867
+ if two_wiki_aliases is None: # pragma: no cover - caller invariant
868
+ raise ArtifactError("2Wiki aliases are required")
869
+ evidences, evidence_ids, evidence_options = _two_wiki_evidence_groups(
870
+ row,
871
+ aliases=two_wiki_aliases,
872
+ )
873
+ answer_id = row.get("answer_id")
874
+ if answer_id is not None and not isinstance(answer_id, str):
875
+ raise ArtifactError("2Wiki answer_id must be a string or null")
876
+ entity_ids = _required_text(row, "entity_ids", label="2Wiki example")
877
+ evaluator_metadata.update(
878
+ {
879
+ "answer_id": answer_id,
880
+ "entity_ids": entity_ids,
881
+ "evidence_options": evidence_options,
882
+ }
883
+ )
884
+ evidence_count += len(evidences)
885
+ answer = _required_text(row, "answer", label=f"{label} example")
886
+ answers = _ordered_unique(
887
+ (
888
+ answer,
889
+ *(two_wiki_aliases.get(answer_id, ()) if answer_id is not None else ()),
890
+ )
891
+ )
892
+ else:
893
+ answers = [_required_text(row, "answer", label=f"{label} example")]
894
+ inputs.append(
895
+ {
896
+ "id": example_id,
897
+ "query": _required_text(row, "question", label=f"{label} example"),
898
+ "instruction": (
899
+ "Answer the multi-hop question using the provided paragraphs and identify "
900
+ "the supporting facts."
901
+ ),
902
+ "visible_context": blocks,
903
+ "visible_metadata": {
904
+ "benchmark_cell": asset.cell_id,
905
+ "source_split": source_split,
906
+ },
907
+ }
908
+ )
909
+ gold.append(
910
+ {
911
+ "id": example_id,
912
+ "answers": answers,
913
+ "support_ids": sorted(support_ids),
914
+ **({"evidence_ids": evidence_ids} if two_wiki else {}),
915
+ "evaluator_metadata": evaluator_metadata,
916
+ }
917
+ )
918
+ context_count += len(blocks)
919
+ support_count += len(support_ids)
920
+ orphan_support_count += len(orphan_support_ids)
921
+ _require_unique_ids(ids, label=f"{label} examples")
922
+ inputs.sort(key=lambda item: item["id"])
923
+ gold.sort(key=lambda item: item["id"])
924
+ counts = {
925
+ "context_blocks": context_count,
926
+ "duplicate_context_sentences": duplicate_context_count,
927
+ "empty_context_sentences": empty_context_count,
928
+ "gold": len(gold),
929
+ "inputs": len(inputs),
930
+ "orphan_support_labels": orphan_support_count,
931
+ "support_labels": support_count,
932
+ }
933
+ if two_wiki:
934
+ counts["evidence_triples"] = evidence_count
935
+ return {"inputs.jsonl": inputs, "gold.jsonl": gold, "counts": counts}
936
+
937
+
938
+ def _normalize_qasper_tgz(
939
+ source: Path,
940
+ asset: BenchmarkSmokeAsset,
941
+ ) -> dict[str, Any]:
942
+ if asset.cell_id != "qasper.v0_3_dev":
943
+ raise ArtifactError(f"unsupported QASPER cell: {asset.cell_id}")
944
+ try:
945
+ with tarfile.open(source, mode="r:gz") as archive:
946
+ _validate_named_tar(
947
+ archive,
948
+ allowed_files=_QASPER_MEMBERS,
949
+ required_files={"qasper-dev-v0.3.json"},
950
+ max_members=8,
951
+ max_member_bytes=64 * 1024 * 1024,
952
+ max_uncompressed_bytes=128 * 1024 * 1024,
953
+ label="QASPER",
954
+ )
955
+ member = archive.getmember("qasper-dev-v0.3.json")
956
+ handle = archive.extractfile(member)
957
+ if handle is None: # pragma: no cover - validated regular member
958
+ raise ArtifactError("QASPER dev member cannot be read")
959
+ try:
960
+ papers = json.load(handle)
961
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
962
+ raise ArtifactError("QASPER dev member is not valid UTF-8 JSON") from exc
963
+ except tarfile.TarError as exc:
964
+ raise ArtifactError("QASPER source is not a valid gzip tar archive") from exc
965
+ if not isinstance(papers, dict) or not papers:
966
+ raise ArtifactError("QASPER dev data must be a non-empty object")
967
+ inputs: list[dict[str, Any]] = []
968
+ gold: list[dict[str, Any]] = []
969
+ question_ids: list[str] = []
970
+ annotation_count = 0
971
+ evidence_count = 0
972
+ context_count = 0
973
+ unique_context_count = 0
974
+ for paper_id in sorted(papers):
975
+ paper = papers[paper_id]
976
+ if not isinstance(paper, Mapping):
977
+ raise ArtifactError("QASPER paper must be an object")
978
+ blocks = _qasper_context_blocks(paper, paper_id=paper_id)
979
+ unique_context_count += len(blocks)
980
+ qas = paper.get("qas")
981
+ if not isinstance(qas, list) or not qas:
982
+ raise ArtifactError("QASPER paper requires a non-empty qas list")
983
+ for qa in qas:
984
+ if not isinstance(qa, Mapping):
985
+ raise ArtifactError("QASPER qa entry must be an object")
986
+ question_id = _required_text(qa, "question_id", label="QASPER question")
987
+ question_ids.append(question_id)
988
+ annotations = qa.get("answers")
989
+ if not isinstance(annotations, list) or not annotations:
990
+ raise ArtifactError("QASPER question requires answer annotations")
991
+ groups: list[dict[str, Any]] = []
992
+ annotation_ids: list[str] = []
993
+ for annotation in annotations:
994
+ if not isinstance(annotation, Mapping):
995
+ raise ArtifactError("QASPER answer annotation must be an object")
996
+ answer_payload = annotation.get("answer")
997
+ if not isinstance(answer_payload, Mapping):
998
+ raise ArtifactError("QASPER answer annotation requires an answer object")
999
+ answer_value, answer_type = _qasper_answer(answer_payload)
1000
+ raw_evidence = _string_list(
1001
+ answer_payload.get("evidence"),
1002
+ label="QASPER evidence",
1003
+ non_empty=False,
1004
+ allow_empty_items=True,
1005
+ )
1006
+ evidence = [item for item in (_normalize_space(x) for x in raw_evidence) if item]
1007
+ group = {
1008
+ "answers": [answer_value],
1009
+ "answer_type": answer_type,
1010
+ "evidence_texts": evidence,
1011
+ }
1012
+ groups.append(group)
1013
+ annotation_id = _required_text(
1014
+ annotation,
1015
+ "annotation_id",
1016
+ label="QASPER answer annotation",
1017
+ )
1018
+ annotation_ids.append(annotation_id)
1019
+ annotation_count += 1
1020
+ evidence_count += len(evidence)
1021
+ inputs.append(
1022
+ {
1023
+ "id": question_id,
1024
+ "query": _required_text(qa, "question", label="QASPER question"),
1025
+ "instruction": (
1026
+ "Answer the question using the provided scientific paper and return the "
1027
+ "supporting evidence passages."
1028
+ ),
1029
+ "visible_context": blocks,
1030
+ "visible_metadata": {
1031
+ "benchmark_cell": asset.cell_id,
1032
+ "paper_id": paper_id,
1033
+ "source_split": "dev",
1034
+ },
1035
+ }
1036
+ )
1037
+ context_count += len(blocks)
1038
+ gold.append(
1039
+ {
1040
+ "id": question_id,
1041
+ "reference_groups": groups,
1042
+ "evaluator_metadata": {"annotation_ids": annotation_ids},
1043
+ }
1044
+ )
1045
+ _require_unique_ids(question_ids, label="QASPER question IDs")
1046
+ inputs.sort(key=lambda item: item["id"])
1047
+ gold.sort(key=lambda item: item["id"])
1048
+ return {
1049
+ "inputs.jsonl": inputs,
1050
+ "gold.jsonl": gold,
1051
+ "counts": {
1052
+ "annotations": annotation_count,
1053
+ "context_blocks": context_count,
1054
+ "evidence_labels": evidence_count,
1055
+ "gold": len(gold),
1056
+ "inputs": len(inputs),
1057
+ "papers": len(papers),
1058
+ "unique_paper_context_blocks": unique_context_count,
1059
+ },
1060
+ }
1061
+
1062
+
1063
+ def _qasper_context_blocks(
1064
+ paper: Mapping[str, Any],
1065
+ *,
1066
+ paper_id: str,
1067
+ ) -> list[dict[str, Any]]:
1068
+ full_text = paper.get("full_text")
1069
+ if not isinstance(full_text, list) or not full_text:
1070
+ raise ArtifactError("QASPER paper requires non-empty full_text")
1071
+ blocks: list[dict[str, Any]] = []
1072
+ for section_index, section in enumerate(full_text):
1073
+ if not isinstance(section, Mapping):
1074
+ raise ArtifactError("QASPER full_text sections must be objects")
1075
+ section_name = section.get("section_name")
1076
+ if section_name is not None and not isinstance(section_name, str):
1077
+ raise ArtifactError("QASPER section_name must be a string or null")
1078
+ normalized_section = _normalize_space(section_name or "")
1079
+ if normalized_section:
1080
+ blocks.append(
1081
+ {
1082
+ "block_id": f"section:{section_index}:heading",
1083
+ "text": normalized_section,
1084
+ "source": paper_id,
1085
+ "metadata": {
1086
+ "kind": "section_heading",
1087
+ "section_index": section_index,
1088
+ },
1089
+ }
1090
+ )
1091
+ paragraphs = section.get("paragraphs")
1092
+ if not isinstance(paragraphs, list):
1093
+ raise ArtifactError("QASPER section paragraphs must be a list")
1094
+ for paragraph_index, paragraph in enumerate(paragraphs):
1095
+ if not isinstance(paragraph, str):
1096
+ raise ArtifactError("QASPER paragraphs must be strings")
1097
+ text = _normalize_space(paragraph)
1098
+ if not text:
1099
+ continue
1100
+ blocks.append(
1101
+ {
1102
+ "block_id": f"section:{section_index}:paragraph:{paragraph_index}",
1103
+ "text": text,
1104
+ "source": normalized_section or paper_id,
1105
+ "metadata": {
1106
+ "kind": "paragraph",
1107
+ "section_index": section_index,
1108
+ "paragraph_index": paragraph_index,
1109
+ },
1110
+ }
1111
+ )
1112
+ figures = paper.get("figures_and_tables")
1113
+ if not isinstance(figures, list):
1114
+ raise ArtifactError("QASPER figures_and_tables must be a list")
1115
+ for figure_index, figure in enumerate(figures):
1116
+ if not isinstance(figure, Mapping):
1117
+ raise ArtifactError("QASPER figure/table entries must be objects")
1118
+ caption = _normalize_space(_required_text(figure, "caption", label="QASPER figure/table"))
1119
+ blocks.append(
1120
+ {
1121
+ "block_id": f"float:{figure_index}",
1122
+ "text": f"FLOAT SELECTED: {caption}",
1123
+ "source": paper_id,
1124
+ "metadata": {
1125
+ "file": _optional_text(figure, "file", label="QASPER figure/table"),
1126
+ "kind": "figure_or_table",
1127
+ },
1128
+ }
1129
+ )
1130
+ if not blocks:
1131
+ raise ArtifactError("QASPER paper produced no visible context blocks")
1132
+ _require_unique_ids((item["block_id"] for item in blocks), label="QASPER context blocks")
1133
+ return blocks
1134
+
1135
+
1136
+ def _qasper_answer(answer: Mapping[str, Any]) -> tuple[str, str]:
1137
+ unanswerable = answer.get("unanswerable")
1138
+ if not isinstance(unanswerable, bool):
1139
+ raise ArtifactError("QASPER unanswerable must be boolean")
1140
+ if unanswerable:
1141
+ return "Unanswerable", "none"
1142
+ extractive = _string_list(
1143
+ answer.get("extractive_spans"),
1144
+ label="QASPER extractive_spans",
1145
+ non_empty=False,
1146
+ )
1147
+ free_form = answer.get("free_form_answer")
1148
+ if not isinstance(free_form, str):
1149
+ raise ArtifactError("QASPER free_form_answer must be a string")
1150
+ yes_no = answer.get("yes_no")
1151
+ if yes_no is not None and not isinstance(yes_no, bool):
1152
+ raise ArtifactError("QASPER yes_no must be boolean or null")
1153
+ populated = int(bool(extractive)) + int(bool(free_form)) + int(yes_no is not None)
1154
+ if populated != 1:
1155
+ raise ArtifactError("QASPER answer must contain exactly one answer type")
1156
+ if extractive:
1157
+ return ", ".join(extractive), "extractive"
1158
+ if free_form:
1159
+ return free_form, "abstractive"
1160
+ return ("Yes" if yes_no else "No"), "boolean"
1161
+
1162
+
1163
+ def _read_parquet_rows(
1164
+ source: Path,
1165
+ *,
1166
+ expected_columns: set[str],
1167
+ label: str,
1168
+ ) -> list[dict[str, Any]]:
1169
+ try:
1170
+ import pyarrow.parquet as parquet
1171
+ except ModuleNotFoundError as exc:
1172
+ raise ArtifactError(
1173
+ f"{label} preprocessing requires pyarrow; install agentinputlab[benchmarks]"
1174
+ ) from exc
1175
+ try:
1176
+ table = parquet.read_table(source)
1177
+ rows = table.to_pylist()
1178
+ except Exception as exc: # pyarrow raises version-specific Arrow exception classes
1179
+ raise ArtifactError(f"{label} source is not a readable Parquet file") from exc
1180
+ if set(table.column_names) != expected_columns or len(table.column_names) != len(
1181
+ expected_columns
1182
+ ):
1183
+ raise ArtifactError(
1184
+ f"{label} columns do not match the audited schema: {sorted(table.column_names)!r}"
1185
+ )
1186
+ if not rows or not all(isinstance(row, dict) for row in rows):
1187
+ raise ArtifactError(f"{label} contains no object rows")
1188
+ return rows
1189
+
1190
+
1191
+ def _hotpot_struct_context(value: Any, *, label: str) -> list[list[Any]]:
1192
+ if not isinstance(value, Mapping):
1193
+ raise ArtifactError(f"{label} context must be an object")
1194
+ titles = value.get("title")
1195
+ sentence_groups = value.get("sentences")
1196
+ if not isinstance(titles, list) or not isinstance(sentence_groups, list):
1197
+ raise ArtifactError(f"{label} context requires title and sentences lists")
1198
+ if len(titles) != len(sentence_groups):
1199
+ raise ArtifactError(f"{label} context title/sentences lengths differ")
1200
+ return [[title, sentences] for title, sentences in zip(titles, sentence_groups, strict=True)]
1201
+
1202
+
1203
+ def _hotpot_struct_support(value: Any, *, label: str) -> list[list[Any]]:
1204
+ if not isinstance(value, Mapping):
1205
+ raise ArtifactError(f"{label} supporting_facts must be an object")
1206
+ titles = value.get("title")
1207
+ sentence_ids = value.get("sent_id")
1208
+ if not isinstance(titles, list) or not isinstance(sentence_ids, list):
1209
+ raise ArtifactError(f"{label} supporting_facts requires title and sent_id lists")
1210
+ if len(titles) != len(sentence_ids):
1211
+ raise ArtifactError(f"{label} supporting-fact title/sent_id lengths differ")
1212
+ return [[title, sentence_id] for title, sentence_id in zip(titles, sentence_ids, strict=True)]
1213
+
1214
+
1215
+ def _normalize_two_wiki_aliases(
1216
+ rows: list[dict[str, Any]],
1217
+ ) -> tuple[dict[str, tuple[str, ...]], list[dict[str, Any]], int]:
1218
+ aliases: dict[str, tuple[str, ...]] = {}
1219
+ audit: list[dict[str, Any]] = []
1220
+ alias_count = 0
1221
+ for row in rows:
1222
+ if set(row) != {"Q_id", "aliases", "demonyms"}:
1223
+ raise ArtifactError("2Wiki alias row schema differs from the audited release")
1224
+ entity_id = _required_text(row, "Q_id", label="2Wiki alias row")
1225
+ if entity_id in aliases:
1226
+ raise ArtifactError("2Wiki alias table contains duplicate entity IDs")
1227
+ names = _string_list(row.get("aliases"), label="2Wiki aliases", non_empty=False)
1228
+ demonyms = _string_list(row.get("demonyms"), label="2Wiki demonyms", non_empty=False)
1229
+ combined = tuple(_ordered_unique((*names, *demonyms)))
1230
+ aliases[entity_id] = combined
1231
+ alias_count += len(combined)
1232
+ audit.append(
1233
+ {
1234
+ "id": entity_id,
1235
+ "aliases": names,
1236
+ "demonyms": demonyms,
1237
+ }
1238
+ )
1239
+ audit.sort(key=lambda item: item["id"])
1240
+ return aliases, audit, alias_count
1241
+
1242
+
1243
+ def _two_wiki_evidence_groups(
1244
+ row: Mapping[str, Any],
1245
+ *,
1246
+ aliases: Mapping[str, tuple[str, ...]],
1247
+ ) -> tuple[list[list[str]], list[str], list[list[str]]]:
1248
+ evidences = row.get("evidences")
1249
+ raw_ids = row.get("evidences_id")
1250
+ if not isinstance(evidences, list) or not evidences:
1251
+ raise ArtifactError("2Wiki evidences must be a non-empty list")
1252
+ if not isinstance(raw_ids, list):
1253
+ raise ArtifactError("2Wiki evidences_id must be a list")
1254
+ if raw_ids and len(raw_ids) != len(evidences):
1255
+ raise ArtifactError("2Wiki evidences and evidences_id lengths differ")
1256
+ normalized_evidences: list[list[str]] = []
1257
+ evidence_ids: list[str] = []
1258
+ evidence_options: list[list[str]] = []
1259
+ for index, evidence in enumerate(evidences):
1260
+ if (
1261
+ not isinstance(evidence, list)
1262
+ or len(evidence) != 3
1263
+ or not all(isinstance(item, str) and item.strip() for item in evidence)
1264
+ ):
1265
+ raise ArtifactError("2Wiki evidence must be a non-empty string triple")
1266
+ subject, relation, object_ = evidence
1267
+ normalized_evidences.append(list(evidence))
1268
+ literal_id = canonical_json(evidence)
1269
+ evidence_ids.append(literal_id)
1270
+ subjects = {subject}
1271
+ objects = {object_}
1272
+ if raw_ids:
1273
+ identifier = raw_ids[index]
1274
+ if (
1275
+ not isinstance(identifier, list)
1276
+ or len(identifier) != 3
1277
+ or not all(isinstance(item, str) and item.strip() for item in identifier)
1278
+ ):
1279
+ raise ArtifactError("2Wiki evidence IDs must be non-empty string triples")
1280
+ subject_id, relation_id, object_id = identifier
1281
+ if relation_id != relation:
1282
+ raise ArtifactError("2Wiki evidence relation ID differs from its relation label")
1283
+ subjects.update(aliases.get(subject_id, ()))
1284
+ objects.update(aliases.get(object_id, ()))
1285
+ options = sorted(
1286
+ canonical_json([subject_name, relation, object_name])
1287
+ for subject_name in subjects
1288
+ for object_name in objects
1289
+ )
1290
+ evidence_options.append(options)
1291
+ if len(set(evidence_ids)) != len(evidence_ids):
1292
+ raise ArtifactError("2Wiki evidences contain duplicate literal triples")
1293
+ return normalized_evidences, sorted(evidence_ids), evidence_options
1294
+
1295
+
1296
+ def _support_identifier(title: str, sentence_index: int) -> str:
1297
+ return canonical_json([title, sentence_index])
1298
+
1299
+
1300
+ def _string_list(
1301
+ value: Any,
1302
+ *,
1303
+ label: str,
1304
+ non_empty: bool,
1305
+ allow_empty_items: bool = False,
1306
+ ) -> list[str]:
1307
+ if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
1308
+ raise ArtifactError(f"{label} must be a list of strings")
1309
+ if non_empty and not value:
1310
+ raise ArtifactError(f"{label} must not be empty")
1311
+ if not allow_empty_items and any(not item.strip() for item in value):
1312
+ raise ArtifactError(f"{label} must not contain empty strings")
1313
+ return list(value)
1314
+
1315
+
1316
+ def _ordered_unique(values: Iterable[str]) -> list[str]:
1317
+ result: list[str] = []
1318
+ seen: set[str] = set()
1319
+ for value in values:
1320
+ if value not in seen:
1321
+ seen.add(value)
1322
+ result.append(value)
1323
+ return result
1324
+
1325
+
1326
+ def _require_unique_ids(values: Iterable[str], *, label: str) -> None:
1327
+ seen: set[str] = set()
1328
+ for value in values:
1329
+ if value in seen:
1330
+ raise ArtifactError(f"{label} contain duplicate IDs")
1331
+ seen.add(value)
1332
+
1333
+
1334
+ def _normalize_space(value: str) -> str:
1335
+ return " ".join(value.replace("\n", " ").split())
1336
+
1337
+
1338
+ def _normalize_musique_leakage_audit(value: Any) -> list[dict[str, Any]]:
1339
+ if not isinstance(value, dict) or not value:
1340
+ raise ArtifactError("MuSiQue leakage audit must be a non-empty object")
1341
+ records: list[dict[str, Any]] = []
1342
+ ids: list[str] = []
1343
+ for source in sorted(value):
1344
+ if not isinstance(source, str) or not source.strip():
1345
+ raise ArtifactError("MuSiQue leakage audit source must be non-empty")
1346
+ entries = value[source]
1347
+ if not isinstance(entries, list):
1348
+ raise ArtifactError("MuSiQue leakage audit source entries must be a list")
1349
+ for entry in entries:
1350
+ if not isinstance(entry, Mapping):
1351
+ raise ArtifactError("MuSiQue leakage audit entries must be objects")
1352
+ source_id = entry.get("id")
1353
+ if isinstance(source_id, bool) or not isinstance(source_id, (str, int)):
1354
+ raise ArtifactError("MuSiQue leakage audit IDs must be strings or integers")
1355
+ identity = canonical_json({"source": source, "source_id": source_id})
1356
+ record_id = f"{source}:{hashlib.sha256(identity.encode()).hexdigest()[:24]}"
1357
+ ids.append(record_id)
1358
+ records.append(
1359
+ {
1360
+ "id": record_id,
1361
+ "question": _required_text(
1362
+ entry,
1363
+ "question",
1364
+ label="MuSiQue leakage audit entry",
1365
+ ),
1366
+ "source": source,
1367
+ "source_id": source_id,
1368
+ }
1369
+ )
1370
+ _require_unique_ids(ids, label="MuSiQue leakage audit")
1371
+ records.sort(key=lambda item: item["id"])
1372
+ return records
1373
+
1374
+
1375
+ def _normalize_tau3_banking_tar(
1376
+ source: Path,
1377
+ asset: BenchmarkSmokeAsset,
1378
+ ) -> dict[str, Any]:
1379
+ """Normalize the official tau2 Banking Knowledge source without extracting it.
1380
+
1381
+ The upstream task files contain a user-simulator script, evaluator criteria,
1382
+ required-document labels, and initial database state. None of those fields
1383
+ is a valid optimizer input. Static preprocessing therefore emits only a
1384
+ task inventory; an explicit captured visible checkpoint is required later by
1385
+ :class:`Tau3BankingCheckpointAdapter`.
1386
+ """
1387
+
1388
+ if asset.cell_id != _TAU3_BANKING_CELL:
1389
+ raise ArtifactError(f"unsupported tau3 Banking Knowledge cell: {asset.cell_id}")
1390
+ selected = _read_tau3_banking_members(source, source_revision=asset.source_revision)
1391
+ data_root = f"{_TAU3_BANKING_DATA_ROOT}/"
1392
+
1393
+ task_members = {
1394
+ name: payload
1395
+ for name, payload in selected.items()
1396
+ if name.startswith(f"{data_root}tasks/task_") and name.endswith(".json")
1397
+ }
1398
+ document_members = {
1399
+ name: payload
1400
+ for name, payload in selected.items()
1401
+ if name.startswith(f"{data_root}documents/") and name.endswith(".json")
1402
+ }
1403
+ if not task_members or not document_members:
1404
+ raise ArtifactError("tau3 Banking Knowledge archive requires tasks and documents")
1405
+
1406
+ tasks: dict[str, dict[str, Any]] = {}
1407
+ for name, payload in sorted(task_members.items()):
1408
+ task = _tau3_json_object(payload, label=name)
1409
+ if set(task) != _TAU3_TASK_FIELDS:
1410
+ raise ArtifactError("tau3 Banking Knowledge task schema differs from the lock")
1411
+ task_id = _required_text(task, "id", label="tau3 Banking Knowledge task")
1412
+ if PurePosixPath(name).stem != task_id:
1413
+ raise ArtifactError("tau3 Banking Knowledge task ID differs from its filename")
1414
+ if task_id in tasks:
1415
+ raise ArtifactError("tau3 Banking Knowledge tasks contain duplicate IDs")
1416
+ _validate_tau3_task(task)
1417
+ tasks[task_id] = task
1418
+
1419
+ documents: dict[str, dict[str, Any]] = {}
1420
+ for name, payload in sorted(document_members.items()):
1421
+ document = _tau3_json_object(payload, label=name)
1422
+ if set(document) != _TAU3_DOCUMENT_FIELDS:
1423
+ raise ArtifactError("tau3 Banking Knowledge document schema differs from the lock")
1424
+ document_id = _required_text(
1425
+ document,
1426
+ "id",
1427
+ label="tau3 Banking Knowledge document",
1428
+ )
1429
+ if PurePosixPath(name).stem != document_id:
1430
+ raise ArtifactError("tau3 Banking Knowledge document ID differs from its filename")
1431
+ if document_id in documents:
1432
+ raise ArtifactError("tau3 Banking Knowledge documents contain duplicate IDs")
1433
+ _required_text(document, "title", label="tau3 Banking Knowledge document")
1434
+ _required_text(document, "content", label="tau3 Banking Knowledge document")
1435
+ documents[document_id] = document
1436
+
1437
+ required_document_mentions = 0
1438
+ required_document_ids: set[str] = set()
1439
+ gold_action_count = 0
1440
+ tasks_with_initial_state = 0
1441
+ tasks_with_initialization_data = 0
1442
+ tasks_with_message_history = 0
1443
+ reward_basis_counts: dict[str, int] = defaultdict(int)
1444
+ action_names: set[str] = set()
1445
+ for task in tasks.values():
1446
+ required_documents = task["required_documents"]
1447
+ required_document_mentions += len(required_documents)
1448
+ required_document_ids.update(required_documents)
1449
+ missing_documents = set(required_documents) - set(documents)
1450
+ if missing_documents:
1451
+ raise ArtifactError("tau3 Banking Knowledge task names missing required documents")
1452
+
1453
+ criteria = task["evaluation_criteria"]
1454
+ actions = criteria["actions"]
1455
+ gold_action_count += len(actions)
1456
+ for action in actions:
1457
+ action_names.add(_required_text(action, "name", label="tau3 gold action"))
1458
+ for basis in criteria["reward_basis"]:
1459
+ reward_basis_counts[basis] += 1
1460
+
1461
+ initial_state = task["initial_state"]
1462
+ if initial_state is not None:
1463
+ tasks_with_initial_state += 1
1464
+ if initial_state.get("initialization_data") is not None:
1465
+ tasks_with_initialization_data += 1
1466
+ if initial_state.get("message_history") is not None:
1467
+ tasks_with_message_history += 1
1468
+
1469
+ legacy_path = f"{_TAU3_BANKING_DATA_ROOT}/tasks.json"
1470
+ legacy_payload = _tau3_json_value(selected[legacy_path], label=legacy_path)
1471
+ if not isinstance(legacy_payload, list) or not all(
1472
+ isinstance(item, dict) for item in legacy_payload
1473
+ ):
1474
+ raise ArtifactError("tau3 legacy combined tasks must be an array of objects")
1475
+ legacy_tasks: dict[str, dict[str, Any]] = {}
1476
+ for task in legacy_payload:
1477
+ task_id = _required_text(task, "id", label="tau3 legacy task")
1478
+ if task_id in legacy_tasks:
1479
+ raise ArtifactError("tau3 legacy tasks contain duplicate IDs")
1480
+ legacy_tasks[task_id] = task
1481
+ if set(legacy_tasks) != set(tasks):
1482
+ raise ArtifactError("tau3 legacy and authoritative task inventories differ")
1483
+ legacy_mismatches = sorted(
1484
+ task_id
1485
+ for task_id in tasks
1486
+ if canonical_json(tasks[task_id]) != canonical_json(legacy_tasks[task_id])
1487
+ )
1488
+
1489
+ database_path = f"{_TAU3_BANKING_DATA_ROOT}/db.json"
1490
+ database = _tau3_json_object(selected[database_path], label=database_path)
1491
+ pyproject = _tau3_text(selected["pyproject.toml"], label="pyproject.toml")
1492
+ version_match = _TAU3_VERSION_PATTERN.search(pyproject)
1493
+ if version_match is None:
1494
+ raise ArtifactError("tau3 source pyproject does not declare a package version")
1495
+ package_version = version_match.group("version")
1496
+ if package_version != "1.0.1":
1497
+ raise ArtifactError("tau3 Banking Knowledge source must declare package version 1.0.1")
1498
+ license_text = _tau3_text(selected["LICENSE"], label="LICENSE")
1499
+ if "MIT License" not in license_text or "Permission is hereby granted" not in license_text:
1500
+ raise ArtifactError("tau3 source license does not match the audited MIT grant")
1501
+
1502
+ prompt_hashes = {
1503
+ name: hashlib.sha256(payload).hexdigest()
1504
+ for name, payload in sorted(selected.items())
1505
+ if name.startswith(f"{_TAU3_BANKING_DATA_ROOT}/prompts/")
1506
+ }
1507
+ runtime_hashes = {
1508
+ name: hashlib.sha256(payload).hexdigest()
1509
+ for name, payload in sorted(selected.items())
1510
+ if _tau3_runtime_member(name)
1511
+ }
1512
+
1513
+ inputs = [
1514
+ {
1515
+ "id": task_id,
1516
+ "checkpoint_required": True,
1517
+ "visible_metadata": {
1518
+ "benchmark_cell": asset.cell_id,
1519
+ "source_split": "base",
1520
+ "source_task_id": task_id,
1521
+ },
1522
+ }
1523
+ for task_id in sorted(tasks)
1524
+ ]
1525
+ gold = [{"id": task_id, "task_spec": tasks[task_id]} for task_id in sorted(tasks)]
1526
+ corpus = [
1527
+ {
1528
+ "id": document_id,
1529
+ "text": documents[document_id]["content"],
1530
+ "title": documents[document_id]["title"],
1531
+ }
1532
+ for document_id in sorted(documents)
1533
+ ]
1534
+ audit = [
1535
+ {
1536
+ "id": "tau3-banking-knowledge-source-audit",
1537
+ "authoritative_task_layout": "individual_task_files",
1538
+ "database": database,
1539
+ "database_sha256": hashlib.sha256(selected[database_path]).hexdigest(),
1540
+ "hidden_state_notice": (
1541
+ "Evaluator-only: never place database, user_scenario, evaluation_criteria, "
1542
+ "required_documents, or initial_state into an optimizer-visible checkpoint."
1543
+ ),
1544
+ "legacy_combined_task_mismatch_ids": legacy_mismatches,
1545
+ "license_sha256": hashlib.sha256(selected["LICENSE"]).hexdigest(),
1546
+ "package_name": "tau2",
1547
+ "package_version": package_version,
1548
+ "prompt_template_sha256": prompt_hashes,
1549
+ "reward_basis_task_counts": {
1550
+ key: reward_basis_counts[key] for key in sorted(reward_basis_counts)
1551
+ },
1552
+ "runtime_source_sha256": runtime_hashes,
1553
+ "source_revision": asset.source_revision,
1554
+ "wrapped_action_names": sorted(action_names),
1555
+ }
1556
+ ]
1557
+ return {
1558
+ "inputs.jsonl": inputs,
1559
+ "gold.jsonl": gold,
1560
+ "corpus.jsonl": corpus,
1561
+ "audit.jsonl": audit,
1562
+ "counts": {
1563
+ "audit": 1,
1564
+ "corpus": len(corpus),
1565
+ "documents": len(documents),
1566
+ "gold": len(gold),
1567
+ "gold_actions": gold_action_count,
1568
+ "inputs": len(inputs),
1569
+ "legacy_combined_task_mismatches": len(legacy_mismatches),
1570
+ "required_document_labels": required_document_mentions,
1571
+ "required_document_unique": len(required_document_ids),
1572
+ "tasks": len(tasks),
1573
+ "tasks_with_initial_state": tasks_with_initial_state,
1574
+ "tasks_with_initialization_data": tasks_with_initialization_data,
1575
+ "tasks_with_message_history": tasks_with_message_history,
1576
+ "user_simulator_scripts": len(tasks),
1577
+ },
1578
+ }
1579
+
1580
+
1581
+ def _normalize_bfcl_v3_simple_tar(
1582
+ source: Path,
1583
+ asset: BenchmarkSmokeAsset,
1584
+ ) -> dict[str, Any]:
1585
+ """Normalize the official BFCL v3 Python-simple cell without vendoring it.
1586
+
1587
+ Function definitions and the user request are legitimate model inputs. The
1588
+ possible-answer file is evaluator-only and is never copied into
1589
+ ``inputs.jsonl``. Scoring remains an external official-AST bridge: this
1590
+ processor records source identities but does not execute a model or create
1591
+ an official result.
1592
+ """
1593
+
1594
+ if asset.cell_id != _BFCL_V3_CELL:
1595
+ raise ArtifactError(f"unsupported BFCL v3 cell: {asset.cell_id}")
1596
+ selected = _read_bfcl_v3_members(source)
1597
+ prompts = _bfcl_jsonl(selected[_BFCL_V3_PROMPT_PATH], label=_BFCL_V3_PROMPT_PATH)
1598
+ answers = _bfcl_jsonl(selected[_BFCL_V3_GOLD_PATH], label=_BFCL_V3_GOLD_PATH)
1599
+ if len(prompts) != len(answers):
1600
+ raise ArtifactError("BFCL v3 prompt and possible-answer counts differ")
1601
+
1602
+ inputs: list[dict[str, Any]] = []
1603
+ gold: list[dict[str, Any]] = []
1604
+ prompt_ids: list[str] = []
1605
+ answer_ids: list[str] = []
1606
+ function_count = 0
1607
+ for prompt, answer in zip(prompts, answers, strict=True):
1608
+ if set(prompt) != {"function", "id", "question"}:
1609
+ raise ArtifactError("BFCL v3 simple prompt schema differs from the audited lock")
1610
+ if set(answer) != {"ground_truth", "id"}:
1611
+ raise ArtifactError("BFCL v3 simple gold schema differs from the audited lock")
1612
+ example_id = _required_text(prompt, "id", label="BFCL v3 simple prompt")
1613
+ answer_id = _required_text(answer, "id", label="BFCL v3 simple gold")
1614
+ if example_id != answer_id:
1615
+ raise ArtifactError("BFCL v3 prompt and possible-answer IDs differ")
1616
+ if not re.fullmatch(r"simple_[0-9]+", example_id):
1617
+ raise ArtifactError("BFCL v3 simple ID differs from the official category format")
1618
+
1619
+ question = prompt["question"]
1620
+ if (
1621
+ not isinstance(question, list)
1622
+ or len(question) != 1
1623
+ or not isinstance(question[0], list)
1624
+ or len(question[0]) != 1
1625
+ or not isinstance(question[0][0], Mapping)
1626
+ or set(question[0][0]) != {"content", "role"}
1627
+ or question[0][0].get("role") != "user"
1628
+ ):
1629
+ raise ArtifactError("BFCL v3 simple question must contain one user message")
1630
+ query = _required_text(
1631
+ question[0][0],
1632
+ "content",
1633
+ label="BFCL v3 simple user message",
1634
+ )
1635
+ functions = prompt["function"]
1636
+ if not isinstance(functions, list) or len(functions) != 1:
1637
+ raise ArtifactError("BFCL v3 simple prompt must contain exactly one function")
1638
+ function = functions[0]
1639
+ if not isinstance(function, Mapping) or set(function) != {
1640
+ "description",
1641
+ "name",
1642
+ "parameters",
1643
+ }:
1644
+ raise ArtifactError("BFCL v3 simple function schema differs from the audited lock")
1645
+ function_name = _required_text(
1646
+ function,
1647
+ "name",
1648
+ label="BFCL v3 simple function",
1649
+ )
1650
+ _required_text(function, "description", label="BFCL v3 simple function")
1651
+ if not isinstance(function.get("parameters"), Mapping):
1652
+ raise ArtifactError("BFCL v3 simple function parameters must be an object")
1653
+
1654
+ ground_truth = answer["ground_truth"]
1655
+ if (
1656
+ not isinstance(ground_truth, list)
1657
+ or not ground_truth
1658
+ or not all(isinstance(item, Mapping) for item in ground_truth)
1659
+ ):
1660
+ raise ArtifactError("BFCL v3 simple ground truth must be a non-empty object list")
1661
+ inputs.append(
1662
+ {
1663
+ "id": example_id,
1664
+ "query": query,
1665
+ "instruction": (
1666
+ "Return the single Python function call requested by the user, "
1667
+ "using the provided function schema."
1668
+ ),
1669
+ "visible_metadata": {
1670
+ "benchmark_cell": asset.cell_id,
1671
+ "bfcl_category": "simple",
1672
+ "function": dict(function),
1673
+ "source_split": "test",
1674
+ },
1675
+ "visible_tool_names": [function_name],
1676
+ }
1677
+ )
1678
+ gold.append(
1679
+ {
1680
+ "id": example_id,
1681
+ "ground_truth": [dict(item) for item in ground_truth],
1682
+ }
1683
+ )
1684
+ prompt_ids.append(example_id)
1685
+ answer_ids.append(answer_id)
1686
+ function_count += len(functions)
1687
+
1688
+ _require_unique_ids(prompt_ids, label="BFCL v3 simple prompt IDs")
1689
+ _require_unique_ids(answer_ids, label="BFCL v3 simple gold IDs")
1690
+ inputs.sort(key=lambda item: int(item["id"].rsplit("_", 1)[1]))
1691
+ gold.sort(key=lambda item: int(item["id"].rsplit("_", 1)[1]))
1692
+
1693
+ license_text = _bfcl_text(selected["LICENSE"], label="LICENSE")
1694
+ if "Apache License" not in license_text or "Version 2.0, January 2004" not in license_text:
1695
+ raise ArtifactError("BFCL source license does not match Apache-2.0")
1696
+ pyproject_path = f"{_BFCL_V3_SOURCE_ROOT}/pyproject.toml"
1697
+ pyproject = _bfcl_text(selected[pyproject_path], label=pyproject_path)
1698
+ if (
1699
+ 'name = "bfcl_eval"' not in pyproject
1700
+ or 'license = { "text" = "Apache 2.0" }' not in pyproject
1701
+ ):
1702
+ raise ArtifactError("BFCL package identity or license differs from the audited lock")
1703
+
1704
+ selected_hashes = {
1705
+ name: hashlib.sha256(payload).hexdigest() for name, payload in sorted(selected.items())
1706
+ }
1707
+ audit = [
1708
+ {
1709
+ "id": "bfcl-v3-simple-source-audit",
1710
+ "category": "simple",
1711
+ "evaluation_boundary": (
1712
+ "The official possible answers remain evaluator-only. AgentInputLab "
1713
+ "does not execute BFCL or claim an official score during preprocessing."
1714
+ ),
1715
+ "license_id": "Apache-2.0",
1716
+ "official_cli": "bfcl evaluate --test-category simple",
1717
+ "selected_source_sha256": selected_hashes,
1718
+ "source_revision": asset.source_revision,
1719
+ "source_tag": "v1.3",
1720
+ }
1721
+ ]
1722
+ return {
1723
+ "inputs.jsonl": inputs,
1724
+ "gold.jsonl": gold,
1725
+ "audit.jsonl": audit,
1726
+ "counts": {
1727
+ "audit": 1,
1728
+ "functions": function_count,
1729
+ "gold": len(gold),
1730
+ "ground_truth_candidates": sum(len(item["ground_truth"]) for item in gold),
1731
+ "inputs": len(inputs),
1732
+ "source_files": len(selected),
1733
+ },
1734
+ }
1735
+
1736
+
1737
+ def _read_bfcl_v3_members(source: Path) -> dict[str, bytes]:
1738
+ try:
1739
+ archive = tarfile.open(source, mode="r:*") # noqa: SIM115 - close after error remap
1740
+ except (tarfile.ReadError, tarfile.CompressionError) as exc:
1741
+ raise ArtifactError("BFCL v3 source is not a valid TAR archive") from exc
1742
+ with archive:
1743
+ members = archive.getmembers()
1744
+ if len(members) > _BFCL_V3_MAX_ARCHIVE_MEMBERS:
1745
+ raise ArtifactError("BFCL v3 archive contains too many members")
1746
+ names: set[str] = set()
1747
+ selected_members: dict[str, tarfile.TarInfo] = {}
1748
+ selected_size = 0
1749
+ for member in members:
1750
+ path = PurePosixPath(member.name)
1751
+ if path.is_absolute() or ".." in path.parts or "\\" in member.name:
1752
+ raise ArtifactError("BFCL v3 archive contains an unsafe path")
1753
+ if member.name in names:
1754
+ raise ArtifactError("BFCL v3 archive contains duplicate member names")
1755
+ names.add(member.name)
1756
+ if not path.parts or path.parts[0] != _BFCL_V3_ARCHIVE_ROOT:
1757
+ raise ArtifactError("BFCL v3 archive root differs from the v1.3 source tag")
1758
+ relative = PurePosixPath(*path.parts[1:]).as_posix()
1759
+ if relative not in _BFCL_V3_REQUIRED_FILES:
1760
+ continue
1761
+ if not member.isfile():
1762
+ raise ArtifactError("BFCL v3 selected archive member is not a regular file")
1763
+ if member.size > _BFCL_V3_MAX_SELECTED_MEMBER_BYTES:
1764
+ raise ArtifactError("BFCL v3 selected archive member exceeds the size limit")
1765
+ selected_size += member.size
1766
+ if selected_size > _BFCL_V3_MAX_SELECTED_BYTES:
1767
+ raise ArtifactError("BFCL v3 selected archive content exceeds the size limit")
1768
+ selected_members[relative] = member
1769
+
1770
+ missing = _BFCL_V3_REQUIRED_FILES - set(selected_members)
1771
+ if missing:
1772
+ raise ArtifactError(f"BFCL v3 archive is missing required files: {sorted(missing)!r}")
1773
+ selected: dict[str, bytes] = {}
1774
+ for relative, member in sorted(selected_members.items()):
1775
+ handle = archive.extractfile(member)
1776
+ if handle is None: # pragma: no cover - guarded by member.isfile()
1777
+ raise ArtifactError(f"BFCL v3 archive member cannot be read: {relative}")
1778
+ payload = handle.read(member.size + 1)
1779
+ if len(payload) != member.size:
1780
+ raise ArtifactError(f"BFCL v3 archive member is truncated: {relative}")
1781
+ selected[relative] = payload
1782
+ return selected
1783
+
1784
+
1785
+ def _bfcl_text(payload: bytes, *, label: str) -> str:
1786
+ try:
1787
+ return payload.decode("utf-8")
1788
+ except UnicodeDecodeError as exc:
1789
+ raise ArtifactError(f"BFCL v3 archive member is not UTF-8: {label}") from exc
1790
+
1791
+
1792
+ def _bfcl_jsonl(payload: bytes, *, label: str) -> list[dict[str, Any]]:
1793
+ records: list[dict[str, Any]] = []
1794
+ for line_number, line in enumerate(_bfcl_text(payload, label=label).splitlines(), start=1):
1795
+ if not line.strip():
1796
+ continue
1797
+ try:
1798
+ value = json.loads(line)
1799
+ except json.JSONDecodeError as exc:
1800
+ raise ArtifactError(f"invalid BFCL v3 JSONL in {label} at line {line_number}") from exc
1801
+ if not isinstance(value, dict):
1802
+ raise ArtifactError(f"BFCL v3 JSONL record in {label} must be an object")
1803
+ records.append(value)
1804
+ if not records:
1805
+ raise ArtifactError(f"BFCL v3 archive member contains no records: {label}")
1806
+ return records
1807
+
1808
+
1809
+ def _validate_tau3_task(task: Mapping[str, Any]) -> None:
1810
+ user_scenario = task["user_scenario"]
1811
+ if not isinstance(user_scenario, Mapping):
1812
+ raise ArtifactError("tau3 task user_scenario must be an object")
1813
+ _required_text(user_scenario, "instructions", label="tau3 task user_scenario")
1814
+
1815
+ required_documents = task["required_documents"]
1816
+ if not isinstance(required_documents, list) or not all(
1817
+ isinstance(item, str) and item.strip() for item in required_documents
1818
+ ):
1819
+ raise ArtifactError("tau3 task required_documents must be non-empty strings")
1820
+ if len(required_documents) != len(set(required_documents)):
1821
+ raise ArtifactError("tau3 task required_documents contain duplicates")
1822
+
1823
+ user_tools = task["user_tools"]
1824
+ if not isinstance(user_tools, list) or not all(
1825
+ isinstance(item, str) and item.strip() for item in user_tools
1826
+ ):
1827
+ raise ArtifactError("tau3 task user_tools must be strings")
1828
+
1829
+ initial_state = task["initial_state"]
1830
+ if initial_state is not None and not isinstance(initial_state, Mapping):
1831
+ raise ArtifactError("tau3 task initial_state must be an object or null")
1832
+
1833
+ criteria = task["evaluation_criteria"]
1834
+ if not isinstance(criteria, Mapping):
1835
+ raise ArtifactError("tau3 task evaluation_criteria must be an object")
1836
+ actions = criteria.get("actions")
1837
+ if not isinstance(actions, list) or not all(isinstance(item, Mapping) for item in actions):
1838
+ raise ArtifactError("tau3 task evaluation actions must be an array of objects")
1839
+ reward_basis = criteria.get("reward_basis")
1840
+ if (
1841
+ not isinstance(reward_basis, list)
1842
+ or not reward_basis
1843
+ or not all(isinstance(item, str) and item.strip() for item in reward_basis)
1844
+ ):
1845
+ raise ArtifactError("tau3 task reward_basis must be non-empty strings")
1846
+
1847
+
1848
+ def _read_tau3_banking_members(source: Path, *, source_revision: str) -> dict[str, bytes]:
1849
+ prefix = f"tau2-bench-{source_revision}"
1850
+ required = {
1851
+ "LICENSE",
1852
+ "pyproject.toml",
1853
+ f"{_TAU3_BANKING_DATA_ROOT}/db.json",
1854
+ f"{_TAU3_BANKING_DATA_ROOT}/tasks.json",
1855
+ f"{_TAU3_BANKING_SOURCE_ROOT}/environment.py",
1856
+ f"{_TAU3_BANKING_SOURCE_ROOT}/retrieval.py",
1857
+ f"{_TAU3_BANKING_SOURCE_ROOT}/tools.py",
1858
+ "src/tau2/data_model/tasks.py",
1859
+ "src/tau2/environment/environment.py",
1860
+ "src/tau2/evaluator/evaluator.py",
1861
+ "src/tau2/orchestrator/orchestrator.py",
1862
+ "src/tau2/runner/checkpoint.py",
1863
+ "src/tau2/user/user_simulator.py",
1864
+ }
1865
+ with _open_tau3_tar(source) as archive:
1866
+ members = archive.getmembers()
1867
+ if len(members) > _TAU3_MAX_ARCHIVE_MEMBERS:
1868
+ raise ArtifactError("tau3 archive contains too many members")
1869
+ names: set[str] = set()
1870
+ selected_members: list[tuple[str, tarfile.TarInfo]] = []
1871
+ selected_size = 0
1872
+ for member in members:
1873
+ path = PurePosixPath(member.name)
1874
+ if path.is_absolute() or ".." in path.parts or "\\" in member.name:
1875
+ raise ArtifactError("tau3 archive contains an unsafe path")
1876
+ if member.name in names:
1877
+ raise ArtifactError("tau3 archive contains duplicate member names")
1878
+ names.add(member.name)
1879
+ if not path.parts or path.parts[0] != prefix:
1880
+ raise ArtifactError("tau3 archive root differs from the pinned source revision")
1881
+ relative = PurePosixPath(*path.parts[1:]).as_posix()
1882
+ if not relative or not _tau3_selected_member(relative):
1883
+ continue
1884
+ if not member.isfile():
1885
+ raise ArtifactError("tau3 selected archive member is not a regular file")
1886
+ if member.size > _TAU3_MAX_SELECTED_MEMBER_BYTES:
1887
+ raise ArtifactError("tau3 selected archive member exceeds the size limit")
1888
+ selected_size += member.size
1889
+ if selected_size > _TAU3_MAX_SELECTED_BYTES:
1890
+ raise ArtifactError("tau3 selected archive content exceeds the size limit")
1891
+ selected_members.append((relative, member))
1892
+ if len(selected_members) > _TAU3_MAX_SELECTED_FILES:
1893
+ raise ArtifactError("tau3 archive contains too many selected files")
1894
+
1895
+ selected: dict[str, bytes] = {}
1896
+ for relative, member in selected_members:
1897
+ extracted = archive.extractfile(member)
1898
+ if extracted is None: # pragma: no cover - guarded by member.isfile()
1899
+ raise ArtifactError(f"tau3 archive member cannot be read: {relative}")
1900
+ payload = extracted.read(member.size + 1)
1901
+ if len(payload) != member.size:
1902
+ raise ArtifactError(f"tau3 archive member is truncated: {relative}")
1903
+ selected[relative] = payload
1904
+ missing = required - set(selected)
1905
+ if missing:
1906
+ raise ArtifactError(f"tau3 archive is missing required files: {sorted(missing)!r}")
1907
+ if not any(name.startswith(f"{_TAU3_BANKING_DATA_ROOT}/prompts/") for name in selected):
1908
+ raise ArtifactError("tau3 archive is missing Banking Knowledge prompt templates")
1909
+ return selected
1910
+
1911
+
1912
+ def _open_tau3_tar(source: Path) -> tarfile.TarFile:
1913
+ try:
1914
+ return tarfile.open(source, mode="r:*")
1915
+ except (tarfile.ReadError, tarfile.CompressionError) as exc:
1916
+ raise ArtifactError("tau3 source is not a valid TAR archive") from exc
1917
+
1918
+
1919
+ def _tau3_selected_member(name: str) -> bool:
1920
+ if name in {"LICENSE", "pyproject.toml", "src/tau2/registry.py"}:
1921
+ return True
1922
+ if name in {
1923
+ f"{_TAU3_BANKING_DATA_ROOT}/db.json",
1924
+ f"{_TAU3_BANKING_DATA_ROOT}/tasks.json",
1925
+ }:
1926
+ return True
1927
+ if name.startswith(f"{_TAU3_BANKING_DATA_ROOT}/tasks/"):
1928
+ return name.endswith(".json")
1929
+ if name.startswith(f"{_TAU3_BANKING_DATA_ROOT}/documents/"):
1930
+ return name.endswith(".json")
1931
+ if name.startswith(f"{_TAU3_BANKING_DATA_ROOT}/prompts/"):
1932
+ return name.endswith(".md")
1933
+ return _tau3_runtime_member(name)
1934
+
1935
+
1936
+ def _tau3_runtime_member(name: str) -> bool:
1937
+ return name == "src/tau2/registry.py" or (
1938
+ name.endswith(".py")
1939
+ and any(name.startswith(prefix) for prefix in _TAU3_RUNTIME_SOURCE_PREFIXES)
1940
+ )
1941
+
1942
+
1943
+ def _tau3_text(payload: bytes, *, label: str) -> str:
1944
+ try:
1945
+ return payload.decode("utf-8")
1946
+ except UnicodeDecodeError as exc:
1947
+ raise ArtifactError(f"tau3 archive member is not UTF-8: {label}") from exc
1948
+
1949
+
1950
+ def _tau3_json_value(payload: bytes, *, label: str) -> Any:
1951
+ try:
1952
+ return json.loads(_tau3_text(payload, label=label))
1953
+ except json.JSONDecodeError as exc:
1954
+ raise ArtifactError(f"tau3 archive member is not valid JSON: {label}") from exc
1955
+
1956
+
1957
+ def _tau3_json_object(payload: bytes, *, label: str) -> dict[str, Any]:
1958
+ value = _tau3_json_value(payload, label=label)
1959
+ if not isinstance(value, dict):
1960
+ raise ArtifactError(f"tau3 archive member must contain a JSON object: {label}")
1961
+ return value
1962
+
1963
+
1964
+ def _read_json_member(
1965
+ archive: zipfile.ZipFile,
1966
+ name: str,
1967
+ *,
1968
+ label: str,
1969
+ ) -> Any:
1970
+ text = _read_text_member(archive, name)
1971
+ try:
1972
+ return json.loads(text)
1973
+ except json.JSONDecodeError as exc:
1974
+ raise ArtifactError(f"{label} is not valid JSON") from exc
1975
+
1976
+
1977
+ def _validate_named_zip(
1978
+ archive: zipfile.ZipFile,
1979
+ *,
1980
+ allowed_files: set[str],
1981
+ required_files: set[str],
1982
+ max_members: int,
1983
+ max_member_bytes: int,
1984
+ max_uncompressed_bytes: int,
1985
+ label: str,
1986
+ ) -> None:
1987
+ infos = archive.infolist()
1988
+ if len(infos) > max_members:
1989
+ raise ArtifactError(f"{label} archive contains too many members")
1990
+ names: set[str] = set()
1991
+ files: set[str] = set()
1992
+ total = 0
1993
+ for info in infos:
1994
+ path = PurePosixPath(info.filename)
1995
+ if path.is_absolute() or ".." in path.parts or "\\" in info.filename:
1996
+ raise ArtifactError(f"{label} archive contains an unsafe path")
1997
+ if info.filename in names:
1998
+ raise ArtifactError(f"{label} archive contains duplicate member names")
1999
+ names.add(info.filename)
2000
+ mode = info.external_attr >> 16
2001
+ if stat.S_ISLNK(mode):
2002
+ raise ArtifactError(f"{label} archive contains a symbolic link")
2003
+ if info.is_dir():
2004
+ continue
2005
+ files.add(info.filename)
2006
+ if info.filename not in allowed_files:
2007
+ raise ArtifactError(f"{label} archive contains an unexpected file: {info.filename}")
2008
+ if info.file_size > max_member_bytes:
2009
+ raise ArtifactError(f"{label} archive member exceeds the size limit")
2010
+ total += info.file_size
2011
+ if info.file_size / max(info.compress_size, 1) > _MAX_COMPRESSION_RATIO:
2012
+ raise ArtifactError(f"{label} archive member exceeds the compression-ratio limit")
2013
+ if total > max_uncompressed_bytes:
2014
+ raise ArtifactError(f"{label} archive exceeds the uncompressed size limit")
2015
+ if not files >= required_files:
2016
+ raise ArtifactError(f"{label} archive is missing required files")
2017
+
2018
+
2019
+ def _validate_named_tar(
2020
+ archive: tarfile.TarFile,
2021
+ *,
2022
+ allowed_files: set[str],
2023
+ required_files: set[str],
2024
+ max_members: int,
2025
+ max_member_bytes: int,
2026
+ max_uncompressed_bytes: int,
2027
+ label: str,
2028
+ ) -> None:
2029
+ members = archive.getmembers()
2030
+ if len(members) > max_members:
2031
+ raise ArtifactError(f"{label} archive contains too many members")
2032
+ names: set[str] = set()
2033
+ files: set[str] = set()
2034
+ total = 0
2035
+ for member in members:
2036
+ path = PurePosixPath(member.name)
2037
+ if path.is_absolute() or ".." in path.parts or "\\" in member.name:
2038
+ raise ArtifactError(f"{label} archive contains an unsafe path")
2039
+ if member.name in names:
2040
+ raise ArtifactError(f"{label} archive contains duplicate member names")
2041
+ names.add(member.name)
2042
+ if member.isdir():
2043
+ continue
2044
+ if not member.isfile():
2045
+ raise ArtifactError(f"{label} archive contains a non-regular member")
2046
+ files.add(member.name)
2047
+ if member.name not in allowed_files:
2048
+ raise ArtifactError(f"{label} archive contains an unexpected file: {member.name}")
2049
+ if member.size > max_member_bytes:
2050
+ raise ArtifactError(f"{label} archive member exceeds the size limit")
2051
+ total += member.size
2052
+ if total > max_uncompressed_bytes:
2053
+ raise ArtifactError(f"{label} archive exceeds the uncompressed size limit")
2054
+ if not files >= required_files:
2055
+ raise ArtifactError(f"{label} archive is missing required files")
2056
+
2057
+
2058
+ def _normalize_clariq_tsv(
2059
+ source: Path,
2060
+ asset: BenchmarkSmokeAsset,
2061
+ ) -> dict[str, Any]:
2062
+ try:
2063
+ handle = source.open(encoding="utf-8", newline="")
2064
+ except UnicodeError as exc: # pragma: no cover - open is lazy on standard Python
2065
+ raise ArtifactError("ClariQ source is not valid UTF-8") from exc
2066
+ with handle:
2067
+ reader = csv.DictReader(handle, delimiter="\t")
2068
+ if tuple(reader.fieldnames or ()) != _CLARIQ_FIELDS:
2069
+ raise ArtifactError("ClariQ TSV schema does not match the audited dev schema")
2070
+ try:
2071
+ rows = [dict(row) for row in reader]
2072
+ except UnicodeDecodeError as exc:
2073
+ raise ArtifactError("ClariQ source is not valid UTF-8") from exc
2074
+ if not rows:
2075
+ raise ArtifactError("ClariQ TSV contains no records")
2076
+
2077
+ normalized: list[tuple[str, dict[str, Any], dict[str, Any]]] = []
2078
+ topics: set[str] = set()
2079
+ for row in rows:
2080
+ for field in (
2081
+ "topic_id",
2082
+ "initial_request",
2083
+ "topic_desc",
2084
+ "clarification_need",
2085
+ "facet_id",
2086
+ "facet_desc",
2087
+ "question_id",
2088
+ ):
2089
+ _required_text(row, field, label="ClariQ row")
2090
+ reference_question = _optional_text(row, "question", label="ClariQ row")
2091
+ reference_answer = _optional_text(row, "answer", label="ClariQ row")
2092
+ topic_id = row["topic_id"]
2093
+ topics.add(topic_id)
2094
+ try:
2095
+ clarification_need = int(row["clarification_need"])
2096
+ except ValueError as exc:
2097
+ raise ArtifactError("ClariQ clarification_need must be an integer") from exc
2098
+ if clarification_need not in {1, 2, 3, 4}:
2099
+ raise ArtifactError("ClariQ clarification_need must be in [1, 4]")
2100
+ row_hash = hashlib.sha256(canonical_json(row).encode("utf-8")).hexdigest()[:16]
2101
+ example_id = f"{topic_id}:{row['facet_id']}:{row['question_id']}:{row_hash}"
2102
+ visible = {
2103
+ "id": example_id,
2104
+ "query": row["initial_request"],
2105
+ "instruction": "Decide whether clarification is needed and, if so, ask one question.",
2106
+ "visible_metadata": {
2107
+ "benchmark_cell": asset.cell_id,
2108
+ "source_topic_id": topic_id,
2109
+ },
2110
+ }
2111
+ evaluator = {
2112
+ "id": example_id,
2113
+ "topic_description": row["topic_desc"],
2114
+ "clarification_need": clarification_need,
2115
+ "facet_id": row["facet_id"],
2116
+ "facet_description": row["facet_desc"],
2117
+ "question_id": row["question_id"],
2118
+ "reference_question": reference_question,
2119
+ "reference_answer": reference_answer,
2120
+ }
2121
+ normalized.append((example_id, visible, evaluator))
2122
+ normalized.sort(key=lambda item: item[0])
2123
+ ids = [item[0] for item in normalized]
2124
+ if len(set(ids)) != len(ids):
2125
+ raise ArtifactError("ClariQ normalization produced duplicate example IDs")
2126
+ return {
2127
+ "inputs.jsonl": [item[1] for item in normalized],
2128
+ "gold.jsonl": [item[2] for item in normalized],
2129
+ "counts": {
2130
+ "inputs": len(normalized),
2131
+ "gold": len(normalized),
2132
+ "topics": len(topics),
2133
+ },
2134
+ }
2135
+
2136
+
2137
+ def _validate_zip(archive: zipfile.ZipFile) -> None:
2138
+ infos = archive.infolist()
2139
+ if len(infos) > _MAX_ARCHIVE_MEMBERS:
2140
+ raise ArtifactError("benchmark archive contains too many members")
2141
+ names: set[str] = set()
2142
+ total = 0
2143
+ for info in infos:
2144
+ path = PurePosixPath(info.filename)
2145
+ if path.is_absolute() or ".." in path.parts or "\\" in info.filename:
2146
+ raise ArtifactError("benchmark archive contains an unsafe path")
2147
+ if info.filename in names:
2148
+ raise ArtifactError("benchmark archive contains duplicate member names")
2149
+ names.add(info.filename)
2150
+ mode = info.external_attr >> 16
2151
+ if stat.S_ISLNK(mode):
2152
+ raise ArtifactError("benchmark archive contains a symbolic link")
2153
+ if info.is_dir():
2154
+ continue
2155
+ if info.filename not in _BEIR_MEMBERS:
2156
+ raise ArtifactError(f"benchmark archive contains an unexpected file: {info.filename}")
2157
+ if info.file_size > _MAX_MEMBER_BYTES:
2158
+ raise ArtifactError("benchmark archive member exceeds the size limit")
2159
+ total += info.file_size
2160
+ denominator = max(info.compress_size, 1)
2161
+ if info.file_size / denominator > _MAX_COMPRESSION_RATIO:
2162
+ raise ArtifactError("benchmark archive member exceeds the compression-ratio limit")
2163
+ if total > _MAX_UNCOMPRESSED_BYTES:
2164
+ raise ArtifactError("benchmark archive exceeds the uncompressed size limit")
2165
+ if not names >= _BEIR_MEMBERS:
2166
+ raise ArtifactError("benchmark archive is missing required BEIR files")
2167
+
2168
+
2169
+ def _read_text_member(archive: zipfile.ZipFile, name: str) -> str:
2170
+ try:
2171
+ return archive.read(name).decode("utf-8")
2172
+ except (KeyError, UnicodeDecodeError) as exc:
2173
+ raise ArtifactError(f"benchmark archive member is missing or not UTF-8: {name}") from exc
2174
+
2175
+
2176
+ def _read_jsonl_member(archive: zipfile.ZipFile, name: str) -> list[dict[str, Any]]:
2177
+ text = _read_text_member(archive, name)
2178
+ records: list[dict[str, Any]] = []
2179
+ for line_number, line in enumerate(text.splitlines(), start=1):
2180
+ if not line.strip():
2181
+ continue
2182
+ try:
2183
+ value = json.loads(line)
2184
+ except json.JSONDecodeError as exc:
2185
+ raise ArtifactError(f"invalid JSONL in {name} at line {line_number}") from exc
2186
+ if not isinstance(value, dict):
2187
+ raise ArtifactError(f"JSONL record in {name} must be an object")
2188
+ records.append(value)
2189
+ if not records:
2190
+ raise ArtifactError(f"benchmark archive member contains no records: {name}")
2191
+ return records
2192
+
2193
+
2194
+ def _records_by_id(
2195
+ records: Iterable[dict[str, Any]],
2196
+ *,
2197
+ label: str,
2198
+ ) -> dict[str, dict[str, Any]]:
2199
+ indexed: dict[str, dict[str, Any]] = {}
2200
+ for record in records:
2201
+ record_id = _required_text(record, "_id", label=label)
2202
+ if record_id in indexed:
2203
+ raise ArtifactError(f"{label} contains duplicate IDs")
2204
+ indexed[record_id] = record
2205
+ return indexed
2206
+
2207
+
2208
+ def _required_text(record: Mapping[str, Any], key: str, *, label: str) -> str:
2209
+ value = record.get(key)
2210
+ if not isinstance(value, str) or not value.strip():
2211
+ raise ArtifactError(f"{label} requires non-empty string field {key!r}")
2212
+ return value
2213
+
2214
+
2215
+ def _optional_text(record: Mapping[str, Any], key: str, *, label: str) -> str:
2216
+ value = record.get(key, "")
2217
+ if not isinstance(value, str):
2218
+ raise ArtifactError(f"{label} field {key!r} must be a string")
2219
+ return value
2220
+
2221
+
2222
+ def _jsonl(records: Iterable[Mapping[str, Any]]) -> bytes:
2223
+ return ("\n".join(canonical_json(record) for record in records) + "\n").encode("utf-8")
2224
+
2225
+
2226
+ def _write_bytes(path: Path, content: bytes) -> None:
2227
+ with path.open("xb") as handle:
2228
+ handle.write(content)
2229
+ handle.flush()
2230
+ os.fsync(handle.fileno())
2231
+
2232
+
2233
+ def _require_expected_counts(asset: BenchmarkSmokeAsset, counts: Mapping[str, int]) -> None:
2234
+ if dict(asset.expected_counts) != dict(counts):
2235
+ raise ArtifactError(
2236
+ f"prepared count mismatch for {asset.asset_id}: expected "
2237
+ f"{dict(asset.expected_counts)!r}, got {dict(counts)!r}"
2238
+ )
2239
+
2240
+
2241
+ def _load_prepared(destination: Path, *, expected_asset: BenchmarkSmokeAsset) -> PreparedBenchmark:
2242
+ metadata_path = destination / "metadata.json"
2243
+ try:
2244
+ metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
2245
+ except (OSError, json.JSONDecodeError) as exc:
2246
+ raise ArtifactError(f"prepared benchmark metadata is invalid: {destination}") from exc
2247
+ if metadata.get("schema_version") != 1:
2248
+ raise ArtifactError("prepared benchmark metadata has an unsupported schema")
2249
+ expected_identity = {
2250
+ "asset_id": expected_asset.asset_id,
2251
+ "cell_id": expected_asset.cell_id,
2252
+ "source_revision": expected_asset.source_revision,
2253
+ "source_sha256": expected_asset.sha256,
2254
+ "source_size_bytes": expected_asset.size_bytes,
2255
+ "processor": expected_asset.processor,
2256
+ "processor_revision": _PROCESSOR_REVISIONS[expected_asset.processor],
2257
+ "gold_separated": True,
2258
+ }
2259
+ if expected_asset.processor not in {"beir_zip_v1", "clariq_tsv_v1"}:
2260
+ expected_identity["split"] = _processor_split(expected_asset.processor)
2261
+ for key, expected in expected_identity.items():
2262
+ if metadata.get(key) != expected:
2263
+ raise ArtifactError(f"prepared benchmark identity mismatch for {key}")
2264
+ raw_counts = metadata.get("counts")
2265
+ raw_files = metadata.get("files")
2266
+ if not isinstance(raw_counts, dict) or not isinstance(raw_files, dict):
2267
+ raise ArtifactError("prepared benchmark metadata is missing counts or files")
2268
+ if any(
2269
+ not isinstance(key, str)
2270
+ or not isinstance(value, int)
2271
+ or isinstance(value, bool)
2272
+ or value < 0
2273
+ for key, value in raw_counts.items()
2274
+ ):
2275
+ raise ArtifactError("prepared benchmark counts must be non-negative integers")
2276
+ counts = dict(raw_counts)
2277
+ files = {str(key): str(value) for key, value in raw_files.items()}
2278
+ _require_expected_counts(expected_asset, counts)
2279
+ required_files = _PROCESSOR_FILES[expected_asset.processor]
2280
+ if set(files) != required_files:
2281
+ raise ArtifactError("prepared benchmark metadata has an incomplete file set")
2282
+ if expected_asset.prepared_sha256 and files != dict(expected_asset.prepared_sha256):
2283
+ raise ArtifactError("prepared benchmark hashes do not match the audited lock")
2284
+ for name, expected_hash in files.items():
2285
+ if name not in {"inputs.jsonl", "gold.jsonl", "corpus.jsonl", "audit.jsonl"}:
2286
+ raise ArtifactError("prepared benchmark metadata names an unexpected file")
2287
+ path = destination / name
2288
+ if not path.is_file() or sha256_file(path) != expected_hash:
2289
+ raise ArtifactError(f"prepared benchmark cache verification failed: {name}")
2290
+ return PreparedBenchmark(
2291
+ asset_id=expected_asset.asset_id,
2292
+ processor=expected_asset.processor,
2293
+ processor_revision=_PROCESSOR_REVISIONS[expected_asset.processor],
2294
+ source_sha256=expected_asset.sha256,
2295
+ root=destination,
2296
+ counts=counts,
2297
+ file_sha256=files,
2298
+ cell_id=expected_asset.cell_id,
2299
+ source_revision=expected_asset.source_revision,
2300
+ split=_processor_split(expected_asset.processor),
2301
+ )
2302
+
2303
+
2304
+ def _processor_split(processor: str) -> str:
2305
+ if processor == "bfcl_v3_simple_tar_v1":
2306
+ return "test"
2307
+ if processor == "tau3_banking_tar_v1":
2308
+ return "base"
2309
+ if processor in {"musique_zip_v1", "two_wiki_zip_v1", "qasper_tgz_v1"}:
2310
+ return "dev"
2311
+ if processor == "hotpotqa_parquet_v1":
2312
+ return "validation"
2313
+ return "test"