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,1643 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Strict JSONL worker for pinned SmartSearch-3B frozen inference.
|
|
3
|
+
|
|
4
|
+
The worker executes the byte-verified upstream ``ReCall.run`` loop directly.
|
|
5
|
+
It replaces only two side-effect seams: the model POST is routed to a pinned
|
|
6
|
+
SGLang/OpenAI-completions deployment, and the upstream arbitrary-code executor
|
|
7
|
+
is replaced by a fixed Wikipedia-search call. The upstream sandbox is never
|
|
8
|
+
imported, started, or sent an ``env``/``call`` string.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import builtins
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
import types
|
|
23
|
+
import urllib.error
|
|
24
|
+
import urllib.parse
|
|
25
|
+
import urllib.request
|
|
26
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any, TextIO
|
|
30
|
+
|
|
31
|
+
UPSTREAM_COMMIT = "5e9e7ec0d6af34f45fad145d4bff442529e7e6ff"
|
|
32
|
+
CHECKPOINT_REPOSITORY = "vvv111222/SmartSearch-3B"
|
|
33
|
+
CHECKPOINT_REVISION = "82bfe8a845e3e08f0d9d00d8e1e2cfb6d50b5da8"
|
|
34
|
+
PROTOCOL = "agentinputlab.smartsearch.frozen.v2"
|
|
35
|
+
RELEASE = "recipe.smartsearch.frozen_3b_inference"
|
|
36
|
+
PROFILE_ID = "smartsearch.controlled_frozen.v1"
|
|
37
|
+
MANIFEST_SCHEMA = "agentinputlab.smartsearch.compatibility.v1"
|
|
38
|
+
TOOL_SCHEMA_SHA256 = "a1349ee4630e42cff2a0631252bdf931d3621f2f1a5bb005430e1c0494508731"
|
|
39
|
+
LOOP_CONTRACT_SHA256 = "8c64cda35d8b08f31c7dd8fba2efbb4e7adf3d57dc3a59a8a8e1c41e64d5e5b2"
|
|
40
|
+
PARSER_CONTRACT_SHA256 = "b2104370b1b39a1a34b809c25fd369a5e97426a70bf55300de5f5dec8bf3af6a"
|
|
41
|
+
DEFAULT_MANIFEST = Path(__file__).with_name("compatibility_manifest.json")
|
|
42
|
+
|
|
43
|
+
CHECKOUT_ENV = "AGENTINPUTLAB_SMARTSEARCH_CHECKOUT"
|
|
44
|
+
RUNTIME_MANIFEST_ENV = "AGENTINPUTLAB_SMARTSEARCH_RUNTIME_MANIFEST"
|
|
45
|
+
SOURCE_ARCHIVE_ENV = "AGENTINPUTLAB_SMARTSEARCH_SOURCE_ARCHIVE"
|
|
46
|
+
SNAPSHOT_COMMIT_ENV = "AGENTINPUTLAB_SMARTSEARCH_SNAPSHOT_COMMIT"
|
|
47
|
+
MODEL_API_KEY_ENV = "AGENTINPUTLAB_SMARTSEARCH_MODEL_API_KEY"
|
|
48
|
+
RETRIEVER_API_KEY_ENV = "AGENTINPUTLAB_SMARTSEARCH_RETRIEVER_API_KEY"
|
|
49
|
+
SANDBOX_ENV = "AGENTINPUTLAB_SMARTSEARCH_SANDBOX_URL"
|
|
50
|
+
LOCAL_RETRIEVER_TRANSPORT = "agentinputlab_prepared_corpus_v1"
|
|
51
|
+
LOCAL_MODEL_TRANSPORT = "openai_completions_local_snapshot"
|
|
52
|
+
|
|
53
|
+
MAX_REQUEST_BYTES = 8 * 1024 * 1024
|
|
54
|
+
MAX_RESPONSE_BYTES = 32 * 1024 * 1024
|
|
55
|
+
MAX_TEXT_CHARS = 8 * 1024 * 1024
|
|
56
|
+
_HEX = frozenset("0123456789abcdef")
|
|
57
|
+
_PROTOCOL_TAG = re.compile(r"</?(?:search|result|answer|think)>", re.IGNORECASE)
|
|
58
|
+
_TOP_FIELDS = frozenset(
|
|
59
|
+
{
|
|
60
|
+
"method_id",
|
|
61
|
+
"release_id",
|
|
62
|
+
"upstream_commit",
|
|
63
|
+
"state_hash",
|
|
64
|
+
"payload",
|
|
65
|
+
"dependency_fingerprints",
|
|
66
|
+
"minimum_reservation",
|
|
67
|
+
"protocol_version",
|
|
68
|
+
"request_id",
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
_PAYLOAD_FIELDS = frozenset(
|
|
72
|
+
{
|
|
73
|
+
"profile_id",
|
|
74
|
+
"task_id",
|
|
75
|
+
"question",
|
|
76
|
+
"artifacts",
|
|
77
|
+
"controls",
|
|
78
|
+
"remaining_budget",
|
|
79
|
+
"seed",
|
|
80
|
+
"inference_contract",
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
_ARTIFACT_FIELDS = frozenset(
|
|
84
|
+
{
|
|
85
|
+
"model_snapshot_manifest_sha256",
|
|
86
|
+
"model_snapshot_tree_sha256",
|
|
87
|
+
"tokenizer_revision",
|
|
88
|
+
"served_model_name",
|
|
89
|
+
"model_deployment_fingerprint_sha256",
|
|
90
|
+
"context_admission_fingerprint_sha256",
|
|
91
|
+
"retriever_revision",
|
|
92
|
+
"retriever_deployment_fingerprint_sha256",
|
|
93
|
+
"corpus_sha256",
|
|
94
|
+
"index_sha256",
|
|
95
|
+
"retrieval_config_sha256",
|
|
96
|
+
"compatibility_manifest_sha256",
|
|
97
|
+
"model_transport",
|
|
98
|
+
"checkpoint_repository",
|
|
99
|
+
"checkpoint_revision",
|
|
100
|
+
"profile_id",
|
|
101
|
+
"tool_schema_sha256",
|
|
102
|
+
"loop_contract_sha256",
|
|
103
|
+
"parser_contract_sha256",
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
_CONTRACT_FIELDS = frozenset(
|
|
107
|
+
{
|
|
108
|
+
"profile_id",
|
|
109
|
+
"tool_schema_sha256",
|
|
110
|
+
"loop_contract_sha256",
|
|
111
|
+
"parser_contract_sha256",
|
|
112
|
+
"max_rounds",
|
|
113
|
+
"top_n",
|
|
114
|
+
"max_new_tokens",
|
|
115
|
+
"temperature",
|
|
116
|
+
"stop",
|
|
117
|
+
"outer_retry_attempts",
|
|
118
|
+
"search_tags_per_turn",
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
_FORBIDDEN_KEYS = frozenset(
|
|
122
|
+
{
|
|
123
|
+
"gold",
|
|
124
|
+
"gold_answer",
|
|
125
|
+
"ground_truth",
|
|
126
|
+
"label",
|
|
127
|
+
"labels",
|
|
128
|
+
"reward",
|
|
129
|
+
"rewards",
|
|
130
|
+
"process_reward",
|
|
131
|
+
"reference_answer",
|
|
132
|
+
"target_answer",
|
|
133
|
+
"verifier_score",
|
|
134
|
+
"endpoint",
|
|
135
|
+
"url",
|
|
136
|
+
"command",
|
|
137
|
+
"callable",
|
|
138
|
+
"import",
|
|
139
|
+
"api_key",
|
|
140
|
+
"credential",
|
|
141
|
+
"sandbox",
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class DriverError(RuntimeError):
|
|
147
|
+
def __init__(self, code: str, message: str, *, consumed: bool = False) -> None:
|
|
148
|
+
super().__init__(message)
|
|
149
|
+
self.code = code
|
|
150
|
+
self.public_message = message
|
|
151
|
+
self.consumed = consumed
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class _UnboundRequestsModule(types.ModuleType):
|
|
155
|
+
"""Fail-closed stand-in for the upstream module's requests import."""
|
|
156
|
+
|
|
157
|
+
def __init__(self) -> None:
|
|
158
|
+
super().__init__("requests")
|
|
159
|
+
|
|
160
|
+
def post(self, *_args: Any, **_kwargs: Any) -> None:
|
|
161
|
+
raise DriverError(
|
|
162
|
+
"UPSTREAM_TRANSPORT_UNBOUND",
|
|
163
|
+
"the upstream requests seam is not bound to the controlled model transport",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(slots=True)
|
|
168
|
+
class Usage:
|
|
169
|
+
model_calls: int = 0
|
|
170
|
+
retrieval_calls: int = 0
|
|
171
|
+
input_tokens: int = 0
|
|
172
|
+
output_tokens: int = 0
|
|
173
|
+
|
|
174
|
+
def as_mapping(self) -> Mapping[str, int]:
|
|
175
|
+
return {
|
|
176
|
+
"model_calls": self.model_calls,
|
|
177
|
+
"retrieval_calls": self.retrieval_calls,
|
|
178
|
+
"input_tokens": self.input_tokens,
|
|
179
|
+
"output_tokens": self.output_tokens,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class BudgetGate:
|
|
184
|
+
def __init__(self, caps: Mapping[str, Any]) -> None:
|
|
185
|
+
self.caps = caps
|
|
186
|
+
self.usage = Usage()
|
|
187
|
+
|
|
188
|
+
def before_model(self, admitted_input_tokens: int | None) -> None:
|
|
189
|
+
if self.usage.model_calls >= self.caps["model_calls"]:
|
|
190
|
+
raise DriverError("MODEL_BUDGET_EXHAUSTED", "model budget exhausted")
|
|
191
|
+
remaining_output = self.caps["output_tokens"] - self.usage.output_tokens
|
|
192
|
+
if remaining_output < 8192:
|
|
193
|
+
raise DriverError(
|
|
194
|
+
"OUTPUT_BUDGET_EXHAUSTED",
|
|
195
|
+
"8192 output tokens must remain before a SmartSearch model call",
|
|
196
|
+
)
|
|
197
|
+
context_cap = self.caps["context_tokens"]
|
|
198
|
+
if context_cap is not None:
|
|
199
|
+
if admitted_input_tokens is None:
|
|
200
|
+
raise DriverError(
|
|
201
|
+
"CONTEXT_ADMISSION_REQUIRED",
|
|
202
|
+
"an exact tokenizer admission result is required before model execution",
|
|
203
|
+
)
|
|
204
|
+
if self.usage.input_tokens + admitted_input_tokens > context_cap:
|
|
205
|
+
raise DriverError(
|
|
206
|
+
"CONTEXT_BUDGET_EXHAUSTED",
|
|
207
|
+
"prompt is rejected before model execution by the context budget",
|
|
208
|
+
)
|
|
209
|
+
self.usage.model_calls += 1
|
|
210
|
+
|
|
211
|
+
def after_model(
|
|
212
|
+
self,
|
|
213
|
+
input_tokens: int,
|
|
214
|
+
output_tokens: int,
|
|
215
|
+
*,
|
|
216
|
+
admitted_input_tokens: int | None,
|
|
217
|
+
) -> None:
|
|
218
|
+
self.usage.input_tokens += input_tokens
|
|
219
|
+
self.usage.output_tokens += output_tokens
|
|
220
|
+
if admitted_input_tokens is not None and input_tokens != admitted_input_tokens:
|
|
221
|
+
raise DriverError(
|
|
222
|
+
"CONTEXT_ADMISSION_MISMATCH",
|
|
223
|
+
"provider usage disagrees with the preflight tokenizer admission",
|
|
224
|
+
consumed=True,
|
|
225
|
+
)
|
|
226
|
+
context_cap = self.caps["context_tokens"]
|
|
227
|
+
if context_cap is not None and self.usage.input_tokens > context_cap:
|
|
228
|
+
raise DriverError(
|
|
229
|
+
"CONTEXT_BUDGET_EXCEEDED",
|
|
230
|
+
"model-reported context token usage exceeded the supplied budget",
|
|
231
|
+
consumed=True,
|
|
232
|
+
)
|
|
233
|
+
if self.usage.output_tokens > self.caps["output_tokens"]:
|
|
234
|
+
raise DriverError(
|
|
235
|
+
"OUTPUT_BUDGET_EXCEEDED",
|
|
236
|
+
"model-reported output token usage exceeded the supplied budget",
|
|
237
|
+
consumed=True,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def before_retrieval(self) -> None:
|
|
241
|
+
if self.usage.retrieval_calls >= self.caps["retrieval_calls"]:
|
|
242
|
+
raise DriverError(
|
|
243
|
+
"RETRIEVAL_BUDGET_EXHAUSTED",
|
|
244
|
+
"retrieval budget exhausted",
|
|
245
|
+
consumed=bool(self.usage.model_calls),
|
|
246
|
+
)
|
|
247
|
+
self.usage.retrieval_calls += 1
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@dataclass(frozen=True, slots=True)
|
|
251
|
+
class ModelTurn:
|
|
252
|
+
text: str
|
|
253
|
+
finish_reason: str
|
|
254
|
+
matched: str
|
|
255
|
+
input_tokens: int
|
|
256
|
+
output_tokens: int
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
260
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
261
|
+
del req, fp, code, msg, headers, newurl
|
|
262
|
+
raise DriverError("REDIRECT_REFUSED", "HTTP redirects are disabled", consumed=True)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class _HTTPJSON:
|
|
266
|
+
def __init__(self, *, api_key: str, timeout: float) -> None:
|
|
267
|
+
self.api_key = api_key
|
|
268
|
+
self.timeout = timeout
|
|
269
|
+
self.opener = urllib.request.build_opener(_NoRedirect())
|
|
270
|
+
|
|
271
|
+
def post(self, endpoint: str, body: Mapping[str, Any]) -> Any:
|
|
272
|
+
parsed = urllib.parse.urlsplit(endpoint)
|
|
273
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
274
|
+
raise DriverError("ENDPOINT_INVALID", "endpoint must be an HTTP(S) URL")
|
|
275
|
+
if parsed.username or parsed.password or parsed.query or parsed.fragment:
|
|
276
|
+
raise DriverError("ENDPOINT_INVALID", "endpoint credentials/query are forbidden")
|
|
277
|
+
if self.api_key and parsed.scheme != "https":
|
|
278
|
+
raise DriverError("ENDPOINT_INVALID", "API keys may only be sent over HTTPS")
|
|
279
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
280
|
+
if self.api_key:
|
|
281
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
282
|
+
request = urllib.request.Request(
|
|
283
|
+
endpoint,
|
|
284
|
+
data=_canonical_json(body).encode("utf-8"),
|
|
285
|
+
headers=headers,
|
|
286
|
+
method="POST",
|
|
287
|
+
)
|
|
288
|
+
try:
|
|
289
|
+
with self.opener.open(request, timeout=self.timeout) as response:
|
|
290
|
+
raw = response.read(MAX_RESPONSE_BYTES + 1)
|
|
291
|
+
except DriverError:
|
|
292
|
+
raise
|
|
293
|
+
except urllib.error.HTTPError as exc:
|
|
294
|
+
raise DriverError(
|
|
295
|
+
"HTTP_FAILED", f"endpoint returned HTTP {exc.code}", consumed=True
|
|
296
|
+
) from exc
|
|
297
|
+
except Exception as exc:
|
|
298
|
+
raise DriverError("HTTP_FAILED", "endpoint request failed", consumed=True) from exc
|
|
299
|
+
if len(raw) > MAX_RESPONSE_BYTES:
|
|
300
|
+
raise DriverError(
|
|
301
|
+
"RESPONSE_TOO_LARGE", "endpoint response exceeds byte limit", consumed=True
|
|
302
|
+
)
|
|
303
|
+
try:
|
|
304
|
+
return _strict_json_loads(raw)
|
|
305
|
+
except (UnicodeDecodeError, json.JSONDecodeError, DriverError) as exc:
|
|
306
|
+
raise DriverError(
|
|
307
|
+
"RESPONSE_INVALID", "endpoint returned invalid JSON", consumed=True
|
|
308
|
+
) from exc
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class HTTPModelTransport:
|
|
312
|
+
def __init__(self, config: Mapping[str, Any], *, api_key: str = "") -> None:
|
|
313
|
+
self.mode = _text(config, "model_transport")
|
|
314
|
+
self.base_url = _http_base(_text(config, "model_base_url"))
|
|
315
|
+
self.tokenizer_base_url = _http_base(_text(config, "tokenizer_base_url"))
|
|
316
|
+
self.model = _text(config, "served_model_name")
|
|
317
|
+
self.identity = {
|
|
318
|
+
key: config[key]
|
|
319
|
+
for key in (
|
|
320
|
+
"checkpoint_repository",
|
|
321
|
+
"checkpoint_revision",
|
|
322
|
+
"model_snapshot_tree_sha256",
|
|
323
|
+
"tokenizer_revision",
|
|
324
|
+
"served_model_name",
|
|
325
|
+
"context_admission_fingerprint_sha256",
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
self.http = _HTTPJSON(
|
|
329
|
+
api_key=api_key, timeout=_positive_float(config, "model_timeout_seconds")
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def fingerprint(self) -> Mapping[str, Any]:
|
|
333
|
+
return {
|
|
334
|
+
"transport": self.mode,
|
|
335
|
+
"base_url": self.base_url,
|
|
336
|
+
"tokenizer_base_url": self.tokenizer_base_url,
|
|
337
|
+
"identity": self.identity,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
def count_prompt_tokens(self, prompt: str) -> int:
|
|
341
|
+
result = self.http.post(
|
|
342
|
+
self.tokenizer_base_url + "/tokenize",
|
|
343
|
+
{
|
|
344
|
+
"model": self.model,
|
|
345
|
+
"tokenizer_revision": self.identity["tokenizer_revision"],
|
|
346
|
+
"text": prompt,
|
|
347
|
+
},
|
|
348
|
+
)
|
|
349
|
+
if not isinstance(result, Mapping) or result.get("identity") != self.identity:
|
|
350
|
+
raise DriverError(
|
|
351
|
+
"CONTEXT_ADMISSION_IDENTITY_MISMATCH",
|
|
352
|
+
"tokenizer admission identity mismatch",
|
|
353
|
+
consumed=True,
|
|
354
|
+
)
|
|
355
|
+
return _nonnegative(result.get("input_tokens"), "admitted input_tokens")
|
|
356
|
+
|
|
357
|
+
def generate(self, prompt: str, sampling: Mapping[str, Any]) -> ModelTurn:
|
|
358
|
+
if self.mode == "sglang_generate":
|
|
359
|
+
result = self.http.post(
|
|
360
|
+
self.base_url + "/generate",
|
|
361
|
+
{"text": prompt, "sampling_params": dict(sampling)},
|
|
362
|
+
)
|
|
363
|
+
if not isinstance(result, Mapping) or result.get("identity") != self.identity:
|
|
364
|
+
raise DriverError(
|
|
365
|
+
"MODEL_IDENTITY_MISMATCH",
|
|
366
|
+
"SGLang immutable model identity mismatch",
|
|
367
|
+
consumed=True,
|
|
368
|
+
)
|
|
369
|
+
meta = result.get("meta_info")
|
|
370
|
+
if not isinstance(meta, Mapping):
|
|
371
|
+
raise DriverError(
|
|
372
|
+
"MODEL_RESPONSE_INVALID", "SGLang meta_info is required", consumed=True
|
|
373
|
+
)
|
|
374
|
+
finish = meta.get("finish_reason")
|
|
375
|
+
if not isinstance(finish, Mapping):
|
|
376
|
+
raise DriverError(
|
|
377
|
+
"MODEL_RESPONSE_INVALID", "SGLang finish_reason is required", consumed=True
|
|
378
|
+
)
|
|
379
|
+
return ModelTurn(
|
|
380
|
+
text=_bounded_text(result.get("text"), "model text"),
|
|
381
|
+
finish_reason=str(finish.get("type", "")),
|
|
382
|
+
matched=str(finish.get("matched", "")),
|
|
383
|
+
input_tokens=_nonnegative(meta.get("prompt_tokens"), "prompt_tokens"),
|
|
384
|
+
output_tokens=_nonnegative(meta.get("completion_tokens"), "completion_tokens"),
|
|
385
|
+
)
|
|
386
|
+
if self.mode != "openai_completions":
|
|
387
|
+
raise DriverError("MODEL_CONFIGURATION_INVALID", "unsupported model transport")
|
|
388
|
+
result = self.http.post(
|
|
389
|
+
self.base_url + "/v1/completions",
|
|
390
|
+
{
|
|
391
|
+
"model": self.model,
|
|
392
|
+
"prompt": prompt,
|
|
393
|
+
"temperature": sampling["temperature"],
|
|
394
|
+
"max_tokens": sampling["max_new_tokens"],
|
|
395
|
+
"stop": sampling["stop"],
|
|
396
|
+
},
|
|
397
|
+
)
|
|
398
|
+
if not isinstance(result, Mapping) or result.get("identity") != self.identity:
|
|
399
|
+
raise DriverError(
|
|
400
|
+
"MODEL_IDENTITY_MISMATCH",
|
|
401
|
+
"completion immutable model identity mismatch",
|
|
402
|
+
consumed=True,
|
|
403
|
+
)
|
|
404
|
+
choices = result.get("choices")
|
|
405
|
+
usage = result.get("usage")
|
|
406
|
+
if not isinstance(choices, Sequence) or not choices or not isinstance(choices[0], Mapping):
|
|
407
|
+
raise DriverError(
|
|
408
|
+
"MODEL_RESPONSE_INVALID", "completion choice is required", consumed=True
|
|
409
|
+
)
|
|
410
|
+
if not isinstance(usage, Mapping):
|
|
411
|
+
raise DriverError(
|
|
412
|
+
"MODEL_RESPONSE_INVALID", "completion usage is required", consumed=True
|
|
413
|
+
)
|
|
414
|
+
text = _bounded_text(choices[0].get("text"), "completion text")
|
|
415
|
+
text, matched = _completion_stop(text, choices[0])
|
|
416
|
+
return ModelTurn(
|
|
417
|
+
text=text,
|
|
418
|
+
finish_reason=str(choices[0].get("finish_reason", "")),
|
|
419
|
+
matched=matched,
|
|
420
|
+
input_tokens=_nonnegative(usage.get("prompt_tokens"), "prompt_tokens"),
|
|
421
|
+
output_tokens=_nonnegative(usage.get("completion_tokens"), "completion_tokens"),
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class LocalSnapshotOpenAIModel:
|
|
426
|
+
"""Use a local vLLM endpoint after matching it to the frozen model root."""
|
|
427
|
+
|
|
428
|
+
def __init__(self, config: Mapping[str, Any], *, api_key: str = "") -> None:
|
|
429
|
+
self.mode = _text(config, "model_transport")
|
|
430
|
+
if self.mode != LOCAL_MODEL_TRANSPORT:
|
|
431
|
+
raise DriverError("MODEL_CONFIGURATION_INVALID", "local model transport drifted")
|
|
432
|
+
self.base_url = _http_base(_text(config, "model_base_url"))
|
|
433
|
+
self.tokenizer_base_url = _http_base(_text(config, "tokenizer_base_url"))
|
|
434
|
+
self.model = _text(config, "served_model_name")
|
|
435
|
+
self.model_root = Path(_text(config, "local_model_root")).expanduser().resolve()
|
|
436
|
+
self.identity = {
|
|
437
|
+
key: config[key]
|
|
438
|
+
for key in (
|
|
439
|
+
"checkpoint_repository",
|
|
440
|
+
"checkpoint_revision",
|
|
441
|
+
"model_snapshot_tree_sha256",
|
|
442
|
+
"tokenizer_revision",
|
|
443
|
+
"served_model_name",
|
|
444
|
+
"context_admission_fingerprint_sha256",
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
self.http = _HTTPJSON(
|
|
448
|
+
api_key=api_key,
|
|
449
|
+
timeout=_positive_float(config, "model_timeout_seconds"),
|
|
450
|
+
)
|
|
451
|
+
self._verify_served_root()
|
|
452
|
+
|
|
453
|
+
def _verify_served_root(self) -> None:
|
|
454
|
+
request = urllib.request.Request(
|
|
455
|
+
self.base_url + "/v1/models",
|
|
456
|
+
headers={"Accept": "application/json"},
|
|
457
|
+
method="GET",
|
|
458
|
+
)
|
|
459
|
+
try:
|
|
460
|
+
with urllib.request.urlopen(request, timeout=self.http.timeout) as response:
|
|
461
|
+
raw = response.read(MAX_RESPONSE_BYTES + 1)
|
|
462
|
+
except Exception as exc:
|
|
463
|
+
raise DriverError(
|
|
464
|
+
"MODEL_IDENTITY_MISMATCH", "local vLLM model inventory is unavailable"
|
|
465
|
+
) from exc
|
|
466
|
+
if len(raw) > MAX_RESPONSE_BYTES:
|
|
467
|
+
raise DriverError("MODEL_IDENTITY_MISMATCH", "model inventory is too large")
|
|
468
|
+
result = _json_object(raw, "model inventory")
|
|
469
|
+
rows = result.get("data")
|
|
470
|
+
if isinstance(rows, (str, bytes)) or not isinstance(rows, Sequence):
|
|
471
|
+
raise DriverError("MODEL_IDENTITY_MISMATCH", "model inventory is invalid")
|
|
472
|
+
matches = [
|
|
473
|
+
item for item in rows if isinstance(item, Mapping) and item.get("id") == self.model
|
|
474
|
+
]
|
|
475
|
+
if len(matches) != 1:
|
|
476
|
+
raise DriverError("MODEL_IDENTITY_MISMATCH", "served model alias is not unique")
|
|
477
|
+
served_root = matches[0].get("root")
|
|
478
|
+
if not isinstance(served_root, str):
|
|
479
|
+
raise DriverError("MODEL_IDENTITY_MISMATCH", "served model root is absent")
|
|
480
|
+
if Path(served_root).expanduser().resolve() != self.model_root:
|
|
481
|
+
raise DriverError("MODEL_IDENTITY_MISMATCH", "served model root differs")
|
|
482
|
+
|
|
483
|
+
def fingerprint(self) -> Mapping[str, Any]:
|
|
484
|
+
return {
|
|
485
|
+
"transport": self.mode,
|
|
486
|
+
"base_url": self.base_url,
|
|
487
|
+
"tokenizer_base_url": self.tokenizer_base_url,
|
|
488
|
+
"identity": self.identity,
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
def count_prompt_tokens(self, prompt: str) -> int:
|
|
492
|
+
result = self.http.post(
|
|
493
|
+
self.tokenizer_base_url + "/tokenize",
|
|
494
|
+
{"model": self.model, "prompt": prompt},
|
|
495
|
+
)
|
|
496
|
+
if not isinstance(result, Mapping):
|
|
497
|
+
raise DriverError(
|
|
498
|
+
"CONTEXT_ADMISSION_FAILED", "local tokenizer response is invalid", consumed=True
|
|
499
|
+
)
|
|
500
|
+
return _nonnegative(result.get("count"), "admitted input_tokens")
|
|
501
|
+
|
|
502
|
+
def generate(self, prompt: str, sampling: Mapping[str, Any]) -> ModelTurn:
|
|
503
|
+
result = self.http.post(
|
|
504
|
+
self.base_url + "/v1/completions",
|
|
505
|
+
{
|
|
506
|
+
"model": self.model,
|
|
507
|
+
"prompt": prompt,
|
|
508
|
+
"temperature": sampling["temperature"],
|
|
509
|
+
"max_tokens": sampling["max_new_tokens"],
|
|
510
|
+
"stop": sampling["stop"],
|
|
511
|
+
},
|
|
512
|
+
)
|
|
513
|
+
choices = result.get("choices") if isinstance(result, Mapping) else None
|
|
514
|
+
usage = result.get("usage") if isinstance(result, Mapping) else None
|
|
515
|
+
if not isinstance(choices, Sequence) or not choices or not isinstance(choices[0], Mapping):
|
|
516
|
+
raise DriverError(
|
|
517
|
+
"MODEL_RESPONSE_INVALID", "completion choice is required", consumed=True
|
|
518
|
+
)
|
|
519
|
+
if not isinstance(usage, Mapping):
|
|
520
|
+
raise DriverError(
|
|
521
|
+
"MODEL_RESPONSE_INVALID", "completion usage is required", consumed=True
|
|
522
|
+
)
|
|
523
|
+
text = _bounded_text(choices[0].get("text"), "completion text")
|
|
524
|
+
text, matched = _completion_stop(text, choices[0])
|
|
525
|
+
return ModelTurn(
|
|
526
|
+
text=text,
|
|
527
|
+
finish_reason=str(choices[0].get("finish_reason", "")),
|
|
528
|
+
matched=matched,
|
|
529
|
+
input_tokens=_nonnegative(usage.get("prompt_tokens"), "prompt_tokens"),
|
|
530
|
+
output_tokens=_nonnegative(usage.get("completion_tokens"), "completion_tokens"),
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
class HTTPRetriever:
|
|
535
|
+
def __init__(self, config: Mapping[str, Any], *, api_key: str = "") -> None:
|
|
536
|
+
self.base_url = _http_base(_text(config, "retriever_base_url"))
|
|
537
|
+
self.http = _HTTPJSON(
|
|
538
|
+
api_key=api_key,
|
|
539
|
+
timeout=_positive_float(config, "retriever_timeout_seconds"),
|
|
540
|
+
)
|
|
541
|
+
self.identity = {
|
|
542
|
+
key: config[key]
|
|
543
|
+
for key in (
|
|
544
|
+
"retriever_revision",
|
|
545
|
+
"corpus_sha256",
|
|
546
|
+
"index_sha256",
|
|
547
|
+
"retrieval_config_sha256",
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
def fingerprint(self) -> Mapping[str, Any]:
|
|
552
|
+
return {
|
|
553
|
+
"transport": "controlled_flashrag_search_v1",
|
|
554
|
+
"base_url": self.base_url,
|
|
555
|
+
**self.identity,
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
def search(self, query: str, *, top_n: int) -> Sequence[Mapping[str, Any]]:
|
|
559
|
+
result = self.http.post(
|
|
560
|
+
self.base_url + "/search",
|
|
561
|
+
{"query": query, "top_n": top_n, "return_score": False},
|
|
562
|
+
)
|
|
563
|
+
if not isinstance(result, Mapping) or result.get("identity") != self.identity:
|
|
564
|
+
raise DriverError(
|
|
565
|
+
"RETRIEVER_IDENTITY_MISMATCH", "retriever identity mismatch", consumed=True
|
|
566
|
+
)
|
|
567
|
+
documents = result.get("documents")
|
|
568
|
+
if not isinstance(documents, Sequence) or isinstance(documents, (str, bytes)):
|
|
569
|
+
raise DriverError(
|
|
570
|
+
"RETRIEVER_RESPONSE_INVALID", "retriever documents are required", consumed=True
|
|
571
|
+
)
|
|
572
|
+
return documents
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class LocalPreparedRetriever:
|
|
576
|
+
"""Run the same checksum-locked production retriever inside the worker."""
|
|
577
|
+
|
|
578
|
+
def __init__(self, config: Mapping[str, Any]) -> None:
|
|
579
|
+
from agentinputlab.experiments.production_builders import (
|
|
580
|
+
PreparedCorpusRetrieverBuilder,
|
|
581
|
+
ProductionRetrieverBuildRequest,
|
|
582
|
+
retriever_product_sha256,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
parameters = config.get("local_retriever_builder_parameters")
|
|
586
|
+
benchmark_id = config.get("local_retriever_benchmark_id")
|
|
587
|
+
model_slot = config.get("local_retriever_model_slot")
|
|
588
|
+
seed = config.get("local_retriever_seed")
|
|
589
|
+
if not isinstance(parameters, Mapping):
|
|
590
|
+
raise DriverError(
|
|
591
|
+
"RUNTIME_MANIFEST_INVALID",
|
|
592
|
+
"local retriever builder parameters are required",
|
|
593
|
+
)
|
|
594
|
+
if not isinstance(benchmark_id, str) or not benchmark_id.strip():
|
|
595
|
+
raise DriverError("RUNTIME_MANIFEST_INVALID", "local retriever benchmark is required")
|
|
596
|
+
if not isinstance(model_slot, str) or not model_slot.strip():
|
|
597
|
+
raise DriverError("RUNTIME_MANIFEST_INVALID", "local retriever model slot is required")
|
|
598
|
+
if isinstance(seed, bool) or not isinstance(seed, int) or seed < 0:
|
|
599
|
+
raise DriverError("RUNTIME_MANIFEST_INVALID", "local retriever seed is invalid")
|
|
600
|
+
builder = PreparedCorpusRetrieverBuilder()
|
|
601
|
+
blockers = tuple(builder.preflight(parameters))
|
|
602
|
+
if blockers:
|
|
603
|
+
raise DriverError(
|
|
604
|
+
"LOCAL_RETRIEVER_PREFLIGHT_FAILED",
|
|
605
|
+
"; ".join(str(item) for item in blockers),
|
|
606
|
+
)
|
|
607
|
+
try:
|
|
608
|
+
retriever = builder.build(
|
|
609
|
+
ProductionRetrieverBuildRequest(
|
|
610
|
+
benchmark_id=benchmark_id,
|
|
611
|
+
model_slot=model_slot,
|
|
612
|
+
seed=seed,
|
|
613
|
+
builder_parameters=parameters,
|
|
614
|
+
)
|
|
615
|
+
)
|
|
616
|
+
observed = retriever_product_sha256(retriever)
|
|
617
|
+
except DriverError:
|
|
618
|
+
raise
|
|
619
|
+
except Exception as exc:
|
|
620
|
+
raise DriverError(
|
|
621
|
+
"LOCAL_RETRIEVER_BUILD_FAILED",
|
|
622
|
+
"checksum-locked local retriever could not be built",
|
|
623
|
+
) from exc
|
|
624
|
+
if observed != config.get("index_sha256"):
|
|
625
|
+
raise DriverError(
|
|
626
|
+
"RETRIEVER_IDENTITY_MISMATCH",
|
|
627
|
+
"local retriever product hash differs from the frozen index",
|
|
628
|
+
)
|
|
629
|
+
self.retriever = retriever
|
|
630
|
+
self.identity = {
|
|
631
|
+
key: config[key]
|
|
632
|
+
for key in (
|
|
633
|
+
"retriever_revision",
|
|
634
|
+
"corpus_sha256",
|
|
635
|
+
"index_sha256",
|
|
636
|
+
"retrieval_config_sha256",
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
self.builder_parameters_sha256 = _content_hash(parameters)
|
|
640
|
+
|
|
641
|
+
def fingerprint(self) -> Mapping[str, Any]:
|
|
642
|
+
return {
|
|
643
|
+
"transport": LOCAL_RETRIEVER_TRANSPORT,
|
|
644
|
+
"builder_parameters_sha256": self.builder_parameters_sha256,
|
|
645
|
+
**self.identity,
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
def search(self, query: str, *, top_n: int) -> Sequence[Mapping[str, Any]]:
|
|
649
|
+
try:
|
|
650
|
+
rows = self.retriever.retrieve(query, top_k=top_n)
|
|
651
|
+
except Exception as exc:
|
|
652
|
+
raise DriverError(
|
|
653
|
+
"RETRIEVER_REQUEST_FAILED", "local retriever failed", consumed=True
|
|
654
|
+
) from exc
|
|
655
|
+
documents = []
|
|
656
|
+
for row in rows:
|
|
657
|
+
title = row.document.title.strip()
|
|
658
|
+
text = row.document.text.strip()
|
|
659
|
+
documents.append(
|
|
660
|
+
{
|
|
661
|
+
"id": row.document.doc_id,
|
|
662
|
+
"contents": f"Title: {title}\n{text}" if title else text,
|
|
663
|
+
"score": row.score,
|
|
664
|
+
}
|
|
665
|
+
)
|
|
666
|
+
return documents
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
class _UpstreamResponse:
|
|
670
|
+
def __init__(self, value: Mapping[str, Any]) -> None:
|
|
671
|
+
self.value = value
|
|
672
|
+
|
|
673
|
+
def json(self) -> Mapping[str, Any]:
|
|
674
|
+
return self.value
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class _ModelRequestsShim:
|
|
678
|
+
def __init__(self, model: Any, gate: BudgetGate) -> None:
|
|
679
|
+
self.model = model
|
|
680
|
+
self.gate = gate
|
|
681
|
+
|
|
682
|
+
def post(self, url: str, json: Mapping[str, Any]) -> _UpstreamResponse:
|
|
683
|
+
if url != "agentinputlab://model/generate":
|
|
684
|
+
raise DriverError("UNEXPECTED_NETWORK_ROUTE", "upstream requested an unapproved URL")
|
|
685
|
+
if set(json) != {"text", "sampling_params"}:
|
|
686
|
+
raise DriverError("UPSTREAM_API_MISMATCH", "unexpected upstream model request")
|
|
687
|
+
sampling = json["sampling_params"]
|
|
688
|
+
expected = {
|
|
689
|
+
"temperature": 0.0,
|
|
690
|
+
"max_new_tokens": 8192,
|
|
691
|
+
"stop": ["</search>", "</answer>"],
|
|
692
|
+
}
|
|
693
|
+
if not isinstance(sampling, Mapping) or dict(sampling) != expected:
|
|
694
|
+
raise DriverError("UPSTREAM_API_MISMATCH", "upstream generation contract drifted")
|
|
695
|
+
prompt = _bounded_text(json["text"], "prompt")
|
|
696
|
+
admitted_input_tokens: int | None = None
|
|
697
|
+
if self.gate.caps["context_tokens"] is not None:
|
|
698
|
+
counter = getattr(self.model, "count_prompt_tokens", None)
|
|
699
|
+
if not callable(counter):
|
|
700
|
+
raise DriverError(
|
|
701
|
+
"CONTEXT_ADMISSION_REQUIRED",
|
|
702
|
+
"model adapter has no exact tokenizer admission interface",
|
|
703
|
+
)
|
|
704
|
+
try:
|
|
705
|
+
admitted_input_tokens = _nonnegative(counter(prompt), "admitted input_tokens")
|
|
706
|
+
except DriverError:
|
|
707
|
+
raise
|
|
708
|
+
except Exception as exc:
|
|
709
|
+
raise DriverError(
|
|
710
|
+
"CONTEXT_ADMISSION_FAILED",
|
|
711
|
+
"tokenizer admission failed before model execution",
|
|
712
|
+
) from exc
|
|
713
|
+
self.gate.before_model(admitted_input_tokens)
|
|
714
|
+
try:
|
|
715
|
+
turn = self.model.generate(prompt, sampling)
|
|
716
|
+
except DriverError as exc:
|
|
717
|
+
exc.consumed = True
|
|
718
|
+
raise
|
|
719
|
+
except Exception as exc:
|
|
720
|
+
raise DriverError(
|
|
721
|
+
"MODEL_REQUEST_FAILED", "model request failed", consumed=True
|
|
722
|
+
) from exc
|
|
723
|
+
if not isinstance(turn, ModelTurn):
|
|
724
|
+
raise DriverError(
|
|
725
|
+
"MODEL_RESPONSE_INVALID", "model adapter returned invalid data", consumed=True
|
|
726
|
+
)
|
|
727
|
+
self.gate.after_model(
|
|
728
|
+
turn.input_tokens,
|
|
729
|
+
turn.output_tokens,
|
|
730
|
+
admitted_input_tokens=admitted_input_tokens,
|
|
731
|
+
)
|
|
732
|
+
return _UpstreamResponse(
|
|
733
|
+
{
|
|
734
|
+
"text": turn.text,
|
|
735
|
+
"meta_info": {
|
|
736
|
+
"finish_reason": {"type": turn.finish_reason, "matched": turn.matched}
|
|
737
|
+
},
|
|
738
|
+
}
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
class SearchRecorder:
|
|
743
|
+
def __init__(self, retriever: Any, gate: BudgetGate, artifacts: Mapping[str, Any]) -> None:
|
|
744
|
+
self.retriever = retriever
|
|
745
|
+
self.gate = gate
|
|
746
|
+
self.artifacts = artifacts
|
|
747
|
+
self.trajectory: list[Mapping[str, Any]] = []
|
|
748
|
+
self.evidence: list[Mapping[str, Any]] = []
|
|
749
|
+
self._documents: dict[str, tuple[str, int]] = {}
|
|
750
|
+
|
|
751
|
+
def execute(self, tool_calls: Sequence[str]) -> list[str]:
|
|
752
|
+
if len(tool_calls) != 1:
|
|
753
|
+
raise DriverError(
|
|
754
|
+
"SEARCH_TAG_POLICY_VIOLATION",
|
|
755
|
+
"exactly one non-nested search tag is allowed per model turn",
|
|
756
|
+
consumed=bool(self.gate.usage.model_calls),
|
|
757
|
+
)
|
|
758
|
+
query = _bounded_text(tool_calls[0], "search query").strip()
|
|
759
|
+
if not query:
|
|
760
|
+
raise DriverError(
|
|
761
|
+
"SEARCH_QUERY_INVALID", "search query must not be empty", consumed=True
|
|
762
|
+
)
|
|
763
|
+
if len(self.trajectory) >= 10:
|
|
764
|
+
raise DriverError(
|
|
765
|
+
"SEARCH_BUDGET_EXHAUSTED", "search loop exceeded ten calls", consumed=True
|
|
766
|
+
)
|
|
767
|
+
if _PROTOCOL_TAG.search(query):
|
|
768
|
+
raise DriverError(
|
|
769
|
+
"SEARCH_TAG_POLICY_VIOLATION",
|
|
770
|
+
"nested protocol tags are forbidden in a search query",
|
|
771
|
+
consumed=True,
|
|
772
|
+
)
|
|
773
|
+
self.gate.before_retrieval()
|
|
774
|
+
try:
|
|
775
|
+
documents = self.retriever.search(query, top_n=5)
|
|
776
|
+
except DriverError as exc:
|
|
777
|
+
exc.consumed = True
|
|
778
|
+
raise
|
|
779
|
+
except Exception as exc:
|
|
780
|
+
raise DriverError(
|
|
781
|
+
"RETRIEVER_REQUEST_FAILED", "retriever request failed", consumed=True
|
|
782
|
+
) from exc
|
|
783
|
+
if len(documents) > 5:
|
|
784
|
+
raise DriverError(
|
|
785
|
+
"RETRIEVER_RESPONSE_INVALID",
|
|
786
|
+
"retriever returned more than five documents",
|
|
787
|
+
consumed=True,
|
|
788
|
+
)
|
|
789
|
+
clean: list[dict[str, Any]] = []
|
|
790
|
+
ids: list[str] = []
|
|
791
|
+
for item in documents:
|
|
792
|
+
if not isinstance(item, Mapping):
|
|
793
|
+
raise DriverError(
|
|
794
|
+
"RETRIEVER_RESPONSE_INVALID", "document must be an object", consumed=True
|
|
795
|
+
)
|
|
796
|
+
unknown = set(item) - {"id", "contents", "score"}
|
|
797
|
+
if unknown:
|
|
798
|
+
raise DriverError(
|
|
799
|
+
"RETRIEVER_RESPONSE_INVALID", "document contains unknown fields", consumed=True
|
|
800
|
+
)
|
|
801
|
+
document_id = _bounded_text(item.get("id"), "document id").strip()
|
|
802
|
+
contents = _bounded_text(item.get("contents"), "document contents").strip()
|
|
803
|
+
if not document_id or not contents:
|
|
804
|
+
raise DriverError(
|
|
805
|
+
"RETRIEVER_RESPONSE_INVALID",
|
|
806
|
+
"document id/text must not be empty",
|
|
807
|
+
consumed=True,
|
|
808
|
+
)
|
|
809
|
+
if _PROTOCOL_TAG.search(contents):
|
|
810
|
+
raise DriverError(
|
|
811
|
+
"DOCUMENT_PROTOCOL_INJECTION", "document contains a protocol tag", consumed=True
|
|
812
|
+
)
|
|
813
|
+
score = item.get("score")
|
|
814
|
+
if score is not None and (
|
|
815
|
+
isinstance(score, bool) or not isinstance(score, (int, float))
|
|
816
|
+
):
|
|
817
|
+
raise DriverError(
|
|
818
|
+
"RETRIEVER_RESPONSE_INVALID", "document score must be numeric", consumed=True
|
|
819
|
+
)
|
|
820
|
+
clean_item = {"id": document_id, "contents": contents, "score": score}
|
|
821
|
+
clean.append(clean_item)
|
|
822
|
+
ids.append(document_id)
|
|
823
|
+
previous = self._documents.get(document_id)
|
|
824
|
+
if previous is not None and previous[0] != contents:
|
|
825
|
+
raise DriverError(
|
|
826
|
+
"DOCUMENT_ID_COLLISION", "document ID changed content", consumed=True
|
|
827
|
+
)
|
|
828
|
+
if previous is None:
|
|
829
|
+
step = len(self.trajectory)
|
|
830
|
+
self._documents[document_id] = (contents, step)
|
|
831
|
+
self.evidence.append(
|
|
832
|
+
{
|
|
833
|
+
"document_id": document_id,
|
|
834
|
+
"text": contents,
|
|
835
|
+
"source": "smartsearch.wikipedia_search",
|
|
836
|
+
"score": score,
|
|
837
|
+
"first_seen_step": step,
|
|
838
|
+
"corpus_sha256": self.artifacts["corpus_sha256"],
|
|
839
|
+
"index_sha256": self.artifacts["index_sha256"],
|
|
840
|
+
"document_sha256": _content_hash(
|
|
841
|
+
{"document_id": document_id, "text": contents}
|
|
842
|
+
),
|
|
843
|
+
"metadata": {},
|
|
844
|
+
}
|
|
845
|
+
)
|
|
846
|
+
if len(ids) != len(set(ids)):
|
|
847
|
+
raise DriverError(
|
|
848
|
+
"RETRIEVER_RESPONSE_INVALID",
|
|
849
|
+
"retriever returned duplicate document IDs",
|
|
850
|
+
consumed=True,
|
|
851
|
+
)
|
|
852
|
+
request_record = {"query": query, "top_n": 5, "return_score": False}
|
|
853
|
+
self.trajectory.append(
|
|
854
|
+
{
|
|
855
|
+
"index": len(self.trajectory),
|
|
856
|
+
"query": query,
|
|
857
|
+
"raw_search_text": query,
|
|
858
|
+
"raw_search_hash": _sha256(query.encode("utf-8")),
|
|
859
|
+
"tag_parse_status": "ok",
|
|
860
|
+
"document_ids": ids,
|
|
861
|
+
"retrieval_request_sha256": _content_hash(request_record),
|
|
862
|
+
"retrieval_response_sha256": _content_hash(clean),
|
|
863
|
+
}
|
|
864
|
+
)
|
|
865
|
+
text = "\n\n".join(item["contents"] for item in clean).strip()
|
|
866
|
+
return [f"result: \n{text}".strip()]
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
class SmartSearchDriver:
|
|
870
|
+
def __init__(
|
|
871
|
+
self,
|
|
872
|
+
*,
|
|
873
|
+
checkout: str | Path | None = None,
|
|
874
|
+
manifest_path: str | Path = DEFAULT_MANIFEST,
|
|
875
|
+
runtime_config: Mapping[str, Any] | None = None,
|
|
876
|
+
model_factory: Callable[[Mapping[str, Any]], Any] | None = None,
|
|
877
|
+
retriever_factory: Callable[[Mapping[str, Any]], Any] | None = None,
|
|
878
|
+
environ: Mapping[str, str] | None = None,
|
|
879
|
+
) -> None:
|
|
880
|
+
self.environ = dict(os.environ if environ is None else environ)
|
|
881
|
+
if self.environ.get(SANDBOX_ENV):
|
|
882
|
+
raise DriverError(
|
|
883
|
+
"SANDBOX_FORBIDDEN", "the upstream code-execution sandbox is forbidden"
|
|
884
|
+
)
|
|
885
|
+
self.checkout = Path(checkout or self.environ.get(CHECKOUT_ENV, "")).expanduser()
|
|
886
|
+
if not str(self.checkout):
|
|
887
|
+
raise DriverError("CHECKOUT_REQUIRED", f"{CHECKOUT_ENV} is required")
|
|
888
|
+
self.manifest_path = Path(manifest_path)
|
|
889
|
+
self.manifest_bytes = self.manifest_path.read_bytes()
|
|
890
|
+
self.manifest_sha256 = _sha256(self.manifest_bytes)
|
|
891
|
+
self.manifest = _json_object(self.manifest_bytes, "compatibility manifest")
|
|
892
|
+
self.runtime = dict(runtime_config or self._runtime_from_environment())
|
|
893
|
+
self.source_attestation = self._verify_source()
|
|
894
|
+
self._verify_runtime_manifest()
|
|
895
|
+
self.recall_class, self.answer_parser = self._load_upstream_surface()
|
|
896
|
+
if model_factory is not None:
|
|
897
|
+
self.model_factory = model_factory
|
|
898
|
+
elif self.runtime.get("model_transport") == LOCAL_MODEL_TRANSPORT:
|
|
899
|
+
self.model_factory = self._local_model
|
|
900
|
+
else:
|
|
901
|
+
self.model_factory = self._http_model
|
|
902
|
+
if retriever_factory is not None:
|
|
903
|
+
self.retriever_factory = retriever_factory
|
|
904
|
+
elif self.runtime.get("retriever_transport") == LOCAL_RETRIEVER_TRANSPORT:
|
|
905
|
+
self.retriever_factory = self._local_retriever
|
|
906
|
+
else:
|
|
907
|
+
self.retriever_factory = self._http_retriever
|
|
908
|
+
self.last_usage: Usage | None = None
|
|
909
|
+
|
|
910
|
+
def _runtime_from_environment(self) -> Mapping[str, Any]:
|
|
911
|
+
path = self.environ.get(RUNTIME_MANIFEST_ENV, "")
|
|
912
|
+
if not path:
|
|
913
|
+
raise DriverError("RUNTIME_MANIFEST_REQUIRED", f"{RUNTIME_MANIFEST_ENV} is required")
|
|
914
|
+
return _json_object(Path(path).read_bytes(), "runtime manifest")
|
|
915
|
+
|
|
916
|
+
def _verify_source(self) -> str:
|
|
917
|
+
if self.manifest.get("schema_version") != MANIFEST_SCHEMA:
|
|
918
|
+
raise DriverError("MANIFEST_INVALID", "compatibility manifest schema mismatch")
|
|
919
|
+
upstream = self.manifest.get("upstream")
|
|
920
|
+
source_files = self.manifest.get("source_files")
|
|
921
|
+
if not isinstance(upstream, Mapping) or not isinstance(source_files, Mapping):
|
|
922
|
+
raise DriverError("MANIFEST_INVALID", "compatibility manifest is incomplete")
|
|
923
|
+
if upstream.get("commit") != UPSTREAM_COMMIT:
|
|
924
|
+
raise DriverError("MANIFEST_INVALID", "compatibility manifest commit mismatch")
|
|
925
|
+
checks = {"LICENSE": upstream.get("license_sha256"), **dict(source_files)}
|
|
926
|
+
for relative, expected in checks.items():
|
|
927
|
+
path = self.checkout / relative
|
|
928
|
+
if not path.is_file() or _sha256(path.read_bytes()) != expected:
|
|
929
|
+
raise DriverError("SOURCE_HASH_MISMATCH", f"pinned source mismatch: {relative}")
|
|
930
|
+
if (self.checkout / ".git").exists():
|
|
931
|
+
git_environment = {
|
|
932
|
+
"PATH": os.defpath,
|
|
933
|
+
"LANG": "C",
|
|
934
|
+
"LC_ALL": "C",
|
|
935
|
+
"GIT_CONFIG_NOSYSTEM": "1",
|
|
936
|
+
"GIT_CONFIG_GLOBAL": os.devnull,
|
|
937
|
+
"GIT_OPTIONAL_LOCKS": "0",
|
|
938
|
+
"GIT_TERMINAL_PROMPT": "0",
|
|
939
|
+
}
|
|
940
|
+
try:
|
|
941
|
+
head = subprocess.run(
|
|
942
|
+
[
|
|
943
|
+
"git",
|
|
944
|
+
"-c",
|
|
945
|
+
"core.fsmonitor=false",
|
|
946
|
+
"-c",
|
|
947
|
+
f"core.hooksPath={os.devnull}",
|
|
948
|
+
"rev-parse",
|
|
949
|
+
"HEAD",
|
|
950
|
+
],
|
|
951
|
+
cwd=self.checkout,
|
|
952
|
+
check=True,
|
|
953
|
+
capture_output=True,
|
|
954
|
+
text=True,
|
|
955
|
+
timeout=10,
|
|
956
|
+
env=git_environment,
|
|
957
|
+
).stdout.strip()
|
|
958
|
+
top_level = subprocess.run(
|
|
959
|
+
[
|
|
960
|
+
"git",
|
|
961
|
+
"-c",
|
|
962
|
+
"core.fsmonitor=false",
|
|
963
|
+
"-c",
|
|
964
|
+
f"core.hooksPath={os.devnull}",
|
|
965
|
+
"rev-parse",
|
|
966
|
+
"--show-toplevel",
|
|
967
|
+
],
|
|
968
|
+
cwd=self.checkout,
|
|
969
|
+
check=True,
|
|
970
|
+
capture_output=True,
|
|
971
|
+
text=True,
|
|
972
|
+
timeout=10,
|
|
973
|
+
env=git_environment,
|
|
974
|
+
).stdout.strip()
|
|
975
|
+
dirty = subprocess.run(
|
|
976
|
+
[
|
|
977
|
+
"git",
|
|
978
|
+
"-c",
|
|
979
|
+
"core.fsmonitor=false",
|
|
980
|
+
"-c",
|
|
981
|
+
f"core.hooksPath={os.devnull}",
|
|
982
|
+
"status",
|
|
983
|
+
"--porcelain",
|
|
984
|
+
"--untracked-files=all",
|
|
985
|
+
],
|
|
986
|
+
cwd=self.checkout,
|
|
987
|
+
check=True,
|
|
988
|
+
capture_output=True,
|
|
989
|
+
text=True,
|
|
990
|
+
timeout=10,
|
|
991
|
+
env=git_environment,
|
|
992
|
+
).stdout.strip()
|
|
993
|
+
except Exception as exc:
|
|
994
|
+
raise DriverError("GIT_ATTESTATION_FAILED", "could not attest checkout") from exc
|
|
995
|
+
if (
|
|
996
|
+
head != UPSTREAM_COMMIT
|
|
997
|
+
or Path(top_level).resolve() != self.checkout.resolve()
|
|
998
|
+
or dirty
|
|
999
|
+
):
|
|
1000
|
+
raise DriverError("GIT_ATTESTATION_FAILED", "checkout is not pinned and clean")
|
|
1001
|
+
return "git_checkout"
|
|
1002
|
+
archive_path = self.runtime.get("source_archive_path") or self.environ.get(
|
|
1003
|
+
SOURCE_ARCHIVE_ENV
|
|
1004
|
+
)
|
|
1005
|
+
snapshot_commit = self.runtime.get("snapshot_commit") or self.environ.get(
|
|
1006
|
+
SNAPSHOT_COMMIT_ENV
|
|
1007
|
+
)
|
|
1008
|
+
if snapshot_commit != UPSTREAM_COMMIT or not isinstance(archive_path, str):
|
|
1009
|
+
raise DriverError(
|
|
1010
|
+
"SOURCE_ATTESTATION_REQUIRED", "snapshot archive attestation is required"
|
|
1011
|
+
)
|
|
1012
|
+
archive = Path(archive_path)
|
|
1013
|
+
if not archive.is_file() or _sha256(archive.read_bytes()) != upstream.get("archive_sha256"):
|
|
1014
|
+
raise DriverError("SOURCE_ARCHIVE_MISMATCH", "source archive hash mismatch")
|
|
1015
|
+
return "verified_archive_snapshot"
|
|
1016
|
+
|
|
1017
|
+
def _verify_runtime_manifest(self) -> None:
|
|
1018
|
+
artifacts = self.runtime.get("artifacts")
|
|
1019
|
+
if not isinstance(artifacts, Mapping):
|
|
1020
|
+
raise DriverError("RUNTIME_MANIFEST_INVALID", "runtime artifacts are required")
|
|
1021
|
+
if set(artifacts) != _ARTIFACT_FIELDS:
|
|
1022
|
+
raise DriverError("RUNTIME_MANIFEST_INVALID", "runtime artifact fields are not exact")
|
|
1023
|
+
_validate_artifact_digests(artifacts)
|
|
1024
|
+
if artifacts.get("compatibility_manifest_sha256") != self.manifest_sha256:
|
|
1025
|
+
raise DriverError("RUNTIME_MANIFEST_INVALID", "compatibility manifest hash mismatch")
|
|
1026
|
+
if (
|
|
1027
|
+
artifacts.get("checkpoint_repository") != CHECKPOINT_REPOSITORY
|
|
1028
|
+
or artifacts.get("checkpoint_revision") != CHECKPOINT_REVISION
|
|
1029
|
+
):
|
|
1030
|
+
raise DriverError("RUNTIME_MANIFEST_INVALID", "released checkpoint identity mismatch")
|
|
1031
|
+
snapshot_path = self.runtime.get("model_snapshot_manifest_path")
|
|
1032
|
+
if not isinstance(snapshot_path, str) or not Path(snapshot_path).is_file():
|
|
1033
|
+
raise DriverError(
|
|
1034
|
+
"MODEL_SNAPSHOT_MANIFEST_REQUIRED", "model snapshot manifest is required"
|
|
1035
|
+
)
|
|
1036
|
+
raw = Path(snapshot_path).read_bytes()
|
|
1037
|
+
if _sha256(raw) != artifacts.get("model_snapshot_manifest_sha256"):
|
|
1038
|
+
raise DriverError("MODEL_SNAPSHOT_MISMATCH", "model snapshot manifest hash mismatch")
|
|
1039
|
+
snapshot = _json_object(raw, "model snapshot manifest")
|
|
1040
|
+
if (
|
|
1041
|
+
snapshot.get("repository") != CHECKPOINT_REPOSITORY
|
|
1042
|
+
or snapshot.get("revision") != CHECKPOINT_REVISION
|
|
1043
|
+
):
|
|
1044
|
+
raise DriverError("MODEL_SNAPSHOT_MISMATCH", "model snapshot revision mismatch")
|
|
1045
|
+
if snapshot.get("tokenizer_revision") != artifacts.get("tokenizer_revision"):
|
|
1046
|
+
raise DriverError("MODEL_SNAPSHOT_MISMATCH", "tokenizer revision mismatch")
|
|
1047
|
+
files = snapshot.get("files")
|
|
1048
|
+
if not isinstance(files, Mapping) or not files:
|
|
1049
|
+
raise DriverError("MODEL_SNAPSHOT_MISMATCH", "model snapshot file manifest is empty")
|
|
1050
|
+
_validate_snapshot_files(files)
|
|
1051
|
+
snapshot_tree_sha256 = _content_hash(files)
|
|
1052
|
+
if (
|
|
1053
|
+
snapshot.get("snapshot_tree_sha256") != snapshot_tree_sha256
|
|
1054
|
+
or artifacts.get("model_snapshot_tree_sha256") != snapshot_tree_sha256
|
|
1055
|
+
):
|
|
1056
|
+
raise DriverError("MODEL_SNAPSHOT_MISMATCH", "model snapshot tree mismatch")
|
|
1057
|
+
|
|
1058
|
+
contracts = self.manifest.get("contracts")
|
|
1059
|
+
if not isinstance(contracts, Mapping) or set(contracts) != _CONTRACT_FIELDS:
|
|
1060
|
+
raise DriverError("MANIFEST_INVALID", "compatibility contracts are not exact")
|
|
1061
|
+
for key in (
|
|
1062
|
+
"tool_schema_sha256",
|
|
1063
|
+
"loop_contract_sha256",
|
|
1064
|
+
"parser_contract_sha256",
|
|
1065
|
+
):
|
|
1066
|
+
if contracts.get(key) != artifacts.get(key):
|
|
1067
|
+
raise DriverError("CONTRACT_MISMATCH", f"{key} disagrees with artifacts")
|
|
1068
|
+
fixed_contracts = {
|
|
1069
|
+
"tool_schema_sha256": TOOL_SCHEMA_SHA256,
|
|
1070
|
+
"loop_contract_sha256": LOOP_CONTRACT_SHA256,
|
|
1071
|
+
"parser_contract_sha256": PARSER_CONTRACT_SHA256,
|
|
1072
|
+
}
|
|
1073
|
+
if any(artifacts.get(key) != expected for key, expected in fixed_contracts.items()):
|
|
1074
|
+
raise DriverError("CONTRACT_MISMATCH", "frozen contract digest drifted")
|
|
1075
|
+
if contracts.get("profile_id") != PROFILE_ID:
|
|
1076
|
+
raise DriverError("CONTRACT_MISMATCH", "contract profile mismatch")
|
|
1077
|
+
expected_loop = {
|
|
1078
|
+
"max_rounds": contracts.get("max_rounds"),
|
|
1079
|
+
"max_results_per_search": contracts.get("top_n"),
|
|
1080
|
+
"max_new_tokens": contracts.get("max_new_tokens"),
|
|
1081
|
+
"temperature": contracts.get("temperature"),
|
|
1082
|
+
"stop": contracts.get("stop"),
|
|
1083
|
+
"retry_attempts": contracts.get("outer_retry_attempts"),
|
|
1084
|
+
"search_tags_per_turn": contracts.get("search_tags_per_turn"),
|
|
1085
|
+
}
|
|
1086
|
+
if _content_hash(expected_loop) != artifacts.get("loop_contract_sha256"):
|
|
1087
|
+
raise DriverError("CONTRACT_MISMATCH", "loop contract hash mismatch")
|
|
1088
|
+
tool_schema = self.runtime.get("tool_schema")
|
|
1089
|
+
if (
|
|
1090
|
+
isinstance(tool_schema, (str, bytes))
|
|
1091
|
+
or not isinstance(tool_schema, Sequence)
|
|
1092
|
+
or _content_hash(tool_schema) != artifacts.get("tool_schema_sha256")
|
|
1093
|
+
):
|
|
1094
|
+
raise DriverError("CONTRACT_MISMATCH", "runtime tool schema hash mismatch")
|
|
1095
|
+
for key in (
|
|
1096
|
+
"model_transport",
|
|
1097
|
+
"served_model_name",
|
|
1098
|
+
"model_snapshot_tree_sha256",
|
|
1099
|
+
"tokenizer_revision",
|
|
1100
|
+
"context_admission_fingerprint_sha256",
|
|
1101
|
+
"retriever_revision",
|
|
1102
|
+
"corpus_sha256",
|
|
1103
|
+
"index_sha256",
|
|
1104
|
+
"retrieval_config_sha256",
|
|
1105
|
+
):
|
|
1106
|
+
if self.runtime.get(key) != artifacts.get(key):
|
|
1107
|
+
raise DriverError(
|
|
1108
|
+
"RUNTIME_MANIFEST_INVALID", f"runtime/artifact identity mismatch: {key}"
|
|
1109
|
+
)
|
|
1110
|
+
if self.runtime.get("retriever_transport") == LOCAL_RETRIEVER_TRANSPORT:
|
|
1111
|
+
required = {
|
|
1112
|
+
"local_retriever_builder_parameters",
|
|
1113
|
+
"local_retriever_benchmark_id",
|
|
1114
|
+
"local_retriever_model_slot",
|
|
1115
|
+
"local_retriever_seed",
|
|
1116
|
+
}
|
|
1117
|
+
if any(key not in self.runtime for key in required):
|
|
1118
|
+
raise DriverError(
|
|
1119
|
+
"RUNTIME_MANIFEST_INVALID",
|
|
1120
|
+
"local retriever runtime binding is incomplete",
|
|
1121
|
+
)
|
|
1122
|
+
if self.runtime.get("model_transport") == LOCAL_MODEL_TRANSPORT:
|
|
1123
|
+
model_root_value = self.runtime.get("local_model_root")
|
|
1124
|
+
if not isinstance(model_root_value, str) or not model_root_value.strip():
|
|
1125
|
+
raise DriverError(
|
|
1126
|
+
"RUNTIME_MANIFEST_INVALID", "local SmartSearch model root is required"
|
|
1127
|
+
)
|
|
1128
|
+
model_root = Path(model_root_value).expanduser().resolve()
|
|
1129
|
+
if not model_root.is_dir() or model_root.is_symlink():
|
|
1130
|
+
raise DriverError(
|
|
1131
|
+
"MODEL_SNAPSHOT_MISMATCH", "local SmartSearch model root is invalid"
|
|
1132
|
+
)
|
|
1133
|
+
for relative, identity in files.items():
|
|
1134
|
+
file_path = (model_root / relative).resolve()
|
|
1135
|
+
try:
|
|
1136
|
+
file_path.relative_to(model_root)
|
|
1137
|
+
except ValueError as exc:
|
|
1138
|
+
raise DriverError(
|
|
1139
|
+
"MODEL_SNAPSHOT_MISMATCH", "snapshot file escapes model root"
|
|
1140
|
+
) from exc
|
|
1141
|
+
if (
|
|
1142
|
+
not file_path.is_file()
|
|
1143
|
+
or file_path.is_symlink()
|
|
1144
|
+
or file_path.stat().st_size != identity.get("bytes")
|
|
1145
|
+
or _sha256_file(file_path) != identity.get("sha256")
|
|
1146
|
+
):
|
|
1147
|
+
raise DriverError(
|
|
1148
|
+
"MODEL_SNAPSHOT_MISMATCH",
|
|
1149
|
+
f"local model file checksum mismatch: {relative}",
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
def _load_upstream_surface(self) -> tuple[type[Any], Callable[[str], str]]:
|
|
1153
|
+
recall_path = self.checkout / "src/re_call/inference/re_call.py"
|
|
1154
|
+
module = types.ModuleType(f"_agentinputlab_smartsearch_{self.manifest_sha256[:12]}")
|
|
1155
|
+
module.__file__ = str(recall_path)
|
|
1156
|
+
requests_placeholder = _UnboundRequestsModule()
|
|
1157
|
+
default_import = builtins.__import__
|
|
1158
|
+
|
|
1159
|
+
def controlled_import(name, globals=None, locals=None, fromlist=(), level=0):
|
|
1160
|
+
if level == 0 and name == "requests":
|
|
1161
|
+
if fromlist:
|
|
1162
|
+
raise DriverError(
|
|
1163
|
+
"UPSTREAM_API_MISMATCH",
|
|
1164
|
+
"pinned SmartSearch changed its requests import contract",
|
|
1165
|
+
)
|
|
1166
|
+
return requests_placeholder
|
|
1167
|
+
return default_import(name, globals, locals, fromlist, level)
|
|
1168
|
+
|
|
1169
|
+
module.__dict__["__builtins__"] = {
|
|
1170
|
+
**vars(builtins),
|
|
1171
|
+
"__import__": controlled_import,
|
|
1172
|
+
}
|
|
1173
|
+
old_dont_write = sys.dont_write_bytecode
|
|
1174
|
+
old_prefix = sys.pycache_prefix
|
|
1175
|
+
try:
|
|
1176
|
+
sys.dont_write_bytecode = True
|
|
1177
|
+
sys.pycache_prefix = tempfile.mkdtemp(prefix="agentinputlab-smartsearch-pycache-")
|
|
1178
|
+
exec(compile(recall_path.read_bytes(), str(recall_path), "exec"), module.__dict__)
|
|
1179
|
+
finally:
|
|
1180
|
+
sys.dont_write_bytecode = old_dont_write
|
|
1181
|
+
sys.pycache_prefix = old_prefix
|
|
1182
|
+
if module.__dict__.get("requests") is not requests_placeholder:
|
|
1183
|
+
raise DriverError(
|
|
1184
|
+
"UPSTREAM_API_MISMATCH",
|
|
1185
|
+
"pinned SmartSearch did not bind the controlled requests import seam",
|
|
1186
|
+
)
|
|
1187
|
+
recall = module.__dict__.get("ReCall")
|
|
1188
|
+
if not isinstance(recall, type) or not hasattr(recall.run, "__wrapped__"):
|
|
1189
|
+
raise DriverError("UPSTREAM_API_MISMATCH", "pinned ReCall API is unavailable")
|
|
1190
|
+
active_path = self.checkout / "src/flashrag/pipeline/active_pipeline.py"
|
|
1191
|
+
helper_path = self.checkout / "src/verl/utils/reward_score/re_call.py"
|
|
1192
|
+
extractor = _load_ast_function(
|
|
1193
|
+
active_path, "extract_last_assistant_response", class_name="ReCallPipeline"
|
|
1194
|
+
)
|
|
1195
|
+
last_boxed = _load_ast_function(helper_path, "last_boxed_only_string")
|
|
1196
|
+
remove_boxed = _load_ast_function(helper_path, "remove_boxed")
|
|
1197
|
+
|
|
1198
|
+
def parse_answer(transcript: str) -> str:
|
|
1199
|
+
part = extractor(object(), transcript)
|
|
1200
|
+
if not isinstance(part, str) or not part:
|
|
1201
|
+
return ""
|
|
1202
|
+
try:
|
|
1203
|
+
boxed = last_boxed(part)
|
|
1204
|
+
if boxed is None:
|
|
1205
|
+
return ""
|
|
1206
|
+
answer = remove_boxed(boxed)
|
|
1207
|
+
except (AssertionError, TypeError):
|
|
1208
|
+
return ""
|
|
1209
|
+
return answer if isinstance(answer, str) else ""
|
|
1210
|
+
|
|
1211
|
+
self._upstream_module = module
|
|
1212
|
+
return recall, parse_answer
|
|
1213
|
+
|
|
1214
|
+
def _http_model(self, config: Mapping[str, Any]) -> HTTPModelTransport:
|
|
1215
|
+
merged = {**dict(config), **dict(self.runtime["artifacts"])}
|
|
1216
|
+
return HTTPModelTransport(merged, api_key=self.environ.get(MODEL_API_KEY_ENV, ""))
|
|
1217
|
+
|
|
1218
|
+
def _http_retriever(self, config: Mapping[str, Any]) -> HTTPRetriever:
|
|
1219
|
+
merged = {**dict(config), **dict(self.runtime["artifacts"])}
|
|
1220
|
+
return HTTPRetriever(merged, api_key=self.environ.get(RETRIEVER_API_KEY_ENV, ""))
|
|
1221
|
+
|
|
1222
|
+
def _local_model(self, config: Mapping[str, Any]) -> LocalSnapshotOpenAIModel:
|
|
1223
|
+
merged = {**dict(config), **dict(self.runtime["artifacts"])}
|
|
1224
|
+
return LocalSnapshotOpenAIModel(
|
|
1225
|
+
merged,
|
|
1226
|
+
api_key=self.environ.get(MODEL_API_KEY_ENV, ""),
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
def _local_retriever(self, config: Mapping[str, Any]) -> LocalPreparedRetriever:
|
|
1230
|
+
merged = {**dict(config), **dict(self.runtime["artifacts"])}
|
|
1231
|
+
return LocalPreparedRetriever(merged)
|
|
1232
|
+
|
|
1233
|
+
def run(self, request: Any) -> Mapping[str, Any]:
|
|
1234
|
+
self.last_usage = None
|
|
1235
|
+
parsed = _validate_request(request)
|
|
1236
|
+
artifacts = parsed["payload"]["artifacts"]
|
|
1237
|
+
if _content_hash(artifacts) != parsed["dependency_fingerprints"].get("artifacts_sha256"):
|
|
1238
|
+
raise DriverError("ARTIFACT_IDENTITY_MISMATCH", "artifact dependency hash mismatch")
|
|
1239
|
+
if _content_hash(artifacts) != _content_hash(self.runtime["artifacts"]):
|
|
1240
|
+
raise DriverError("ARTIFACT_IDENTITY_MISMATCH", "request/runtime artifact mismatch")
|
|
1241
|
+
model = self.model_factory(self.runtime)
|
|
1242
|
+
retriever = self.retriever_factory(self.runtime)
|
|
1243
|
+
if _content_hash(model.fingerprint()) != artifacts.get(
|
|
1244
|
+
"model_deployment_fingerprint_sha256"
|
|
1245
|
+
):
|
|
1246
|
+
raise DriverError("MODEL_IDENTITY_MISMATCH", "model deployment fingerprint mismatch")
|
|
1247
|
+
if _content_hash(retriever.fingerprint()) != artifacts.get(
|
|
1248
|
+
"retriever_deployment_fingerprint_sha256"
|
|
1249
|
+
):
|
|
1250
|
+
raise DriverError(
|
|
1251
|
+
"RETRIEVER_IDENTITY_MISMATCH", "retriever deployment fingerprint mismatch"
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
gate = BudgetGate(parsed["payload"]["remaining_budget"])
|
|
1255
|
+
self.last_usage = gate.usage
|
|
1256
|
+
recorder = SearchRecorder(retriever, gate, artifacts)
|
|
1257
|
+
upstream_module = self._upstream_module
|
|
1258
|
+
upstream_module.requests = _ModelRequestsShim(model, gate)
|
|
1259
|
+
|
|
1260
|
+
base = self.recall_class
|
|
1261
|
+
|
|
1262
|
+
class SafeReCall(base):
|
|
1263
|
+
def execute_tool_calls(self, env, tool_calls):
|
|
1264
|
+
if env != "agentinputlab:no-exec":
|
|
1265
|
+
raise DriverError("SANDBOX_FORBIDDEN", "dynamic tool environment is forbidden")
|
|
1266
|
+
return recorder.execute(tool_calls)
|
|
1267
|
+
|
|
1268
|
+
agent = SafeReCall("agentinputlab://model", "agentinputlab://disabled-executor")
|
|
1269
|
+
try:
|
|
1270
|
+
transcript = base.run.__wrapped__(
|
|
1271
|
+
agent,
|
|
1272
|
+
"agentinputlab:no-exec",
|
|
1273
|
+
_canonical_json(self.runtime.get("tool_schema", [])),
|
|
1274
|
+
parsed["payload"]["question"],
|
|
1275
|
+
)
|
|
1276
|
+
except DriverError:
|
|
1277
|
+
raise
|
|
1278
|
+
except Exception as exc:
|
|
1279
|
+
raise DriverError(
|
|
1280
|
+
"UPSTREAM_EXECUTION_FAILED",
|
|
1281
|
+
"pinned SmartSearch inference failed",
|
|
1282
|
+
consumed=bool(gate.usage.model_calls or gate.usage.retrieval_calls),
|
|
1283
|
+
) from exc
|
|
1284
|
+
answer = self.answer_parser(transcript)
|
|
1285
|
+
if not answer.strip():
|
|
1286
|
+
raise DriverError(
|
|
1287
|
+
"NO_FINAL_ANSWER", "official parser found no boxed final answer", consumed=True
|
|
1288
|
+
)
|
|
1289
|
+
termination = "direct_answer" if not recorder.trajectory else "answer"
|
|
1290
|
+
if gate.usage.model_calls == 10 and len(recorder.trajectory) == 10:
|
|
1291
|
+
termination = "max_search_rounds"
|
|
1292
|
+
payload = {
|
|
1293
|
+
"profile_id": PROFILE_ID,
|
|
1294
|
+
"artifact_identity_sha256": _content_hash(artifacts),
|
|
1295
|
+
"artifacts": artifacts,
|
|
1296
|
+
"gold_answer_used": False,
|
|
1297
|
+
"test_time_reward_used": False,
|
|
1298
|
+
"external_query_refiner_used": False,
|
|
1299
|
+
"training_code_loaded": False,
|
|
1300
|
+
"trajectory": recorder.trajectory,
|
|
1301
|
+
"evidence": recorder.evidence,
|
|
1302
|
+
"final_answer": answer,
|
|
1303
|
+
"termination": termination,
|
|
1304
|
+
"raw_transcript_sha256": _sha256(transcript.encode("utf-8")),
|
|
1305
|
+
"runtime_attestation": {
|
|
1306
|
+
"source_commit": UPSTREAM_COMMIT,
|
|
1307
|
+
"source_attestation": self.source_attestation,
|
|
1308
|
+
"compatibility_manifest_sha256": self.manifest_sha256,
|
|
1309
|
+
"model_transport": artifacts["model_transport"],
|
|
1310
|
+
"model_snapshot_tree_sha256": artifacts["model_snapshot_tree_sha256"],
|
|
1311
|
+
"context_admission_fingerprint_sha256": artifacts[
|
|
1312
|
+
"context_admission_fingerprint_sha256"
|
|
1313
|
+
],
|
|
1314
|
+
"contract_hashes_verified": True,
|
|
1315
|
+
"immutable_model_identity_handshake_required": True,
|
|
1316
|
+
"context_admission_before_generation": True,
|
|
1317
|
+
"official_loop_called": True,
|
|
1318
|
+
"outer_retry_disabled": True,
|
|
1319
|
+
"dangerous_sandbox_used": False,
|
|
1320
|
+
"training_code_loaded": False,
|
|
1321
|
+
"seed_applied": False,
|
|
1322
|
+
},
|
|
1323
|
+
}
|
|
1324
|
+
return {
|
|
1325
|
+
"status": "ok",
|
|
1326
|
+
"request_id": parsed["request_id"],
|
|
1327
|
+
"upstream_commit": UPSTREAM_COMMIT,
|
|
1328
|
+
"protocol_version": PROTOCOL,
|
|
1329
|
+
"payload": payload,
|
|
1330
|
+
"usage": gate.usage.as_mapping(),
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def _validate_request(value: Any) -> Mapping[str, Any]:
|
|
1335
|
+
if not isinstance(value, Mapping) or set(value) != _TOP_FIELDS:
|
|
1336
|
+
raise DriverError("REQUEST_SCHEMA_INVALID", "request envelope fields are invalid")
|
|
1337
|
+
_reject_forbidden(value)
|
|
1338
|
+
if value.get("method_id") != "recipe.smartsearch" or value.get("release_id") != RELEASE:
|
|
1339
|
+
raise DriverError("REQUEST_IDENTITY_MISMATCH", "method/release mismatch")
|
|
1340
|
+
if value.get("upstream_commit") != UPSTREAM_COMMIT or value.get("protocol_version") != PROTOCOL:
|
|
1341
|
+
raise DriverError("REQUEST_IDENTITY_MISMATCH", "commit/protocol mismatch")
|
|
1342
|
+
identity = {key: value[key] for key in _TOP_FIELDS if key != "request_id"}
|
|
1343
|
+
if value.get("request_id") != _content_hash(identity):
|
|
1344
|
+
raise DriverError("REQUEST_IDENTITY_MISMATCH", "request_id mismatch")
|
|
1345
|
+
if value.get("minimum_reservation") != {"model_calls": 1}:
|
|
1346
|
+
raise DriverError("REQUEST_SCHEMA_INVALID", "minimum reservation must be one model call")
|
|
1347
|
+
payload = value.get("payload")
|
|
1348
|
+
if not isinstance(payload, Mapping) or set(payload) != _PAYLOAD_FIELDS:
|
|
1349
|
+
raise DriverError("REQUEST_SCHEMA_INVALID", "payload fields are invalid")
|
|
1350
|
+
if payload.get("profile_id") != PROFILE_ID:
|
|
1351
|
+
raise DriverError("REQUEST_IDENTITY_MISMATCH", "profile mismatch")
|
|
1352
|
+
_bounded_text(payload.get("question"), "question")
|
|
1353
|
+
if not isinstance(payload.get("task_id"), str):
|
|
1354
|
+
raise DriverError("REQUEST_SCHEMA_INVALID", "task_id must be text")
|
|
1355
|
+
seed = payload.get("seed")
|
|
1356
|
+
if isinstance(seed, bool) or not isinstance(seed, int) or seed < 0:
|
|
1357
|
+
raise DriverError("REQUEST_SCHEMA_INVALID", "seed must be non-negative")
|
|
1358
|
+
if payload.get("controls") != {
|
|
1359
|
+
"max_search_rounds": 10,
|
|
1360
|
+
"max_results_per_round": 5,
|
|
1361
|
+
"max_evidence_documents": 50,
|
|
1362
|
+
"max_output_tokens_per_call": 8192,
|
|
1363
|
+
}:
|
|
1364
|
+
raise DriverError("REQUEST_SCHEMA_INVALID", "SmartSearch controls mismatch")
|
|
1365
|
+
expected_contract = {
|
|
1366
|
+
"mode": "frozen_inference",
|
|
1367
|
+
"allow_training": False,
|
|
1368
|
+
"allow_gold_answer": False,
|
|
1369
|
+
"allow_test_time_reward": False,
|
|
1370
|
+
"allow_external_query_refiner": False,
|
|
1371
|
+
}
|
|
1372
|
+
if payload.get("inference_contract") != expected_contract:
|
|
1373
|
+
raise DriverError("REQUEST_SCHEMA_INVALID", "frozen inference contract mismatch")
|
|
1374
|
+
budget = payload.get("remaining_budget")
|
|
1375
|
+
if not isinstance(budget, Mapping) or set(budget) != {
|
|
1376
|
+
"model_calls",
|
|
1377
|
+
"retrieval_calls",
|
|
1378
|
+
"output_tokens",
|
|
1379
|
+
"context_tokens",
|
|
1380
|
+
}:
|
|
1381
|
+
raise DriverError("REQUEST_SCHEMA_INVALID", "remaining budget fields are invalid")
|
|
1382
|
+
for key in ("model_calls", "retrieval_calls", "output_tokens"):
|
|
1383
|
+
_nonnegative(budget.get(key), key)
|
|
1384
|
+
if budget.get("context_tokens") is not None:
|
|
1385
|
+
_nonnegative(budget.get("context_tokens"), "context_tokens")
|
|
1386
|
+
artifacts = payload.get("artifacts")
|
|
1387
|
+
if not isinstance(artifacts, Mapping) or set(artifacts) != _ARTIFACT_FIELDS:
|
|
1388
|
+
raise DriverError("REQUEST_SCHEMA_INVALID", "artifact fields are not exact")
|
|
1389
|
+
_validate_artifact_digests(artifacts)
|
|
1390
|
+
if not isinstance(value.get("dependency_fingerprints"), Mapping):
|
|
1391
|
+
raise DriverError("REQUEST_SCHEMA_INVALID", "dependency fingerprints must be an object")
|
|
1392
|
+
return value
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
def _load_ast_function(
|
|
1396
|
+
path: Path, function_name: str, *, class_name: str = ""
|
|
1397
|
+
) -> Callable[..., Any]:
|
|
1398
|
+
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
1399
|
+
candidates: Sequence[ast.stmt] = tree.body
|
|
1400
|
+
if class_name:
|
|
1401
|
+
owner = next(
|
|
1402
|
+
(
|
|
1403
|
+
node
|
|
1404
|
+
for node in tree.body
|
|
1405
|
+
if isinstance(node, ast.ClassDef) and node.name == class_name
|
|
1406
|
+
),
|
|
1407
|
+
None,
|
|
1408
|
+
)
|
|
1409
|
+
if owner is None:
|
|
1410
|
+
raise DriverError("UPSTREAM_API_MISMATCH", f"missing upstream class {class_name}")
|
|
1411
|
+
candidates = owner.body
|
|
1412
|
+
node = next(
|
|
1413
|
+
(
|
|
1414
|
+
node
|
|
1415
|
+
for node in candidates
|
|
1416
|
+
if isinstance(node, ast.FunctionDef) and node.name == function_name
|
|
1417
|
+
),
|
|
1418
|
+
None,
|
|
1419
|
+
)
|
|
1420
|
+
if node is None:
|
|
1421
|
+
raise DriverError("UPSTREAM_API_MISMATCH", f"missing upstream parser {function_name}")
|
|
1422
|
+
isolated = ast.Module(body=[node], type_ignores=[])
|
|
1423
|
+
ast.fix_missing_locations(isolated)
|
|
1424
|
+
namespace: dict[str, Any] = {"re": re}
|
|
1425
|
+
exec(compile(isolated, str(path), "exec"), namespace)
|
|
1426
|
+
return namespace[function_name]
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
def _reject_forbidden(value: Any) -> None:
|
|
1430
|
+
if isinstance(value, Mapping):
|
|
1431
|
+
for key, item in value.items():
|
|
1432
|
+
normalized = str(key).strip().lower().replace("-", "_")
|
|
1433
|
+
if normalized in _FORBIDDEN_KEYS:
|
|
1434
|
+
raise DriverError("FORBIDDEN_FIELD", f"forbidden request field: {key}")
|
|
1435
|
+
_reject_forbidden(item)
|
|
1436
|
+
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
|
1437
|
+
for item in value:
|
|
1438
|
+
_reject_forbidden(item)
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
def _infer_matched_stop(text: str) -> str:
|
|
1442
|
+
for marker in ("</search>", "</answer>"):
|
|
1443
|
+
if text.endswith(marker):
|
|
1444
|
+
return marker
|
|
1445
|
+
return ""
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
def _completion_stop(text: str, choice: Mapping[str, Any]) -> tuple[str, str]:
|
|
1449
|
+
"""Reconstruct the SGLang stop contract without duplicating provider text.
|
|
1450
|
+
|
|
1451
|
+
vLLM normally strips a matched stop string and reports it in stop_reason.
|
|
1452
|
+
Upstream ReCall appends that matched string itself. EOS IDs/unknown strings
|
|
1453
|
+
do not authorize inventing an answer/search closing tag.
|
|
1454
|
+
"""
|
|
1455
|
+
if choice.get("finish_reason") != "stop":
|
|
1456
|
+
return text, ""
|
|
1457
|
+
suffix = _infer_matched_stop(text)
|
|
1458
|
+
reason = choice.get("stop_reason")
|
|
1459
|
+
matched = reason if reason in ("</search>", "</answer>") else suffix
|
|
1460
|
+
if matched and suffix and matched != suffix:
|
|
1461
|
+
raise DriverError(
|
|
1462
|
+
"MODEL_RESPONSE_INVALID", "conflicting completion stop identities", consumed=True
|
|
1463
|
+
)
|
|
1464
|
+
if matched and text.endswith(matched):
|
|
1465
|
+
text = text[: -len(matched)]
|
|
1466
|
+
return text, matched
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def _text(value: Mapping[str, Any], key: str) -> str:
|
|
1470
|
+
result = value.get(key)
|
|
1471
|
+
if not isinstance(result, str) or not result.strip():
|
|
1472
|
+
raise DriverError("RUNTIME_MANIFEST_INVALID", f"runtime {key} is required")
|
|
1473
|
+
return result.strip()
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
def _bounded_text(value: Any, label: str) -> str:
|
|
1477
|
+
if not isinstance(value, str) or len(value) > MAX_TEXT_CHARS:
|
|
1478
|
+
raise DriverError("TEXT_INVALID", f"{label} must be bounded text")
|
|
1479
|
+
return value
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
def _positive_float(value: Mapping[str, Any], key: str) -> float:
|
|
1483
|
+
result = value.get(key)
|
|
1484
|
+
if isinstance(result, bool) or not isinstance(result, (int, float)) or result <= 0:
|
|
1485
|
+
raise DriverError("RUNTIME_MANIFEST_INVALID", f"runtime {key} must be positive")
|
|
1486
|
+
return float(result)
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
def _nonnegative(value: Any, label: str) -> int:
|
|
1490
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
1491
|
+
raise DriverError("VALUE_INVALID", f"{label} must be a non-negative integer")
|
|
1492
|
+
return value
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
def _validate_artifact_digests(artifacts: Mapping[str, Any]) -> None:
|
|
1496
|
+
for key, value in artifacts.items():
|
|
1497
|
+
if key.endswith("_sha256") and (
|
|
1498
|
+
not isinstance(value, str)
|
|
1499
|
+
or len(value) != 64
|
|
1500
|
+
or any(character not in _HEX for character in value)
|
|
1501
|
+
):
|
|
1502
|
+
raise DriverError("RUNTIME_MANIFEST_INVALID", f"invalid artifact digest: {key}")
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
def _validate_snapshot_files(files: Mapping[str, Any]) -> None:
|
|
1506
|
+
for relative, identity in files.items():
|
|
1507
|
+
if (
|
|
1508
|
+
not isinstance(relative, str)
|
|
1509
|
+
or not relative
|
|
1510
|
+
or Path(relative).is_absolute()
|
|
1511
|
+
or ".." in Path(relative).parts
|
|
1512
|
+
or not isinstance(identity, Mapping)
|
|
1513
|
+
or set(identity) - {"sha256", "bytes"}
|
|
1514
|
+
):
|
|
1515
|
+
raise DriverError("MODEL_SNAPSHOT_MISMATCH", "snapshot file entry is invalid")
|
|
1516
|
+
digest = identity.get("sha256")
|
|
1517
|
+
if (
|
|
1518
|
+
not isinstance(digest, str)
|
|
1519
|
+
or len(digest) != 64
|
|
1520
|
+
or any(character not in _HEX for character in digest)
|
|
1521
|
+
):
|
|
1522
|
+
raise DriverError("MODEL_SNAPSHOT_MISMATCH", "snapshot file digest is invalid")
|
|
1523
|
+
if "bytes" in identity:
|
|
1524
|
+
_nonnegative(identity["bytes"], "snapshot file bytes")
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
def _http_base(value: str) -> str:
|
|
1528
|
+
parsed = urllib.parse.urlsplit(value)
|
|
1529
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
1530
|
+
raise DriverError("ENDPOINT_INVALID", "runtime endpoint must be HTTP(S)")
|
|
1531
|
+
if parsed.username or parsed.password or parsed.query or parsed.fragment:
|
|
1532
|
+
raise DriverError("ENDPOINT_INVALID", "runtime endpoint contains forbidden components")
|
|
1533
|
+
return urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, parsed.path.rstrip("/"), "", ""))
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
def _json_object(raw: bytes, label: str) -> Mapping[str, Any]:
|
|
1537
|
+
try:
|
|
1538
|
+
value = _strict_json_loads(raw)
|
|
1539
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
1540
|
+
raise DriverError("JSON_INVALID", f"{label} is invalid JSON") from exc
|
|
1541
|
+
if not isinstance(value, Mapping):
|
|
1542
|
+
raise DriverError("JSON_INVALID", f"{label} must be an object")
|
|
1543
|
+
return value
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
def _strict_json_loads(raw: str | bytes) -> Any:
|
|
1547
|
+
return json.loads(
|
|
1548
|
+
raw,
|
|
1549
|
+
object_pairs_hook=_no_duplicate_object,
|
|
1550
|
+
parse_constant=_reject_json_constant,
|
|
1551
|
+
)
|
|
1552
|
+
|
|
1553
|
+
|
|
1554
|
+
def _reject_json_constant(value: str) -> Any:
|
|
1555
|
+
raise DriverError("JSON_INVALID", f"non-finite JSON number is forbidden: {value}")
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
def _no_duplicate_object(pairs: Sequence[tuple[str, Any]]) -> Mapping[str, Any]:
|
|
1559
|
+
result: dict[str, Any] = {}
|
|
1560
|
+
for key, value in pairs:
|
|
1561
|
+
if key in result:
|
|
1562
|
+
raise DriverError("JSON_INVALID", f"duplicate JSON key: {key}")
|
|
1563
|
+
result[key] = value
|
|
1564
|
+
return result
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
def _canonical_json(value: Any) -> str:
|
|
1568
|
+
return json.dumps(
|
|
1569
|
+
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False
|
|
1570
|
+
)
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
def _content_hash(value: Any) -> str:
|
|
1574
|
+
return _sha256(_canonical_json(value).encode("utf-8"))
|
|
1575
|
+
|
|
1576
|
+
|
|
1577
|
+
def _sha256(value: bytes) -> str:
|
|
1578
|
+
return hashlib.sha256(value).hexdigest()
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
def _sha256_file(path: Path) -> str:
|
|
1582
|
+
digest = hashlib.sha256()
|
|
1583
|
+
with path.open("rb") as handle:
|
|
1584
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
1585
|
+
digest.update(chunk)
|
|
1586
|
+
return digest.hexdigest()
|
|
1587
|
+
|
|
1588
|
+
|
|
1589
|
+
def _error_response(
|
|
1590
|
+
error: DriverError, request: Any, usage: Usage | None = None
|
|
1591
|
+
) -> Mapping[str, Any]:
|
|
1592
|
+
response: dict[str, Any] = {
|
|
1593
|
+
"status": "error",
|
|
1594
|
+
"failure_code": error.code,
|
|
1595
|
+
"error_message": error.public_message,
|
|
1596
|
+
}
|
|
1597
|
+
if usage is None or (
|
|
1598
|
+
not error.consumed and not usage.model_calls and not usage.retrieval_calls
|
|
1599
|
+
):
|
|
1600
|
+
response["consumed"] = False
|
|
1601
|
+
else:
|
|
1602
|
+
response["consumed"] = True
|
|
1603
|
+
response["usage"] = usage.as_mapping()
|
|
1604
|
+
if isinstance(request, Mapping):
|
|
1605
|
+
for key in ("request_id", "upstream_commit", "protocol_version"):
|
|
1606
|
+
if isinstance(request.get(key), str):
|
|
1607
|
+
response[key] = request[key]
|
|
1608
|
+
return response
|
|
1609
|
+
|
|
1610
|
+
|
|
1611
|
+
def serve_jsonl(driver: SmartSearchDriver, source: TextIO, sink: TextIO) -> int:
|
|
1612
|
+
for line in source:
|
|
1613
|
+
request: Any = None
|
|
1614
|
+
if len(line.encode("utf-8")) > MAX_REQUEST_BYTES:
|
|
1615
|
+
response = _error_response(DriverError("REQUEST_TOO_LARGE", "request too large"), None)
|
|
1616
|
+
else:
|
|
1617
|
+
try:
|
|
1618
|
+
request = _strict_json_loads(line)
|
|
1619
|
+
response = driver.run(request)
|
|
1620
|
+
except (json.JSONDecodeError, DriverError) as exc:
|
|
1621
|
+
error = (
|
|
1622
|
+
exc
|
|
1623
|
+
if isinstance(exc, DriverError)
|
|
1624
|
+
else DriverError("INVALID_JSON", "invalid JSON")
|
|
1625
|
+
)
|
|
1626
|
+
response = _error_response(error, request, driver.last_usage)
|
|
1627
|
+
except Exception:
|
|
1628
|
+
response = _error_response(
|
|
1629
|
+
DriverError("WORKER_FAILURE", "worker failed safely"),
|
|
1630
|
+
request,
|
|
1631
|
+
driver.last_usage,
|
|
1632
|
+
)
|
|
1633
|
+
sink.write(_canonical_json(response) + "\n")
|
|
1634
|
+
sink.flush()
|
|
1635
|
+
return 0
|
|
1636
|
+
|
|
1637
|
+
|
|
1638
|
+
def main() -> int:
|
|
1639
|
+
return serve_jsonl(SmartSearchDriver(), sys.stdin, sys.stdout)
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
if __name__ == "__main__":
|
|
1643
|
+
raise SystemExit(main())
|