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.
- agentinputlab/__init__.py +28 -0
- agentinputlab/algorithms/__init__.py +6 -0
- agentinputlab/algorithms/acquire/__init__.py +145 -0
- agentinputlab/algorithms/acquire/adaptive_rag/__init__.py +43 -0
- agentinputlab/algorithms/acquire/adaptive_rag/replay.py +367 -0
- agentinputlab/algorithms/acquire/adaptive_rag/runtime.py +1202 -0
- agentinputlab/algorithms/acquire/adaptive_rag_common_support.py +542 -0
- agentinputlab/algorithms/acquire/hyde.py +296 -0
- agentinputlab/algorithms/acquire/hyde_controlled.py +200 -0
- agentinputlab/algorithms/acquire/hyde_external.py +148 -0
- agentinputlab/algorithms/acquire/ircot.py +28 -0
- agentinputlab/algorithms/acquire/ircot_common_support.py +478 -0
- agentinputlab/algorithms/acquire/ircot_native.py +355 -0
- agentinputlab/algorithms/acquire/ircot_replay.py +327 -0
- agentinputlab/algorithms/acquire/qrag/__init__.py +79 -0
- agentinputlab/algorithms/acquire/qrag/bridge.py +828 -0
- agentinputlab/algorithms/acquire/qrag/common_support.py +891 -0
- agentinputlab/algorithms/acquire/qrag/native_axis.py +71 -0
- agentinputlab/algorithms/acquire/qrag/reader_adapter.py +139 -0
- agentinputlab/algorithms/acquire/r2ag/__init__.py +57 -0
- agentinputlab/algorithms/acquire/r2ag/bridge.py +1312 -0
- agentinputlab/algorithms/acquire/r2ag/common_support.py +1029 -0
- agentinputlab/algorithms/acquire/r2ag/socket_runtime.py +255 -0
- agentinputlab/algorithms/clarify/__init__.py +61 -0
- agentinputlab/algorithms/clarify/_structured.py +64 -0
- agentinputlab/algorithms/clarify/active_task_disambiguation.py +371 -0
- agentinputlab/algorithms/clarify/ask_or_assume/__init__.py +17 -0
- agentinputlab/algorithms/clarify/ask_or_assume/monitor.py +410 -0
- agentinputlab/algorithms/clarify/future_turns.py +379 -0
- agentinputlab/algorithms/clarify/udcs.py +255 -0
- agentinputlab/algorithms/clarify/udcs_deterministic.py +196 -0
- agentinputlab/algorithms/curate/__init__.py +65 -0
- agentinputlab/algorithms/curate/acon/__init__.py +27 -0
- agentinputlab/algorithms/curate/acon/common.py +284 -0
- agentinputlab/algorithms/curate/acon/common_support.py +136 -0
- agentinputlab/algorithms/curate/acon/history_bridge.py +309 -0
- agentinputlab/algorithms/curate/acon/observation_bridge.py +290 -0
- agentinputlab/algorithms/curate/llmlingua/__init__.py +30 -0
- agentinputlab/algorithms/curate/llmlingua/adapters.py +537 -0
- agentinputlab/algorithms/curate/llmlingua/budget.py +48 -0
- agentinputlab/algorithms/curate/llmlingua/controlled.py +175 -0
- agentinputlab/algorithms/curate/llmlingua/multilingual.py +105 -0
- agentinputlab/algorithms/curate/llmlingua/span_map.py +167 -0
- agentinputlab/algorithms/curate/recomp/__init__.py +47 -0
- agentinputlab/algorithms/curate/recomp/external.py +1129 -0
- agentinputlab/algorithms/curate/recomp/selective.py +304 -0
- agentinputlab/algorithms/decompose/__init__.py +107 -0
- agentinputlab/algorithms/decompose/decomp/__init__.py +53 -0
- agentinputlab/algorithms/decompose/decomp/common_support.py +478 -0
- agentinputlab/algorithms/decompose/decomp/planner_component.py +199 -0
- agentinputlab/algorithms/decompose/decomp/worker_bridge.py +416 -0
- agentinputlab/algorithms/decompose/llm_compiler/__init__.py +35 -0
- agentinputlab/algorithms/decompose/llm_compiler/bridge.py +520 -0
- agentinputlab/algorithms/decompose/llm_compiler/common_support.py +504 -0
- agentinputlab/algorithms/decompose/plan_and_budget/__init__.py +43 -0
- agentinputlab/algorithms/decompose/plan_and_budget/cached_soft_budget.py +362 -0
- agentinputlab/algorithms/decompose/plan_and_budget/decomposer_component.py +107 -0
- agentinputlab/algorithms/decompose/plan_and_budget/full.py +205 -0
- agentinputlab/algorithms/decompose/plan_and_budget/hard_cap.py +246 -0
- agentinputlab/algorithms/decompose/plan_and_budget/regenerated_soft_budget.py +448 -0
- agentinputlab/algorithms/decompose/plan_and_solve/__init__.py +33 -0
- agentinputlab/algorithms/decompose/plan_and_solve/plan_component.py +192 -0
- agentinputlab/algorithms/decompose/plan_and_solve/ps_prompt.py +127 -0
- agentinputlab/algorithms/decompose/self_ask/__init__.py +27 -0
- agentinputlab/algorithms/decompose/self_ask/loop.py +362 -0
- agentinputlab/algorithms/decompose/self_ask/planner_component.py +117 -0
- agentinputlab/algorithms/reformulate/__init__.py +85 -0
- agentinputlab/algorithms/reformulate/infocqr/__init__.py +40 -0
- agentinputlab/algorithms/reformulate/infocqr/cache_replay.py +362 -0
- agentinputlab/algorithms/reformulate/infocqr/external.py +675 -0
- agentinputlab/algorithms/reformulate/infocqr/student.py +189 -0
- agentinputlab/algorithms/reformulate/query2doc/__init__.py +42 -0
- agentinputlab/algorithms/reformulate/query2doc/controlled_live.py +190 -0
- agentinputlab/algorithms/reformulate/query2doc/models.py +95 -0
- agentinputlab/algorithms/reformulate/query2doc/official_cached.py +335 -0
- agentinputlab/algorithms/reformulate/query2doc/querygym_bridge.py +223 -0
- agentinputlab/algorithms/reformulate/query2doc/querygym_runtime.py +326 -0
- agentinputlab/algorithms/reformulate/reformer/__init__.py +23 -0
- agentinputlab/algorithms/reformulate/reformer/external.py +417 -0
- agentinputlab/algorithms/reformulate/rrr.py +696 -0
- agentinputlab/annotation/__init__.py +5 -0
- agentinputlab/annotation/independent.py +428 -0
- agentinputlab/annotation/server.py +233 -0
- agentinputlab/annotation/static/app.js +121 -0
- agentinputlab/annotation/static/index.html +60 -0
- agentinputlab/annotation/static/review.css +2 -0
- agentinputlab/annotation/static/review.html +10 -0
- agentinputlab/annotation/static/review.js +11 -0
- agentinputlab/annotation/static/styles.css +55 -0
- agentinputlab/annotation/store.py +444 -0
- agentinputlab/api.py +368 -0
- agentinputlab/artifacts/__init__.py +15 -0
- agentinputlab/artifacts/json_file.py +104 -0
- agentinputlab/artifacts/manifest.py +98 -0
- agentinputlab/artifacts/store.py +157 -0
- agentinputlab/backends/__init__.py +98 -0
- agentinputlab/backends/embedding/__init__.py +25 -0
- agentinputlab/backends/embedding/base.py +145 -0
- agentinputlab/backends/embedding/callable.py +75 -0
- agentinputlab/backends/embedding/replay.py +129 -0
- agentinputlab/backends/llm/__init__.py +31 -0
- agentinputlab/backends/llm/base.py +149 -0
- agentinputlab/backends/llm/callable.py +108 -0
- agentinputlab/backends/llm/decomp_completion.py +88 -0
- agentinputlab/backends/llm/final_content_stop.py +72 -0
- agentinputlab/backends/llm/gpt_oss_final_channel.py +84 -0
- agentinputlab/backends/llm/openai_compatible.py +332 -0
- agentinputlab/backends/llm/transformers.py +171 -0
- agentinputlab/backends/retrieval/__init__.py +35 -0
- agentinputlab/backends/retrieval/base.py +50 -0
- agentinputlab/backends/retrieval/dense.py +777 -0
- agentinputlab/backends/retrieval/memory.py +119 -0
- agentinputlab/backends/retrieval/sqlite_fts.py +155 -0
- agentinputlab/backends/user/__init__.py +15 -0
- agentinputlab/backends/user/base.py +66 -0
- agentinputlab/backends/user/callable.py +54 -0
- agentinputlab/backends/user/replay.py +89 -0
- agentinputlab/backends/vector.py +38 -0
- agentinputlab/benchmarks/__init__.py +246 -0
- agentinputlab/benchmarks/acquisition.py +106 -0
- agentinputlab/benchmarks/ale.py +540 -0
- agentinputlab/benchmarks/appworld_ul.py +669 -0
- agentinputlab/benchmarks/assets.py +226 -0
- agentinputlab/benchmarks/base.py +68 -0
- agentinputlab/benchmarks/beir_archive.py +235 -0
- agentinputlab/benchmarks/beir_sample.py +269 -0
- agentinputlab/benchmarks/bfcl_native.py +245 -0
- agentinputlab/benchmarks/bfcl_v3.py +607 -0
- agentinputlab/benchmarks/cache.py +218 -0
- agentinputlab/benchmarks/clariq.py +203 -0
- agentinputlab/benchmarks/data/registry.json +321 -0
- agentinputlab/benchmarks/data/smoke_assets.json +511 -0
- agentinputlab/benchmarks/data/statebench_v0_8_1_task_ids.json +475 -0
- agentinputlab/benchmarks/data/toolsandbox_165848b_scenario_names.json +1 -0
- agentinputlab/benchmarks/execution.py +1111 -0
- agentinputlab/benchmarks/interactive.py +257 -0
- agentinputlab/benchmarks/ir_query.py +204 -0
- agentinputlab/benchmarks/longbench.py +203 -0
- agentinputlab/benchmarks/paired_replay.py +479 -0
- agentinputlab/benchmarks/preprocess.py +2313 -0
- agentinputlab/benchmarks/production.py +236 -0
- agentinputlab/benchmarks/qa.py +963 -0
- agentinputlab/benchmarks/registry.py +110 -0
- agentinputlab/benchmarks/retrieval.py +459 -0
- agentinputlab/benchmarks/runtime.py +509 -0
- agentinputlab/benchmarks/statebench.py +938 -0
- agentinputlab/benchmarks/tau3.py +725 -0
- agentinputlab/benchmarks/toolsandbox.py +626 -0
- agentinputlab/benchmarks/trec_dl_sample.py +263 -0
- agentinputlab/campaign.py +654 -0
- agentinputlab/campaign_runner.py +953 -0
- agentinputlab/cli/__init__.py +6 -0
- agentinputlab/cli/main.py +933 -0
- agentinputlab/connectors/__init__.py +46 -0
- agentinputlab/connectors/knowledge/__init__.py +4 -0
- agentinputlab/connectors/tools/__init__.py +30 -0
- agentinputlab/connectors/tools/base.py +57 -0
- agentinputlab/connectors/tools/mcp.py +222 -0
- agentinputlab/connectors/tools/memory.py +41 -0
- agentinputlab/connectors/tools/openapi.py +695 -0
- agentinputlab/connectors/tools/schema.py +102 -0
- agentinputlab/contracts/__init__.py +91 -0
- agentinputlab/contracts/availability.py +24 -0
- agentinputlab/contracts/dependency.py +58 -0
- agentinputlab/contracts/enums.py +82 -0
- agentinputlab/contracts/errors.py +63 -0
- agentinputlab/contracts/evidence.py +43 -0
- agentinputlab/contracts/intent.py +110 -0
- agentinputlab/contracts/lifecycle.py +164 -0
- agentinputlab/contracts/patch.py +167 -0
- agentinputlab/contracts/serialization.py +93 -0
- agentinputlab/contracts/state.py +127 -0
- agentinputlab/contracts/trace.py +59 -0
- agentinputlab/contracts/worker.py +42 -0
- agentinputlab/core/__init__.py +7 -0
- agentinputlab/data/__init__.py +32 -0
- agentinputlab/data/checksums.py +9 -0
- agentinputlab/data/downloader.py +235 -0
- agentinputlab/data/http.py +171 -0
- agentinputlab/data/index.py +21 -0
- agentinputlab/data/overlap.py +28 -0
- agentinputlab/data/preprocess.py +32 -0
- agentinputlab/data/storage.py +57 -0
- agentinputlab/data/trec_sample.py +427 -0
- agentinputlab/evaluation/__init__.py +5 -0
- agentinputlab/evaluation/outcomes.py +32 -0
- agentinputlab/experiments/__init__.py +278 -0
- agentinputlab/experiments/analysis.py +981 -0
- agentinputlab/experiments/artifacts.py +687 -0
- agentinputlab/experiments/automatic_measurement.py +678 -0
- agentinputlab/experiments/auxiliary_matrix.py +203 -0
- agentinputlab/experiments/bindings.py +430 -0
- agentinputlab/experiments/blueprints.py +147 -0
- agentinputlab/experiments/builders/__init__.py +45 -0
- agentinputlab/experiments/builders/executable_external_payload.py +423 -0
- agentinputlab/experiments/builders/external_process_arena.py +1018 -0
- agentinputlab/experiments/builders/registry_intervention.py +463 -0
- agentinputlab/experiments/cli.py +158 -0
- agentinputlab/experiments/controls.py +340 -0
- agentinputlab/experiments/driver_guard.py +205 -0
- agentinputlab/experiments/drivers/__init__.py +89 -0
- agentinputlab/experiments/drivers/external_arena.py +2908 -0
- agentinputlab/experiments/drivers/method_process.py +662 -0
- agentinputlab/experiments/drivers/parity_process.py +1484 -0
- agentinputlab/experiments/drivers/static_production.py +1574 -0
- agentinputlab/experiments/executor.py +448 -0
- agentinputlab/experiments/loader.py +605 -0
- agentinputlab/experiments/measurement.py +675 -0
- agentinputlab/experiments/paper_analysis.py +1818 -0
- agentinputlab/experiments/parity_identity.py +605 -0
- agentinputlab/experiments/plan.py +277 -0
- agentinputlab/experiments/production_builders.py +3739 -0
- agentinputlab/experiments/readiness.py +432 -0
- agentinputlab/experiments/schema.py +514 -0
- agentinputlab/experiments/secrets.py +177 -0
- agentinputlab/experiments/stateful_analysis.py +1738 -0
- agentinputlab/experiments/transfer_analysis.py +1316 -0
- agentinputlab/experiments/unseen_analysis.py +1025 -0
- agentinputlab/integrations/__init__.py +1 -0
- agentinputlab/integrations/action_space/__init__.py +1 -0
- agentinputlab/integrations/external_workers/__init__.py +76 -0
- agentinputlab/integrations/external_workers/ace.py +431 -0
- agentinputlab/integrations/external_workers/base.py +181 -0
- agentinputlab/integrations/external_workers/decomp_live.py +249 -0
- agentinputlab/integrations/external_workers/decomp_planner.py +30 -0
- agentinputlab/integrations/external_workers/infocqr_student.py +280 -0
- agentinputlab/integrations/external_workers/ircot.py +576 -0
- agentinputlab/integrations/external_workers/llmcompiler_live.py +360 -0
- agentinputlab/integrations/external_workers/longllmlingua.py +162 -0
- agentinputlab/integrations/external_workers/method.py +314 -0
- agentinputlab/integrations/external_workers/process.py +678 -0
- agentinputlab/integrations/external_workers/supervisor.py +1071 -0
- agentinputlab/integrations/full_architecture/__init__.py +1 -0
- agentinputlab/integrations/full_architecture/hyde.py +212 -0
- agentinputlab/integrations/full_architecture/plan_and_budget.py +188 -0
- agentinputlab/integrations/live_history.py +222 -0
- agentinputlab/integrations/prompt_optimization/__init__.py +1 -0
- agentinputlab/integrations/training/__init__.py +1 -0
- agentinputlab/metrics/__init__.py +50 -0
- agentinputlab/metrics/catalog.py +313 -0
- agentinputlab/metrics/harm.py +95 -0
- agentinputlab/metrics/paired.py +40 -0
- agentinputlab/metrics/qa.py +76 -0
- agentinputlab/metrics/text.py +33 -0
- agentinputlab/metrics/third_party.py +161 -0
- agentinputlab/models/__init__.py +4 -0
- agentinputlab/observability/__init__.py +56 -0
- agentinputlab/observability/budget.py +316 -0
- agentinputlab/observability/instrumentation.py +754 -0
- agentinputlab/observability/models.py +403 -0
- agentinputlab/observability/monitor.py +380 -0
- agentinputlab/optimizers/__init__.py +1 -0
- agentinputlab/optimizers/clarify/__init__.py +12 -0
- agentinputlab/optimizers/context/__init__.py +22 -0
- agentinputlab/optimizers/decompose/__init__.py +64 -0
- agentinputlab/optimizers/evidence/__init__.py +22 -0
- agentinputlab/optimizers/rewrite/__init__.py +30 -0
- agentinputlab/parity/__init__.py +5 -0
- agentinputlab/parity/base.py +206 -0
- agentinputlab/production_campaign.py +763 -0
- agentinputlab/py.typed +1 -0
- agentinputlab/recipes/__init__.py +333 -0
- agentinputlab/recipes/ace.py +418 -0
- agentinputlab/recipes/ace_bfcl.py +273 -0
- agentinputlab/recipes/ace_common_support.py +169 -0
- agentinputlab/recipes/ace_native_plan.py +322 -0
- agentinputlab/recipes/ace_online.py +711 -0
- agentinputlab/recipes/ace_retrieval.py +330 -0
- agentinputlab/recipes/ace_statebench.py +382 -0
- agentinputlab/recipes/ace_tau.py +463 -0
- agentinputlab/recipes/ace_toolsandbox.py +432 -0
- agentinputlab/recipes/acon_guideline.py +22 -0
- agentinputlab/recipes/adaptive_rag.py +822 -0
- agentinputlab/recipes/llmlingua2_inference.py +146 -0
- agentinputlab/recipes/llmlingua2_training.py +796 -0
- agentinputlab/recipes/mce.py +1486 -0
- agentinputlab/recipes/mce_final.py +335 -0
- agentinputlab/recipes/mce_text_fit.py +499 -0
- agentinputlab/recipes/mce_text_only.py +298 -0
- agentinputlab/recipes/qrag.py +729 -0
- agentinputlab/recipes/qrag_inference.py +364 -0
- agentinputlab/recipes/r2ag_features.py +236 -0
- agentinputlab/recipes/r2ag_inference.py +503 -0
- agentinputlab/recipes/r2ag_training.py +948 -0
- agentinputlab/recipes/recomp_abstractive.py +592 -0
- agentinputlab/recipes/recomp_extractive.py +525 -0
- agentinputlab/recipes/reformer.py +17 -0
- agentinputlab/recipes/rqrag.py +1516 -0
- agentinputlab/recipes/rqrag_common_support.py +193 -0
- agentinputlab/recipes/rqrag_completion.py +147 -0
- agentinputlab/recipes/skillopt.py +462 -0
- agentinputlab/recipes/skillopt_common_support.py +154 -0
- agentinputlab/recipes/skillopt_environment.py +165 -0
- agentinputlab/recipes/skillopt_environment_cli.py +5 -0
- agentinputlab/recipes/skillopt_retrieval.py +188 -0
- agentinputlab/recipes/smartsearch.py +897 -0
- agentinputlab/recipes/smartsearch_curriculum.py +283 -0
- agentinputlab/recipes/smartsearch_grpo_contracts.py +58 -0
- agentinputlab/recipes/smartsearch_grpo_data.py +314 -0
- agentinputlab/recipes/smartsearch_grpo_meter.py +324 -0
- agentinputlab/recipes/smartsearch_grpo_prefix.py +144 -0
- agentinputlab/recipes/smartsearch_grpo_rollout.py +119 -0
- agentinputlab/recipes/smartsearch_grpo_tools.py +364 -0
- agentinputlab/recipes/smartsearch_grpo_training.py +305 -0
- agentinputlab/recipes/smartsearch_grpo_transport.py +248 -0
- agentinputlab/recipes/smartsearch_grpo_worker.py +364 -0
- agentinputlab/recipes/smartsearch_inference.py +524 -0
- agentinputlab/recipes/smartsearch_native_data.py +172 -0
- agentinputlab/recipes/smartsearch_policy.py +616 -0
- agentinputlab/recipes/smartsearch_policy_worker.py +9 -0
- agentinputlab/recipes/smartsearch_sampling.py +232 -0
- agentinputlab/recipes/smartsearch_teacher.py +203 -0
- agentinputlab/recipes/smartsearch_training.py +265 -0
- agentinputlab/registry/__init__.py +46 -0
- agentinputlab/registry/beta.py +727 -0
- agentinputlab/registry/data/algorithms.json +1959 -0
- agentinputlab/registry/data/auxiliary_method_profiles.json +34 -0
- agentinputlab/registry/loader.py +100 -0
- agentinputlab/registry/result_manifest.py +184 -0
- agentinputlab/registry/runtime.py +195 -0
- agentinputlab/registry/schema.py +449 -0
- agentinputlab/runner/__init__.py +30 -0
- agentinputlab/runner/balance.py +248 -0
- agentinputlab/runner/cache.py +135 -0
- agentinputlab/runner/composite.py +130 -0
- agentinputlab/runner/engine.py +285 -0
- agentinputlab/runner/runtime_guard.py +268 -0
- agentinputlab/runner/worker.py +80 -0
- agentinputlab/runtime_bootstrap.py +288 -0
- agentinputlab/selectors/__init__.py +196 -0
- agentinputlab/selectors/advanced.py +1514 -0
- agentinputlab/selectors/artifacts.py +1087 -0
- agentinputlab/selectors/base.py +112 -0
- agentinputlab/selectors/campaigns.py +1893 -0
- agentinputlab/selectors/catalog.py +86 -0
- agentinputlab/selectors/discovery.py +1860 -0
- agentinputlab/selectors/evaluation.py +1348 -0
- agentinputlab/selectors/features.py +383 -0
- agentinputlab/selectors/learned.py +502 -0
- agentinputlab/selectors/no_op.py +28 -0
- agentinputlab/selectors/outcome_table.py +475 -0
- agentinputlab/selectors/producer.py +853 -0
- agentinputlab/selectors/rules.py +109 -0
- agentinputlab/selectors/runtime.py +420 -0
- agentinputlab/studies/__init__.py +16 -0
- agentinputlab/studies/design.py +165 -0
- agentinputlab/studies/paired.py +322 -0
- agentinputlab/studio/__init__.py +6 -0
- agentinputlab/studio/campaigns.py +819 -0
- agentinputlab/studio/engine.py +1408 -0
- agentinputlab/studio/model_backends.py +425 -0
- agentinputlab/studio/prepared.py +410 -0
- agentinputlab/studio/server.py +613 -0
- agentinputlab/studio/static/app.js +397 -0
- agentinputlab/studio/static/campaigns.html +175 -0
- agentinputlab/studio/static/campaigns.js +426 -0
- agentinputlab/studio/static/index.html +192 -0
- agentinputlab/studio/static/result.html +103 -0
- agentinputlab/studio/static/result.js +273 -0
- agentinputlab/studio/static/styles.css +467 -0
- agentinputlab-0.1.0a1.data/data/share/agentinputlab/CITATION.cff +22 -0
- agentinputlab-0.1.0a1.dist-info/METADATA +392 -0
- agentinputlab-0.1.0a1.dist-info/RECORD +469 -0
- agentinputlab-0.1.0a1.dist-info/WHEEL +5 -0
- agentinputlab-0.1.0a1.dist-info/entry_points.txt +6 -0
- agentinputlab-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- agentinputlab-0.1.0a1.dist-info/licenses/NOTICE +31 -0
- agentinputlab-0.1.0a1.dist-info/licenses/THIRD_PARTY_NOTICES.md +94 -0
- agentinputlab-0.1.0a1.dist-info/top_level.txt +2 -0
- workers/README.md +94 -0
- workers/ace/README.md +261 -0
- workers/ace/__init__.py +1 -0
- workers/ace/compatibility_manifest.json +39 -0
- workers/ace/driver.py +1682 -0
- workers/acon/README.md +215 -0
- workers/acon/__init__.py +5 -0
- workers/acon/compatibility_manifest.json +93 -0
- workers/acon/driver.py +1859 -0
- workers/acon/fitted.py +395 -0
- workers/acon/guideline.py +470 -0
- workers/acon/guideline_source_manifest.json +20 -0
- workers/adaptive_rag/README.md +96 -0
- workers/adaptive_rag/__init__.py +15 -0
- workers/adaptive_rag/compatibility_manifest.json +61 -0
- workers/adaptive_rag/driver.py +1470 -0
- workers/adaptive_rag/fixture_runtime.py +163 -0
- workers/adaptive_rag/runtime.py +252 -0
- workers/ask_or_assume/README.md +124 -0
- workers/ask_or_assume/__init__.py +1 -0
- workers/ask_or_assume/compatibility_manifest.json +36 -0
- workers/ask_or_assume/driver.py +1227 -0
- workers/decomp/README.md +133 -0
- workers/decomp/UPSTREAM_LICENSE.Apache-2.0.txt +201 -0
- workers/decomp/__init__.py +5 -0
- workers/decomp/compatibility_manifest.json +205 -0
- workers/decomp/driver.py +1015 -0
- workers/decomp/live.py +137 -0
- workers/decomp/runtime.py +231 -0
- workers/infocqr/README.md +106 -0
- workers/infocqr/__init__.py +1 -0
- workers/infocqr/compatibility_manifest.json +84 -0
- workers/infocqr/driver.py +1766 -0
- workers/ircot/README.md +167 -0
- workers/ircot/__init__.py +7 -0
- workers/ircot/compatibility_manifest.json +121 -0
- workers/ircot/controlled.py +457 -0
- workers/ircot/driver.py +1373 -0
- workers/ircot/fixture_bindings.json +39 -0
- workers/ircot/fixture_config.json +48 -0
- workers/ircot/fixture_prompt.txt +2 -0
- workers/ircot/native_qa.py +398 -0
- workers/ircot/qa_compatibility_manifest.json +57 -0
- workers/ircot/runtime.py +97 -0
- workers/llmcompiler/README.md +160 -0
- workers/llmcompiler/__init__.py +1 -0
- workers/llmcompiler/compatibility_manifest.json +45 -0
- workers/llmcompiler/driver.py +1514 -0
- workers/llmcompiler/export_prompts.py +74 -0
- workers/llmcompiler/live.py +174 -0
- workers/llmcompiler/patch_manifest.json +147 -0
- workers/mce/README.md +198 -0
- workers/mce/__init__.py +29 -0
- workers/mce/compatibility_manifest.json +115 -0
- workers/mce/driver.py +2585 -0
- workers/mce/final_eval.py +173 -0
- workers/official_arenas/README.md +25 -0
- workers/official_arenas/__init__.py +1 -0
- workers/official_arenas/capture_records.py +110 -0
- workers/official_arenas/driver.py +619 -0
- workers/official_arenas/protocol.py +334 -0
- workers/official_arenas/providers/__init__.py +1 -0
- workers/official_arenas/providers/_live_history.py +228 -0
- workers/official_arenas/providers/_message_compat.py +77 -0
- workers/official_arenas/providers/appworld_ul.py +196 -0
- workers/official_arenas/providers/fixture.py +112 -0
- workers/official_arenas/providers/statebench.py +960 -0
- workers/official_arenas/providers/tau3.py +1672 -0
- workers/official_arenas/providers/tau_retail.py +586 -0
- workers/official_arenas/providers/toolsandbox.py +1321 -0
- workers/official_arenas/providers/tooltalk.py +699 -0
- workers/official_arenas/retail_capture.py +188 -0
- workers/official_arenas/retail_driver.py +275 -0
- workers/qrag/README.md +172 -0
- workers/qrag/__init__.py +1 -0
- workers/qrag/compatibility_manifest.json +64 -0
- workers/qrag/driver.py +673 -0
- workers/r2ag/README.md +154 -0
- workers/r2ag/__init__.py +5 -0
- workers/r2ag/compatibility_manifest.json +55 -0
- workers/r2ag/driver.py +1287 -0
- workers/r2ag/feature_schema.json +8 -0
- workers/r2ag/runtime.py +589 -0
- workers/recomp/README.md +120 -0
- workers/recomp/__init__.py +1 -0
- workers/recomp/compatibility_manifest.json +43 -0
- workers/recomp/driver.py +1653 -0
- workers/reformer/README.md +125 -0
- workers/reformer/__init__.py +1 -0
- workers/reformer/compatibility_manifest.json +68 -0
- workers/reformer/driver.py +1459 -0
- workers/reformer/induction.py +457 -0
- workers/rrr/README.md +126 -0
- workers/rrr/__init__.py +1 -0
- workers/rrr/compatibility_manifest.json +57 -0
- workers/rrr/driver.py +1133 -0
- workers/smartsearch/README.md +331 -0
- workers/smartsearch/__init__.py +1 -0
- workers/smartsearch/compatibility_manifest.json +41 -0
- 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"
|