agentinputlab 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (469) hide show
  1. agentinputlab/__init__.py +28 -0
  2. agentinputlab/algorithms/__init__.py +6 -0
  3. agentinputlab/algorithms/acquire/__init__.py +145 -0
  4. agentinputlab/algorithms/acquire/adaptive_rag/__init__.py +43 -0
  5. agentinputlab/algorithms/acquire/adaptive_rag/replay.py +367 -0
  6. agentinputlab/algorithms/acquire/adaptive_rag/runtime.py +1202 -0
  7. agentinputlab/algorithms/acquire/adaptive_rag_common_support.py +542 -0
  8. agentinputlab/algorithms/acquire/hyde.py +296 -0
  9. agentinputlab/algorithms/acquire/hyde_controlled.py +200 -0
  10. agentinputlab/algorithms/acquire/hyde_external.py +148 -0
  11. agentinputlab/algorithms/acquire/ircot.py +28 -0
  12. agentinputlab/algorithms/acquire/ircot_common_support.py +478 -0
  13. agentinputlab/algorithms/acquire/ircot_native.py +355 -0
  14. agentinputlab/algorithms/acquire/ircot_replay.py +327 -0
  15. agentinputlab/algorithms/acquire/qrag/__init__.py +79 -0
  16. agentinputlab/algorithms/acquire/qrag/bridge.py +828 -0
  17. agentinputlab/algorithms/acquire/qrag/common_support.py +891 -0
  18. agentinputlab/algorithms/acquire/qrag/native_axis.py +71 -0
  19. agentinputlab/algorithms/acquire/qrag/reader_adapter.py +139 -0
  20. agentinputlab/algorithms/acquire/r2ag/__init__.py +57 -0
  21. agentinputlab/algorithms/acquire/r2ag/bridge.py +1312 -0
  22. agentinputlab/algorithms/acquire/r2ag/common_support.py +1029 -0
  23. agentinputlab/algorithms/acquire/r2ag/socket_runtime.py +255 -0
  24. agentinputlab/algorithms/clarify/__init__.py +61 -0
  25. agentinputlab/algorithms/clarify/_structured.py +64 -0
  26. agentinputlab/algorithms/clarify/active_task_disambiguation.py +371 -0
  27. agentinputlab/algorithms/clarify/ask_or_assume/__init__.py +17 -0
  28. agentinputlab/algorithms/clarify/ask_or_assume/monitor.py +410 -0
  29. agentinputlab/algorithms/clarify/future_turns.py +379 -0
  30. agentinputlab/algorithms/clarify/udcs.py +255 -0
  31. agentinputlab/algorithms/clarify/udcs_deterministic.py +196 -0
  32. agentinputlab/algorithms/curate/__init__.py +65 -0
  33. agentinputlab/algorithms/curate/acon/__init__.py +27 -0
  34. agentinputlab/algorithms/curate/acon/common.py +284 -0
  35. agentinputlab/algorithms/curate/acon/common_support.py +136 -0
  36. agentinputlab/algorithms/curate/acon/history_bridge.py +309 -0
  37. agentinputlab/algorithms/curate/acon/observation_bridge.py +290 -0
  38. agentinputlab/algorithms/curate/llmlingua/__init__.py +30 -0
  39. agentinputlab/algorithms/curate/llmlingua/adapters.py +537 -0
  40. agentinputlab/algorithms/curate/llmlingua/budget.py +48 -0
  41. agentinputlab/algorithms/curate/llmlingua/controlled.py +175 -0
  42. agentinputlab/algorithms/curate/llmlingua/multilingual.py +105 -0
  43. agentinputlab/algorithms/curate/llmlingua/span_map.py +167 -0
  44. agentinputlab/algorithms/curate/recomp/__init__.py +47 -0
  45. agentinputlab/algorithms/curate/recomp/external.py +1129 -0
  46. agentinputlab/algorithms/curate/recomp/selective.py +304 -0
  47. agentinputlab/algorithms/decompose/__init__.py +107 -0
  48. agentinputlab/algorithms/decompose/decomp/__init__.py +53 -0
  49. agentinputlab/algorithms/decompose/decomp/common_support.py +478 -0
  50. agentinputlab/algorithms/decompose/decomp/planner_component.py +199 -0
  51. agentinputlab/algorithms/decompose/decomp/worker_bridge.py +416 -0
  52. agentinputlab/algorithms/decompose/llm_compiler/__init__.py +35 -0
  53. agentinputlab/algorithms/decompose/llm_compiler/bridge.py +520 -0
  54. agentinputlab/algorithms/decompose/llm_compiler/common_support.py +504 -0
  55. agentinputlab/algorithms/decompose/plan_and_budget/__init__.py +43 -0
  56. agentinputlab/algorithms/decompose/plan_and_budget/cached_soft_budget.py +362 -0
  57. agentinputlab/algorithms/decompose/plan_and_budget/decomposer_component.py +107 -0
  58. agentinputlab/algorithms/decompose/plan_and_budget/full.py +205 -0
  59. agentinputlab/algorithms/decompose/plan_and_budget/hard_cap.py +246 -0
  60. agentinputlab/algorithms/decompose/plan_and_budget/regenerated_soft_budget.py +448 -0
  61. agentinputlab/algorithms/decompose/plan_and_solve/__init__.py +33 -0
  62. agentinputlab/algorithms/decompose/plan_and_solve/plan_component.py +192 -0
  63. agentinputlab/algorithms/decompose/plan_and_solve/ps_prompt.py +127 -0
  64. agentinputlab/algorithms/decompose/self_ask/__init__.py +27 -0
  65. agentinputlab/algorithms/decompose/self_ask/loop.py +362 -0
  66. agentinputlab/algorithms/decompose/self_ask/planner_component.py +117 -0
  67. agentinputlab/algorithms/reformulate/__init__.py +85 -0
  68. agentinputlab/algorithms/reformulate/infocqr/__init__.py +40 -0
  69. agentinputlab/algorithms/reformulate/infocqr/cache_replay.py +362 -0
  70. agentinputlab/algorithms/reformulate/infocqr/external.py +675 -0
  71. agentinputlab/algorithms/reformulate/infocqr/student.py +189 -0
  72. agentinputlab/algorithms/reformulate/query2doc/__init__.py +42 -0
  73. agentinputlab/algorithms/reformulate/query2doc/controlled_live.py +190 -0
  74. agentinputlab/algorithms/reformulate/query2doc/models.py +95 -0
  75. agentinputlab/algorithms/reformulate/query2doc/official_cached.py +335 -0
  76. agentinputlab/algorithms/reformulate/query2doc/querygym_bridge.py +223 -0
  77. agentinputlab/algorithms/reformulate/query2doc/querygym_runtime.py +326 -0
  78. agentinputlab/algorithms/reformulate/reformer/__init__.py +23 -0
  79. agentinputlab/algorithms/reformulate/reformer/external.py +417 -0
  80. agentinputlab/algorithms/reformulate/rrr.py +696 -0
  81. agentinputlab/annotation/__init__.py +5 -0
  82. agentinputlab/annotation/independent.py +428 -0
  83. agentinputlab/annotation/server.py +233 -0
  84. agentinputlab/annotation/static/app.js +121 -0
  85. agentinputlab/annotation/static/index.html +60 -0
  86. agentinputlab/annotation/static/review.css +2 -0
  87. agentinputlab/annotation/static/review.html +10 -0
  88. agentinputlab/annotation/static/review.js +11 -0
  89. agentinputlab/annotation/static/styles.css +55 -0
  90. agentinputlab/annotation/store.py +444 -0
  91. agentinputlab/api.py +368 -0
  92. agentinputlab/artifacts/__init__.py +15 -0
  93. agentinputlab/artifacts/json_file.py +104 -0
  94. agentinputlab/artifacts/manifest.py +98 -0
  95. agentinputlab/artifacts/store.py +157 -0
  96. agentinputlab/backends/__init__.py +98 -0
  97. agentinputlab/backends/embedding/__init__.py +25 -0
  98. agentinputlab/backends/embedding/base.py +145 -0
  99. agentinputlab/backends/embedding/callable.py +75 -0
  100. agentinputlab/backends/embedding/replay.py +129 -0
  101. agentinputlab/backends/llm/__init__.py +31 -0
  102. agentinputlab/backends/llm/base.py +149 -0
  103. agentinputlab/backends/llm/callable.py +108 -0
  104. agentinputlab/backends/llm/decomp_completion.py +88 -0
  105. agentinputlab/backends/llm/final_content_stop.py +72 -0
  106. agentinputlab/backends/llm/gpt_oss_final_channel.py +84 -0
  107. agentinputlab/backends/llm/openai_compatible.py +332 -0
  108. agentinputlab/backends/llm/transformers.py +171 -0
  109. agentinputlab/backends/retrieval/__init__.py +35 -0
  110. agentinputlab/backends/retrieval/base.py +50 -0
  111. agentinputlab/backends/retrieval/dense.py +777 -0
  112. agentinputlab/backends/retrieval/memory.py +119 -0
  113. agentinputlab/backends/retrieval/sqlite_fts.py +155 -0
  114. agentinputlab/backends/user/__init__.py +15 -0
  115. agentinputlab/backends/user/base.py +66 -0
  116. agentinputlab/backends/user/callable.py +54 -0
  117. agentinputlab/backends/user/replay.py +89 -0
  118. agentinputlab/backends/vector.py +38 -0
  119. agentinputlab/benchmarks/__init__.py +246 -0
  120. agentinputlab/benchmarks/acquisition.py +106 -0
  121. agentinputlab/benchmarks/ale.py +540 -0
  122. agentinputlab/benchmarks/appworld_ul.py +669 -0
  123. agentinputlab/benchmarks/assets.py +226 -0
  124. agentinputlab/benchmarks/base.py +68 -0
  125. agentinputlab/benchmarks/beir_archive.py +235 -0
  126. agentinputlab/benchmarks/beir_sample.py +269 -0
  127. agentinputlab/benchmarks/bfcl_native.py +245 -0
  128. agentinputlab/benchmarks/bfcl_v3.py +607 -0
  129. agentinputlab/benchmarks/cache.py +218 -0
  130. agentinputlab/benchmarks/clariq.py +203 -0
  131. agentinputlab/benchmarks/data/registry.json +321 -0
  132. agentinputlab/benchmarks/data/smoke_assets.json +511 -0
  133. agentinputlab/benchmarks/data/statebench_v0_8_1_task_ids.json +475 -0
  134. agentinputlab/benchmarks/data/toolsandbox_165848b_scenario_names.json +1 -0
  135. agentinputlab/benchmarks/execution.py +1111 -0
  136. agentinputlab/benchmarks/interactive.py +257 -0
  137. agentinputlab/benchmarks/ir_query.py +204 -0
  138. agentinputlab/benchmarks/longbench.py +203 -0
  139. agentinputlab/benchmarks/paired_replay.py +479 -0
  140. agentinputlab/benchmarks/preprocess.py +2313 -0
  141. agentinputlab/benchmarks/production.py +236 -0
  142. agentinputlab/benchmarks/qa.py +963 -0
  143. agentinputlab/benchmarks/registry.py +110 -0
  144. agentinputlab/benchmarks/retrieval.py +459 -0
  145. agentinputlab/benchmarks/runtime.py +509 -0
  146. agentinputlab/benchmarks/statebench.py +938 -0
  147. agentinputlab/benchmarks/tau3.py +725 -0
  148. agentinputlab/benchmarks/toolsandbox.py +626 -0
  149. agentinputlab/benchmarks/trec_dl_sample.py +263 -0
  150. agentinputlab/campaign.py +654 -0
  151. agentinputlab/campaign_runner.py +953 -0
  152. agentinputlab/cli/__init__.py +6 -0
  153. agentinputlab/cli/main.py +933 -0
  154. agentinputlab/connectors/__init__.py +46 -0
  155. agentinputlab/connectors/knowledge/__init__.py +4 -0
  156. agentinputlab/connectors/tools/__init__.py +30 -0
  157. agentinputlab/connectors/tools/base.py +57 -0
  158. agentinputlab/connectors/tools/mcp.py +222 -0
  159. agentinputlab/connectors/tools/memory.py +41 -0
  160. agentinputlab/connectors/tools/openapi.py +695 -0
  161. agentinputlab/connectors/tools/schema.py +102 -0
  162. agentinputlab/contracts/__init__.py +91 -0
  163. agentinputlab/contracts/availability.py +24 -0
  164. agentinputlab/contracts/dependency.py +58 -0
  165. agentinputlab/contracts/enums.py +82 -0
  166. agentinputlab/contracts/errors.py +63 -0
  167. agentinputlab/contracts/evidence.py +43 -0
  168. agentinputlab/contracts/intent.py +110 -0
  169. agentinputlab/contracts/lifecycle.py +164 -0
  170. agentinputlab/contracts/patch.py +167 -0
  171. agentinputlab/contracts/serialization.py +93 -0
  172. agentinputlab/contracts/state.py +127 -0
  173. agentinputlab/contracts/trace.py +59 -0
  174. agentinputlab/contracts/worker.py +42 -0
  175. agentinputlab/core/__init__.py +7 -0
  176. agentinputlab/data/__init__.py +32 -0
  177. agentinputlab/data/checksums.py +9 -0
  178. agentinputlab/data/downloader.py +235 -0
  179. agentinputlab/data/http.py +171 -0
  180. agentinputlab/data/index.py +21 -0
  181. agentinputlab/data/overlap.py +28 -0
  182. agentinputlab/data/preprocess.py +32 -0
  183. agentinputlab/data/storage.py +57 -0
  184. agentinputlab/data/trec_sample.py +427 -0
  185. agentinputlab/evaluation/__init__.py +5 -0
  186. agentinputlab/evaluation/outcomes.py +32 -0
  187. agentinputlab/experiments/__init__.py +278 -0
  188. agentinputlab/experiments/analysis.py +981 -0
  189. agentinputlab/experiments/artifacts.py +687 -0
  190. agentinputlab/experiments/automatic_measurement.py +678 -0
  191. agentinputlab/experiments/auxiliary_matrix.py +203 -0
  192. agentinputlab/experiments/bindings.py +430 -0
  193. agentinputlab/experiments/blueprints.py +147 -0
  194. agentinputlab/experiments/builders/__init__.py +45 -0
  195. agentinputlab/experiments/builders/executable_external_payload.py +423 -0
  196. agentinputlab/experiments/builders/external_process_arena.py +1018 -0
  197. agentinputlab/experiments/builders/registry_intervention.py +463 -0
  198. agentinputlab/experiments/cli.py +158 -0
  199. agentinputlab/experiments/controls.py +340 -0
  200. agentinputlab/experiments/driver_guard.py +205 -0
  201. agentinputlab/experiments/drivers/__init__.py +89 -0
  202. agentinputlab/experiments/drivers/external_arena.py +2908 -0
  203. agentinputlab/experiments/drivers/method_process.py +662 -0
  204. agentinputlab/experiments/drivers/parity_process.py +1484 -0
  205. agentinputlab/experiments/drivers/static_production.py +1574 -0
  206. agentinputlab/experiments/executor.py +448 -0
  207. agentinputlab/experiments/loader.py +605 -0
  208. agentinputlab/experiments/measurement.py +675 -0
  209. agentinputlab/experiments/paper_analysis.py +1818 -0
  210. agentinputlab/experiments/parity_identity.py +605 -0
  211. agentinputlab/experiments/plan.py +277 -0
  212. agentinputlab/experiments/production_builders.py +3739 -0
  213. agentinputlab/experiments/readiness.py +432 -0
  214. agentinputlab/experiments/schema.py +514 -0
  215. agentinputlab/experiments/secrets.py +177 -0
  216. agentinputlab/experiments/stateful_analysis.py +1738 -0
  217. agentinputlab/experiments/transfer_analysis.py +1316 -0
  218. agentinputlab/experiments/unseen_analysis.py +1025 -0
  219. agentinputlab/integrations/__init__.py +1 -0
  220. agentinputlab/integrations/action_space/__init__.py +1 -0
  221. agentinputlab/integrations/external_workers/__init__.py +76 -0
  222. agentinputlab/integrations/external_workers/ace.py +431 -0
  223. agentinputlab/integrations/external_workers/base.py +181 -0
  224. agentinputlab/integrations/external_workers/decomp_live.py +249 -0
  225. agentinputlab/integrations/external_workers/decomp_planner.py +30 -0
  226. agentinputlab/integrations/external_workers/infocqr_student.py +280 -0
  227. agentinputlab/integrations/external_workers/ircot.py +576 -0
  228. agentinputlab/integrations/external_workers/llmcompiler_live.py +360 -0
  229. agentinputlab/integrations/external_workers/longllmlingua.py +162 -0
  230. agentinputlab/integrations/external_workers/method.py +314 -0
  231. agentinputlab/integrations/external_workers/process.py +678 -0
  232. agentinputlab/integrations/external_workers/supervisor.py +1071 -0
  233. agentinputlab/integrations/full_architecture/__init__.py +1 -0
  234. agentinputlab/integrations/full_architecture/hyde.py +212 -0
  235. agentinputlab/integrations/full_architecture/plan_and_budget.py +188 -0
  236. agentinputlab/integrations/live_history.py +222 -0
  237. agentinputlab/integrations/prompt_optimization/__init__.py +1 -0
  238. agentinputlab/integrations/training/__init__.py +1 -0
  239. agentinputlab/metrics/__init__.py +50 -0
  240. agentinputlab/metrics/catalog.py +313 -0
  241. agentinputlab/metrics/harm.py +95 -0
  242. agentinputlab/metrics/paired.py +40 -0
  243. agentinputlab/metrics/qa.py +76 -0
  244. agentinputlab/metrics/text.py +33 -0
  245. agentinputlab/metrics/third_party.py +161 -0
  246. agentinputlab/models/__init__.py +4 -0
  247. agentinputlab/observability/__init__.py +56 -0
  248. agentinputlab/observability/budget.py +316 -0
  249. agentinputlab/observability/instrumentation.py +754 -0
  250. agentinputlab/observability/models.py +403 -0
  251. agentinputlab/observability/monitor.py +380 -0
  252. agentinputlab/optimizers/__init__.py +1 -0
  253. agentinputlab/optimizers/clarify/__init__.py +12 -0
  254. agentinputlab/optimizers/context/__init__.py +22 -0
  255. agentinputlab/optimizers/decompose/__init__.py +64 -0
  256. agentinputlab/optimizers/evidence/__init__.py +22 -0
  257. agentinputlab/optimizers/rewrite/__init__.py +30 -0
  258. agentinputlab/parity/__init__.py +5 -0
  259. agentinputlab/parity/base.py +206 -0
  260. agentinputlab/production_campaign.py +763 -0
  261. agentinputlab/py.typed +1 -0
  262. agentinputlab/recipes/__init__.py +333 -0
  263. agentinputlab/recipes/ace.py +418 -0
  264. agentinputlab/recipes/ace_bfcl.py +273 -0
  265. agentinputlab/recipes/ace_common_support.py +169 -0
  266. agentinputlab/recipes/ace_native_plan.py +322 -0
  267. agentinputlab/recipes/ace_online.py +711 -0
  268. agentinputlab/recipes/ace_retrieval.py +330 -0
  269. agentinputlab/recipes/ace_statebench.py +382 -0
  270. agentinputlab/recipes/ace_tau.py +463 -0
  271. agentinputlab/recipes/ace_toolsandbox.py +432 -0
  272. agentinputlab/recipes/acon_guideline.py +22 -0
  273. agentinputlab/recipes/adaptive_rag.py +822 -0
  274. agentinputlab/recipes/llmlingua2_inference.py +146 -0
  275. agentinputlab/recipes/llmlingua2_training.py +796 -0
  276. agentinputlab/recipes/mce.py +1486 -0
  277. agentinputlab/recipes/mce_final.py +335 -0
  278. agentinputlab/recipes/mce_text_fit.py +499 -0
  279. agentinputlab/recipes/mce_text_only.py +298 -0
  280. agentinputlab/recipes/qrag.py +729 -0
  281. agentinputlab/recipes/qrag_inference.py +364 -0
  282. agentinputlab/recipes/r2ag_features.py +236 -0
  283. agentinputlab/recipes/r2ag_inference.py +503 -0
  284. agentinputlab/recipes/r2ag_training.py +948 -0
  285. agentinputlab/recipes/recomp_abstractive.py +592 -0
  286. agentinputlab/recipes/recomp_extractive.py +525 -0
  287. agentinputlab/recipes/reformer.py +17 -0
  288. agentinputlab/recipes/rqrag.py +1516 -0
  289. agentinputlab/recipes/rqrag_common_support.py +193 -0
  290. agentinputlab/recipes/rqrag_completion.py +147 -0
  291. agentinputlab/recipes/skillopt.py +462 -0
  292. agentinputlab/recipes/skillopt_common_support.py +154 -0
  293. agentinputlab/recipes/skillopt_environment.py +165 -0
  294. agentinputlab/recipes/skillopt_environment_cli.py +5 -0
  295. agentinputlab/recipes/skillopt_retrieval.py +188 -0
  296. agentinputlab/recipes/smartsearch.py +897 -0
  297. agentinputlab/recipes/smartsearch_curriculum.py +283 -0
  298. agentinputlab/recipes/smartsearch_grpo_contracts.py +58 -0
  299. agentinputlab/recipes/smartsearch_grpo_data.py +314 -0
  300. agentinputlab/recipes/smartsearch_grpo_meter.py +324 -0
  301. agentinputlab/recipes/smartsearch_grpo_prefix.py +144 -0
  302. agentinputlab/recipes/smartsearch_grpo_rollout.py +119 -0
  303. agentinputlab/recipes/smartsearch_grpo_tools.py +364 -0
  304. agentinputlab/recipes/smartsearch_grpo_training.py +305 -0
  305. agentinputlab/recipes/smartsearch_grpo_transport.py +248 -0
  306. agentinputlab/recipes/smartsearch_grpo_worker.py +364 -0
  307. agentinputlab/recipes/smartsearch_inference.py +524 -0
  308. agentinputlab/recipes/smartsearch_native_data.py +172 -0
  309. agentinputlab/recipes/smartsearch_policy.py +616 -0
  310. agentinputlab/recipes/smartsearch_policy_worker.py +9 -0
  311. agentinputlab/recipes/smartsearch_sampling.py +232 -0
  312. agentinputlab/recipes/smartsearch_teacher.py +203 -0
  313. agentinputlab/recipes/smartsearch_training.py +265 -0
  314. agentinputlab/registry/__init__.py +46 -0
  315. agentinputlab/registry/beta.py +727 -0
  316. agentinputlab/registry/data/algorithms.json +1959 -0
  317. agentinputlab/registry/data/auxiliary_method_profiles.json +34 -0
  318. agentinputlab/registry/loader.py +100 -0
  319. agentinputlab/registry/result_manifest.py +184 -0
  320. agentinputlab/registry/runtime.py +195 -0
  321. agentinputlab/registry/schema.py +449 -0
  322. agentinputlab/runner/__init__.py +30 -0
  323. agentinputlab/runner/balance.py +248 -0
  324. agentinputlab/runner/cache.py +135 -0
  325. agentinputlab/runner/composite.py +130 -0
  326. agentinputlab/runner/engine.py +285 -0
  327. agentinputlab/runner/runtime_guard.py +268 -0
  328. agentinputlab/runner/worker.py +80 -0
  329. agentinputlab/runtime_bootstrap.py +288 -0
  330. agentinputlab/selectors/__init__.py +196 -0
  331. agentinputlab/selectors/advanced.py +1514 -0
  332. agentinputlab/selectors/artifacts.py +1087 -0
  333. agentinputlab/selectors/base.py +112 -0
  334. agentinputlab/selectors/campaigns.py +1893 -0
  335. agentinputlab/selectors/catalog.py +86 -0
  336. agentinputlab/selectors/discovery.py +1860 -0
  337. agentinputlab/selectors/evaluation.py +1348 -0
  338. agentinputlab/selectors/features.py +383 -0
  339. agentinputlab/selectors/learned.py +502 -0
  340. agentinputlab/selectors/no_op.py +28 -0
  341. agentinputlab/selectors/outcome_table.py +475 -0
  342. agentinputlab/selectors/producer.py +853 -0
  343. agentinputlab/selectors/rules.py +109 -0
  344. agentinputlab/selectors/runtime.py +420 -0
  345. agentinputlab/studies/__init__.py +16 -0
  346. agentinputlab/studies/design.py +165 -0
  347. agentinputlab/studies/paired.py +322 -0
  348. agentinputlab/studio/__init__.py +6 -0
  349. agentinputlab/studio/campaigns.py +819 -0
  350. agentinputlab/studio/engine.py +1408 -0
  351. agentinputlab/studio/model_backends.py +425 -0
  352. agentinputlab/studio/prepared.py +410 -0
  353. agentinputlab/studio/server.py +613 -0
  354. agentinputlab/studio/static/app.js +397 -0
  355. agentinputlab/studio/static/campaigns.html +175 -0
  356. agentinputlab/studio/static/campaigns.js +426 -0
  357. agentinputlab/studio/static/index.html +192 -0
  358. agentinputlab/studio/static/result.html +103 -0
  359. agentinputlab/studio/static/result.js +273 -0
  360. agentinputlab/studio/static/styles.css +467 -0
  361. agentinputlab-0.1.0a1.data/data/share/agentinputlab/CITATION.cff +22 -0
  362. agentinputlab-0.1.0a1.dist-info/METADATA +392 -0
  363. agentinputlab-0.1.0a1.dist-info/RECORD +469 -0
  364. agentinputlab-0.1.0a1.dist-info/WHEEL +5 -0
  365. agentinputlab-0.1.0a1.dist-info/entry_points.txt +6 -0
  366. agentinputlab-0.1.0a1.dist-info/licenses/LICENSE +21 -0
  367. agentinputlab-0.1.0a1.dist-info/licenses/NOTICE +31 -0
  368. agentinputlab-0.1.0a1.dist-info/licenses/THIRD_PARTY_NOTICES.md +94 -0
  369. agentinputlab-0.1.0a1.dist-info/top_level.txt +2 -0
  370. workers/README.md +94 -0
  371. workers/ace/README.md +261 -0
  372. workers/ace/__init__.py +1 -0
  373. workers/ace/compatibility_manifest.json +39 -0
  374. workers/ace/driver.py +1682 -0
  375. workers/acon/README.md +215 -0
  376. workers/acon/__init__.py +5 -0
  377. workers/acon/compatibility_manifest.json +93 -0
  378. workers/acon/driver.py +1859 -0
  379. workers/acon/fitted.py +395 -0
  380. workers/acon/guideline.py +470 -0
  381. workers/acon/guideline_source_manifest.json +20 -0
  382. workers/adaptive_rag/README.md +96 -0
  383. workers/adaptive_rag/__init__.py +15 -0
  384. workers/adaptive_rag/compatibility_manifest.json +61 -0
  385. workers/adaptive_rag/driver.py +1470 -0
  386. workers/adaptive_rag/fixture_runtime.py +163 -0
  387. workers/adaptive_rag/runtime.py +252 -0
  388. workers/ask_or_assume/README.md +124 -0
  389. workers/ask_or_assume/__init__.py +1 -0
  390. workers/ask_or_assume/compatibility_manifest.json +36 -0
  391. workers/ask_or_assume/driver.py +1227 -0
  392. workers/decomp/README.md +133 -0
  393. workers/decomp/UPSTREAM_LICENSE.Apache-2.0.txt +201 -0
  394. workers/decomp/__init__.py +5 -0
  395. workers/decomp/compatibility_manifest.json +205 -0
  396. workers/decomp/driver.py +1015 -0
  397. workers/decomp/live.py +137 -0
  398. workers/decomp/runtime.py +231 -0
  399. workers/infocqr/README.md +106 -0
  400. workers/infocqr/__init__.py +1 -0
  401. workers/infocqr/compatibility_manifest.json +84 -0
  402. workers/infocqr/driver.py +1766 -0
  403. workers/ircot/README.md +167 -0
  404. workers/ircot/__init__.py +7 -0
  405. workers/ircot/compatibility_manifest.json +121 -0
  406. workers/ircot/controlled.py +457 -0
  407. workers/ircot/driver.py +1373 -0
  408. workers/ircot/fixture_bindings.json +39 -0
  409. workers/ircot/fixture_config.json +48 -0
  410. workers/ircot/fixture_prompt.txt +2 -0
  411. workers/ircot/native_qa.py +398 -0
  412. workers/ircot/qa_compatibility_manifest.json +57 -0
  413. workers/ircot/runtime.py +97 -0
  414. workers/llmcompiler/README.md +160 -0
  415. workers/llmcompiler/__init__.py +1 -0
  416. workers/llmcompiler/compatibility_manifest.json +45 -0
  417. workers/llmcompiler/driver.py +1514 -0
  418. workers/llmcompiler/export_prompts.py +74 -0
  419. workers/llmcompiler/live.py +174 -0
  420. workers/llmcompiler/patch_manifest.json +147 -0
  421. workers/mce/README.md +198 -0
  422. workers/mce/__init__.py +29 -0
  423. workers/mce/compatibility_manifest.json +115 -0
  424. workers/mce/driver.py +2585 -0
  425. workers/mce/final_eval.py +173 -0
  426. workers/official_arenas/README.md +25 -0
  427. workers/official_arenas/__init__.py +1 -0
  428. workers/official_arenas/capture_records.py +110 -0
  429. workers/official_arenas/driver.py +619 -0
  430. workers/official_arenas/protocol.py +334 -0
  431. workers/official_arenas/providers/__init__.py +1 -0
  432. workers/official_arenas/providers/_live_history.py +228 -0
  433. workers/official_arenas/providers/_message_compat.py +77 -0
  434. workers/official_arenas/providers/appworld_ul.py +196 -0
  435. workers/official_arenas/providers/fixture.py +112 -0
  436. workers/official_arenas/providers/statebench.py +960 -0
  437. workers/official_arenas/providers/tau3.py +1672 -0
  438. workers/official_arenas/providers/tau_retail.py +586 -0
  439. workers/official_arenas/providers/toolsandbox.py +1321 -0
  440. workers/official_arenas/providers/tooltalk.py +699 -0
  441. workers/official_arenas/retail_capture.py +188 -0
  442. workers/official_arenas/retail_driver.py +275 -0
  443. workers/qrag/README.md +172 -0
  444. workers/qrag/__init__.py +1 -0
  445. workers/qrag/compatibility_manifest.json +64 -0
  446. workers/qrag/driver.py +673 -0
  447. workers/r2ag/README.md +154 -0
  448. workers/r2ag/__init__.py +5 -0
  449. workers/r2ag/compatibility_manifest.json +55 -0
  450. workers/r2ag/driver.py +1287 -0
  451. workers/r2ag/feature_schema.json +8 -0
  452. workers/r2ag/runtime.py +589 -0
  453. workers/recomp/README.md +120 -0
  454. workers/recomp/__init__.py +1 -0
  455. workers/recomp/compatibility_manifest.json +43 -0
  456. workers/recomp/driver.py +1653 -0
  457. workers/reformer/README.md +125 -0
  458. workers/reformer/__init__.py +1 -0
  459. workers/reformer/compatibility_manifest.json +68 -0
  460. workers/reformer/driver.py +1459 -0
  461. workers/reformer/induction.py +457 -0
  462. workers/rrr/README.md +126 -0
  463. workers/rrr/__init__.py +1 -0
  464. workers/rrr/compatibility_manifest.json +57 -0
  465. workers/rrr/driver.py +1133 -0
  466. workers/smartsearch/README.md +331 -0
  467. workers/smartsearch/__init__.py +1 -0
  468. workers/smartsearch/compatibility_manifest.json +41 -0
  469. workers/smartsearch/driver.py +1643 -0
@@ -0,0 +1,1514 @@
1
+ """Auditable, dependency-free selector baselines for offline confirmation.
2
+
3
+ All fitting functions consume outcome-derived information only from an explicit
4
+ ``train`` split. Calibration functions accept only ``calibration`` rows. At
5
+ runtime the selectors receive :class:`SelectionFeatures` and an eligibility
6
+ mask; evaluator gold and other-arm outcomes never cross the ``Selector``
7
+ protocol.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ from collections.abc import Collection, Mapping, Sequence
14
+ from dataclasses import dataclass, is_dataclass, replace
15
+ from statistics import fmean
16
+ from types import MappingProxyType
17
+ from typing import Any
18
+
19
+ from agentinputlab.contracts.enums import SemanticFamily
20
+ from agentinputlab.contracts.serialization import content_hash
21
+ from agentinputlab.contracts.state import FrozenAgentState
22
+
23
+ from .base import SelectionDecision, SelectionFeatures, Selector, UtilityGainProvider
24
+ from .learned import (
25
+ PolicyEvaluation,
26
+ SelectorExample,
27
+ _selector_id_set_digest,
28
+ evaluate_selector,
29
+ )
30
+
31
+ _ACTIONS = (
32
+ SemanticFamily.REFORMULATE,
33
+ SemanticFamily.DECOMPOSE,
34
+ SemanticFamily.ACQUIRE,
35
+ SemanticFamily.CLARIFY,
36
+ SemanticFamily.CURATE,
37
+ )
38
+ _FEATURE_WIDTH = len(SelectionFeatures().as_vector())
39
+ _EMPTY_TRAIN_ID_SET_DIGEST = content_hash({"split": "train", "example_ids": []})
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class KNNTrainingRow:
44
+ """One identity-free feature/gain row retained by a KNN selector."""
45
+
46
+ feature_vector: tuple[float, ...]
47
+ gains: Mapping[SemanticFamily, float]
48
+
49
+ def __post_init__(self) -> None:
50
+ raw_vector = tuple(self.feature_vector)
51
+ if len(raw_vector) != _FEATURE_WIDTH or any(not _finite(value) for value in raw_vector):
52
+ raise ValueError(f"KNN feature vectors must contain {_FEATURE_WIDTH} finite values")
53
+ raw_gains = dict(self.gains)
54
+ if not raw_gains or any(family not in _ACTIONS for family in raw_gains):
55
+ raise ValueError("KNN rows require intervention-family gains")
56
+ if any(not _finite(value) for value in raw_gains.values()):
57
+ raise ValueError("KNN gains must be finite numbers")
58
+ vector = tuple(float(value) for value in raw_vector)
59
+ copied = {family: float(value) for family, value in raw_gains.items()}
60
+ object.__setattr__(self, "feature_vector", vector)
61
+ object.__setattr__(self, "gains", MappingProxyType(copied))
62
+
63
+ @property
64
+ def row_hash(self) -> str:
65
+ return content_hash(
66
+ {
67
+ "feature_vector": self.feature_vector,
68
+ "gains": {family.value: value for family, value in self.gains.items()},
69
+ }
70
+ )
71
+
72
+
73
+ @dataclass(frozen=True, slots=True)
74
+ class KNNUtilitySelector:
75
+ """Estimate family utility gains from nearby gold-free feature vectors."""
76
+
77
+ training_rows: tuple[KNNTrainingRow, ...]
78
+ release_by_family: Mapping[SemanticFamily, str]
79
+ k: int = 5
80
+ distance_power: float = 2.0
81
+ min_gain: float = 0.0
82
+ margin: float = 0.0
83
+
84
+ def __post_init__(self) -> None:
85
+ rows = tuple(self.training_rows)
86
+ if not rows or any(not isinstance(row, KNNTrainingRow) for row in rows):
87
+ raise ValueError("KNN selector requires frozen training rows")
88
+ releases = _validated_release_mapping(self.release_by_family)
89
+ observed = {family for row in rows for family in row.gains}
90
+ missing = set(releases) - observed
91
+ if missing:
92
+ names = ", ".join(sorted(family.value for family in missing))
93
+ raise ValueError(f"KNN training rows have no outcomes for: {names}")
94
+ if isinstance(self.k, bool) or not isinstance(self.k, int) or self.k < 1:
95
+ raise ValueError("KNN k must be a positive integer")
96
+ if not _finite(self.distance_power) or self.distance_power <= 0:
97
+ raise ValueError("KNN distance_power must be finite and positive")
98
+ _validate_abstention(self.min_gain, self.margin)
99
+ object.__setattr__(self, "training_rows", rows)
100
+ object.__setattr__(self, "release_by_family", MappingProxyType(releases))
101
+
102
+ @property
103
+ def model_hash(self) -> str:
104
+ return content_hash(
105
+ {
106
+ "selector": "knn_utility",
107
+ "training_rows": [
108
+ {
109
+ "row_hash": row.row_hash,
110
+ "feature_vector": row.feature_vector,
111
+ "gains": {family.value: value for family, value in row.gains.items()},
112
+ }
113
+ for row in self.training_rows
114
+ ],
115
+ "release_by_family": {
116
+ family.value: release for family, release in self.release_by_family.items()
117
+ },
118
+ "k": self.k,
119
+ "distance_power": self.distance_power,
120
+ "min_gain": self.min_gain,
121
+ "margin": self.margin,
122
+ }
123
+ )
124
+
125
+ def predict_gains(self, features: SelectionFeatures) -> dict[SemanticFamily, float]:
126
+ target = features.as_vector()
127
+ predictions: dict[SemanticFamily, float] = {}
128
+ for family in self.release_by_family:
129
+ distances = sorted(
130
+ (
131
+ (_euclidean(target, row.feature_vector), row.row_hash, row.gains[family])
132
+ for row in self.training_rows
133
+ if family in row.gains
134
+ ),
135
+ key=lambda item: (item[0], item[1]),
136
+ )
137
+ exact = [gain for distance, _, gain in distances if distance <= 1e-12]
138
+ if exact:
139
+ predictions[family] = fmean(exact)
140
+ continue
141
+ neighbors = distances[: min(self.k, len(distances))]
142
+ weights = [1.0 / (distance**self.distance_power) for distance, _, _ in neighbors]
143
+ predictions[family] = math.fsum(
144
+ weight * item[2] for weight, item in zip(weights, neighbors, strict=True)
145
+ ) / math.fsum(weights)
146
+ return predictions
147
+
148
+ def select(
149
+ self,
150
+ state: FrozenAgentState,
151
+ features: SelectionFeatures,
152
+ *,
153
+ eligible_families: Collection[SemanticFamily] | None = None,
154
+ ) -> SelectionDecision:
155
+ del state
156
+ return _decision_from_gains(
157
+ self.predict_gains(features),
158
+ self.release_by_family,
159
+ eligible_families=eligible_families,
160
+ min_gain=self.min_gain,
161
+ margin=self.margin,
162
+ label="KNN utility",
163
+ )
164
+
165
+
166
+ def fit_knn_selector(
167
+ examples: Sequence[SelectorExample],
168
+ *,
169
+ release_by_family: Mapping[SemanticFamily, str],
170
+ k: int = 5,
171
+ distance_power: float = 2.0,
172
+ ) -> KNNUtilitySelector:
173
+ """Fit an identity-free KNN utility model on discovery ``train`` rows."""
174
+
175
+ rows = _training_rows(examples, label="KNN selector")
176
+ releases = _validated_release_mapping(release_by_family)
177
+ frozen_rows: list[KNNTrainingRow] = []
178
+ for example in rows:
179
+ no_op = example.utilities[SemanticFamily.NO_OP]
180
+ gains = {
181
+ family: example.utilities[family] - no_op
182
+ for family in releases
183
+ if family in example.utilities
184
+ }
185
+ if gains:
186
+ frozen_rows.append(KNNTrainingRow(example.features.as_vector(), gains))
187
+ frozen_rows.sort(key=lambda row: row.row_hash)
188
+ return KNNUtilitySelector(
189
+ tuple(frozen_rows),
190
+ releases,
191
+ k=k,
192
+ distance_power=distance_power,
193
+ )
194
+
195
+
196
+ @dataclass(frozen=True, slots=True)
197
+ class LogisticTLearnerSelector:
198
+ """Penalized logistic T-learner over full-information potential outcomes."""
199
+
200
+ outcome_coefficients: Mapping[SemanticFamily, tuple[float, ...]]
201
+ release_by_family: Mapping[SemanticFamily, str]
202
+ l2: float
203
+ learning_rate: float
204
+ iterations: int
205
+ min_gain: float = 0.0
206
+ margin: float = 0.0
207
+
208
+ def __post_init__(self) -> None:
209
+ coefficients = {
210
+ family: tuple(values) for family, values in self.outcome_coefficients.items()
211
+ }
212
+ if SemanticFamily.NO_OP not in coefficients:
213
+ raise ValueError("logistic T-learner requires an explicit NO_OP outcome model")
214
+ if any(family not in {*_ACTIONS, SemanticFamily.NO_OP} for family in coefficients):
215
+ raise ValueError("logistic T-learner coefficient keys must be selector actions")
216
+ expected = _FEATURE_WIDTH + 1
217
+ if any(
218
+ len(values) != expected or any(not _finite(value) for value in values)
219
+ for values in coefficients.values()
220
+ ):
221
+ raise ValueError(
222
+ f"logistic T-learner coefficient vectors must contain {expected} finite values"
223
+ )
224
+ releases = _validated_release_mapping(self.release_by_family)
225
+ modeled = set(coefficients) - {SemanticFamily.NO_OP}
226
+ if modeled != set(releases):
227
+ raise ValueError("logistic T-learner models and release mappings must match")
228
+ _validate_logistic_config(self.l2, self.learning_rate, self.iterations)
229
+ _validate_abstention(self.min_gain, self.margin)
230
+ object.__setattr__(self, "outcome_coefficients", MappingProxyType(coefficients))
231
+ object.__setattr__(self, "release_by_family", MappingProxyType(releases))
232
+
233
+ @property
234
+ def model_hash(self) -> str:
235
+ return content_hash(
236
+ {
237
+ "selector": "logistic_t_learner",
238
+ "outcome_coefficients": {
239
+ family.value: values for family, values in self.outcome_coefficients.items()
240
+ },
241
+ "release_by_family": {
242
+ family.value: release for family, release in self.release_by_family.items()
243
+ },
244
+ "fit": {
245
+ "l2": self.l2,
246
+ "learning_rate": self.learning_rate,
247
+ "iterations": self.iterations,
248
+ },
249
+ "min_gain": self.min_gain,
250
+ "margin": self.margin,
251
+ }
252
+ )
253
+
254
+ def predict_outcomes(self, features: SelectionFeatures) -> dict[SemanticFamily, float]:
255
+ vector = (1.0, *features.as_vector())
256
+ return {
257
+ family: _sigmoid(
258
+ math.fsum(weight * value for weight, value in zip(weights, vector, strict=True))
259
+ )
260
+ for family, weights in self.outcome_coefficients.items()
261
+ }
262
+
263
+ def predict_gains(self, features: SelectionFeatures) -> dict[SemanticFamily, float]:
264
+ outcomes = self.predict_outcomes(features)
265
+ no_op = outcomes[SemanticFamily.NO_OP]
266
+ return {family: outcomes[family] - no_op for family in self.release_by_family}
267
+
268
+ def select(
269
+ self,
270
+ state: FrozenAgentState,
271
+ features: SelectionFeatures,
272
+ *,
273
+ eligible_families: Collection[SemanticFamily] | None = None,
274
+ ) -> SelectionDecision:
275
+ del state
276
+ return _decision_from_gains(
277
+ self.predict_gains(features),
278
+ self.release_by_family,
279
+ eligible_families=eligible_families,
280
+ min_gain=self.min_gain,
281
+ margin=self.margin,
282
+ label="logistic T-learner",
283
+ )
284
+
285
+
286
+ def fit_logistic_t_learner(
287
+ examples: Sequence[SelectorExample],
288
+ *,
289
+ release_by_family: Mapping[SemanticFamily, str],
290
+ l2: float = 1e-3,
291
+ learning_rate: float = 0.2,
292
+ iterations: int = 600,
293
+ ) -> LogisticTLearnerSelector:
294
+ """Fit one outcome model per action using discovery ``train`` rows only."""
295
+
296
+ rows = _training_rows(examples, label="logistic T-learner")
297
+ releases = _validated_release_mapping(release_by_family)
298
+ _validate_logistic_config(l2, learning_rate, iterations)
299
+ actions = (SemanticFamily.NO_OP, *releases)
300
+ coefficients: dict[SemanticFamily, tuple[float, ...]] = {}
301
+ for family in actions:
302
+ supported = tuple(example for example in rows if family in example.utilities)
303
+ if not supported:
304
+ raise ValueError(f"logistic T-learner has no outcomes for {family.value}")
305
+ labels = tuple(float(example.utilities[family]) for example in supported)
306
+ if any(not 0 <= label <= 1 for label in labels):
307
+ raise ValueError("logistic T-learner utilities must be probabilities in [0, 1]")
308
+ matrix = tuple((1.0, *example.features.as_vector()) for example in supported)
309
+ coefficients[family] = _fit_logistic(
310
+ matrix,
311
+ labels,
312
+ l2=l2,
313
+ learning_rate=learning_rate,
314
+ iterations=iterations,
315
+ )
316
+ return LogisticTLearnerSelector(
317
+ coefficients,
318
+ releases,
319
+ l2=l2,
320
+ learning_rate=learning_rate,
321
+ iterations=iterations,
322
+ )
323
+
324
+
325
+ @dataclass(frozen=True, slots=True)
326
+ class RidgeTLearnerSelector:
327
+ """Linear T-learner with one ridge potential-outcome model per action."""
328
+
329
+ outcome_coefficients: Mapping[SemanticFamily, tuple[float, ...]]
330
+ release_by_family: Mapping[SemanticFamily, str]
331
+ ridge: float = 1e-3
332
+ min_gain: float = 0.0
333
+ margin: float = 0.0
334
+
335
+ def __post_init__(self) -> None:
336
+ coefficients = {
337
+ family: tuple(values) for family, values in self.outcome_coefficients.items()
338
+ }
339
+ if SemanticFamily.NO_OP not in coefficients:
340
+ raise ValueError("ridge T-learner requires an explicit NO_OP outcome model")
341
+ if any(family not in {*_ACTIONS, SemanticFamily.NO_OP} for family in coefficients):
342
+ raise ValueError("ridge T-learner coefficient keys must be selector actions")
343
+ expected = _FEATURE_WIDTH + 1
344
+ if any(
345
+ len(values) != expected or any(not _finite(value) for value in values)
346
+ for values in coefficients.values()
347
+ ):
348
+ raise ValueError(
349
+ f"ridge T-learner coefficient vectors must contain {expected} finite values"
350
+ )
351
+ releases = _validated_release_mapping(self.release_by_family)
352
+ if set(coefficients) - {SemanticFamily.NO_OP} != set(releases):
353
+ raise ValueError("ridge T-learner models and release mappings must match")
354
+ if not _finite(self.ridge) or self.ridge < 0:
355
+ raise ValueError("ridge T-learner penalty must be finite and non-negative")
356
+ _validate_abstention(self.min_gain, self.margin)
357
+ object.__setattr__(self, "outcome_coefficients", MappingProxyType(coefficients))
358
+ object.__setattr__(self, "release_by_family", MappingProxyType(releases))
359
+
360
+ @property
361
+ def model_hash(self) -> str:
362
+ return content_hash(
363
+ {
364
+ "selector": "ridge_t_learner",
365
+ "outcome_coefficients": {
366
+ family.value: values for family, values in self.outcome_coefficients.items()
367
+ },
368
+ "release_by_family": {
369
+ family.value: release for family, release in self.release_by_family.items()
370
+ },
371
+ "ridge": self.ridge,
372
+ "min_gain": self.min_gain,
373
+ "margin": self.margin,
374
+ }
375
+ )
376
+
377
+ def predict_outcomes(self, features: SelectionFeatures) -> dict[SemanticFamily, float]:
378
+ vector = (1.0, *features.as_vector())
379
+ return {
380
+ family: math.fsum(weight * value for weight, value in zip(weights, vector, strict=True))
381
+ for family, weights in self.outcome_coefficients.items()
382
+ }
383
+
384
+ def predict_gains(self, features: SelectionFeatures) -> dict[SemanticFamily, float]:
385
+ outcomes = self.predict_outcomes(features)
386
+ no_op = outcomes[SemanticFamily.NO_OP]
387
+ return {family: outcomes[family] - no_op for family in self.release_by_family}
388
+
389
+ def select(
390
+ self,
391
+ state: FrozenAgentState,
392
+ features: SelectionFeatures,
393
+ *,
394
+ eligible_families: Collection[SemanticFamily] | None = None,
395
+ ) -> SelectionDecision:
396
+ del state
397
+ return _decision_from_gains(
398
+ self.predict_gains(features),
399
+ self.release_by_family,
400
+ eligible_families=eligible_families,
401
+ min_gain=self.min_gain,
402
+ margin=self.margin,
403
+ label="ridge T-learner",
404
+ )
405
+
406
+
407
+ def fit_ridge_t_learner(
408
+ examples: Sequence[SelectorExample],
409
+ *,
410
+ release_by_family: Mapping[SemanticFamily, str],
411
+ ridge: float = 1e-3,
412
+ ) -> RidgeTLearnerSelector:
413
+ """Fit per-action ridge potential outcomes on discovery ``train`` rows."""
414
+
415
+ rows = _training_rows(examples, label="ridge T-learner")
416
+ releases = _validated_release_mapping(release_by_family)
417
+ if not _finite(ridge) or ridge < 0:
418
+ raise ValueError("ridge T-learner penalty must be finite and non-negative")
419
+ coefficients: dict[SemanticFamily, tuple[float, ...]] = {}
420
+ for family in (SemanticFamily.NO_OP, *releases):
421
+ supported = tuple(example for example in rows if family in example.utilities)
422
+ if not supported:
423
+ raise ValueError(f"ridge T-learner has no outcomes for {family.value}")
424
+ matrix = tuple((1.0, *example.features.as_vector()) for example in supported)
425
+ labels = tuple(float(example.utilities[family]) for example in supported)
426
+ coefficients[family] = _fit_ridge(matrix, labels, ridge=ridge)
427
+ return RidgeTLearnerSelector(coefficients, releases, ridge=ridge)
428
+
429
+
430
+ @dataclass(frozen=True, slots=True)
431
+ class FrozenRegressionTree:
432
+ """Minimal immutable regression tree extracted from a fitted sklearn tree."""
433
+
434
+ children_left: tuple[int, ...]
435
+ children_right: tuple[int, ...]
436
+ features: tuple[int, ...]
437
+ thresholds: tuple[float, ...]
438
+ values: tuple[float, ...]
439
+
440
+ def __post_init__(self) -> None:
441
+ left = tuple(self.children_left)
442
+ right = tuple(self.children_right)
443
+ features = tuple(self.features)
444
+ thresholds = tuple(self.thresholds)
445
+ values = tuple(self.values)
446
+ size = len(left)
447
+ if size < 1 or any(len(items) != size for items in (right, features, thresholds, values)):
448
+ raise ValueError("frozen regression tree arrays must have one non-zero length")
449
+ if any(type(value) is not int for value in (*left, *right, *features)):
450
+ raise ValueError("frozen regression tree indices must be integers")
451
+ if any(not _finite(value) for value in (*thresholds, *values)):
452
+ raise ValueError("frozen regression tree thresholds and values must be finite")
453
+ parents = [0] * size
454
+ for index, (left_child, right_child, feature) in enumerate(
455
+ zip(left, right, features, strict=True)
456
+ ):
457
+ if left_child == -1 and right_child == -1:
458
+ if feature != -2:
459
+ raise ValueError("frozen regression tree leaves require feature sentinel -2")
460
+ continue
461
+ if left_child == -1 or right_child == -1 or left_child == right_child:
462
+ raise ValueError("frozen regression tree nodes require two distinct children")
463
+ if not (index < left_child < size and index < right_child < size):
464
+ raise ValueError("frozen regression tree child indices must form an acyclic tree")
465
+ if not 0 <= feature < _FEATURE_WIDTH:
466
+ raise ValueError("frozen regression tree feature index is out of range")
467
+ parents[left_child] += 1
468
+ parents[right_child] += 1
469
+ if parents[0] != 0 or any(parent_count != 1 for parent_count in parents[1:]):
470
+ raise ValueError("frozen regression tree nodes must form one connected tree")
471
+ object.__setattr__(self, "children_left", left)
472
+ object.__setattr__(self, "children_right", right)
473
+ object.__setattr__(self, "features", features)
474
+ object.__setattr__(self, "thresholds", tuple(float(value) for value in thresholds))
475
+ object.__setattr__(self, "values", tuple(float(value) for value in values))
476
+
477
+ def predict(self, features: Sequence[float]) -> float:
478
+ if len(features) != _FEATURE_WIDTH or any(not _finite(value) for value in features):
479
+ raise ValueError("tree prediction requires one finite selector feature vector")
480
+ node = 0
481
+ while self.children_left[node] != -1:
482
+ node = (
483
+ self.children_left[node]
484
+ if features[self.features[node]] <= self.thresholds[node]
485
+ else self.children_right[node]
486
+ )
487
+ return self.values[node]
488
+
489
+ def to_artifact(self) -> dict[str, Any]:
490
+ return {
491
+ "children_left": list(self.children_left),
492
+ "children_right": list(self.children_right),
493
+ "features": list(self.features),
494
+ "thresholds": list(self.thresholds),
495
+ "values": list(self.values),
496
+ }
497
+
498
+ @classmethod
499
+ def from_artifact(cls, value: Mapping[str, Any]) -> FrozenRegressionTree:
500
+ try:
501
+ artifact = _strict_artifact_mapping(
502
+ value,
503
+ expected_keys={
504
+ "children_left",
505
+ "children_right",
506
+ "features",
507
+ "thresholds",
508
+ "values",
509
+ },
510
+ label="frozen regression tree",
511
+ )
512
+ return cls(
513
+ tuple(
514
+ _strict_json_integer(item, label="children_left item")
515
+ for item in _strict_json_array(
516
+ artifact["children_left"],
517
+ label="children_left",
518
+ )
519
+ ),
520
+ tuple(
521
+ _strict_json_integer(item, label="children_right item")
522
+ for item in _strict_json_array(
523
+ artifact["children_right"],
524
+ label="children_right",
525
+ )
526
+ ),
527
+ tuple(
528
+ _strict_json_integer(item, label="feature item")
529
+ for item in _strict_json_array(artifact["features"], label="features")
530
+ ),
531
+ tuple(
532
+ _strict_json_number(item, label="threshold item")
533
+ for item in _strict_json_array(
534
+ artifact["thresholds"],
535
+ label="thresholds",
536
+ )
537
+ ),
538
+ tuple(
539
+ _strict_json_number(item, label="tree value")
540
+ for item in _strict_json_array(artifact["values"], label="values")
541
+ ),
542
+ )
543
+ except (KeyError, TypeError, ValueError) as exc:
544
+ raise ValueError("invalid frozen regression tree artifact") from exc
545
+
546
+
547
+ @dataclass(frozen=True, slots=True)
548
+ class FrozenGBDTOutcomeModel:
549
+ initial_value: float
550
+ learning_rate: float
551
+ trees: tuple[FrozenRegressionTree, ...]
552
+
553
+ def __post_init__(self) -> None:
554
+ trees = tuple(self.trees)
555
+ if not _finite(self.initial_value):
556
+ raise ValueError("GBDT initial value must be finite")
557
+ if not _finite(self.learning_rate) or self.learning_rate <= 0:
558
+ raise ValueError("GBDT learning rate must be finite and positive")
559
+ if not trees or any(not isinstance(tree, FrozenRegressionTree) for tree in trees):
560
+ raise ValueError("GBDT outcome model requires frozen regression trees")
561
+ object.__setattr__(self, "trees", trees)
562
+
563
+ def predict(self, features: Sequence[float]) -> float:
564
+ return self.initial_value + self.learning_rate * math.fsum(
565
+ tree.predict(features) for tree in self.trees
566
+ )
567
+
568
+ def to_artifact(self) -> dict[str, Any]:
569
+ return {
570
+ "initial_value": self.initial_value,
571
+ "learning_rate": self.learning_rate,
572
+ "trees": [tree.to_artifact() for tree in self.trees],
573
+ }
574
+
575
+ @classmethod
576
+ def from_artifact(cls, value: Mapping[str, Any]) -> FrozenGBDTOutcomeModel:
577
+ try:
578
+ artifact = _strict_artifact_mapping(
579
+ value,
580
+ expected_keys={"initial_value", "learning_rate", "trees"},
581
+ label="frozen GBDT outcome",
582
+ )
583
+ trees = tuple(
584
+ FrozenRegressionTree.from_artifact(item)
585
+ for item in _strict_json_array(artifact["trees"], label="trees")
586
+ )
587
+ return cls(
588
+ _strict_json_number(artifact["initial_value"], label="initial_value"),
589
+ _strict_json_number(artifact["learning_rate"], label="learning_rate"),
590
+ trees,
591
+ )
592
+ except (KeyError, TypeError, ValueError) as exc:
593
+ raise ValueError("invalid frozen GBDT outcome artifact") from exc
594
+
595
+
596
+ @dataclass(frozen=True, slots=True)
597
+ class GBDTTLearnerSelector:
598
+ """Pure-Python inference for frozen sklearn GBDT T-learner artifacts."""
599
+
600
+ outcome_models: Mapping[SemanticFamily, FrozenGBDTOutcomeModel]
601
+ release_by_family: Mapping[SemanticFamily, str]
602
+ sklearn_version: str
603
+ n_estimators: int
604
+ max_depth: int
605
+ min_samples_leaf: int
606
+ random_state: int
607
+ min_gain: float = 0.0
608
+ margin: float = 0.0
609
+ train_id_set_digest: str = _EMPTY_TRAIN_ID_SET_DIGEST
610
+ train_example_count: int = 0
611
+
612
+ def __post_init__(self) -> None:
613
+ models = dict(self.outcome_models)
614
+ if SemanticFamily.NO_OP not in models:
615
+ raise ValueError("GBDT T-learner requires an explicit NO_OP outcome model")
616
+ if any(
617
+ family not in {*_ACTIONS, SemanticFamily.NO_OP}
618
+ or not isinstance(model, FrozenGBDTOutcomeModel)
619
+ for family, model in models.items()
620
+ ):
621
+ raise ValueError("GBDT outcome models require typed selector actions")
622
+ releases = _validated_release_mapping(self.release_by_family)
623
+ if set(models) - {SemanticFamily.NO_OP} != set(releases):
624
+ raise ValueError("GBDT T-learner models and release mappings must match")
625
+ if not isinstance(self.sklearn_version, str) or not self.sklearn_version.strip():
626
+ raise ValueError("GBDT artifact requires the sklearn fit version")
627
+ for name, value in (
628
+ ("n_estimators", self.n_estimators),
629
+ ("max_depth", self.max_depth),
630
+ ("min_samples_leaf", self.min_samples_leaf),
631
+ ):
632
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
633
+ raise ValueError(f"GBDT {name} must be a positive integer")
634
+ if any(len(model.trees) != self.n_estimators for model in models.values()):
635
+ raise ValueError("GBDT outcome models must match the declared n_estimators")
636
+ if isinstance(self.random_state, bool) or not isinstance(self.random_state, int):
637
+ raise ValueError("GBDT random_state must be an integer")
638
+ if not _valid_sha256(self.train_id_set_digest):
639
+ raise ValueError("GBDT train_id_set_digest must be a lowercase SHA-256 digest")
640
+ if (
641
+ isinstance(self.train_example_count, bool)
642
+ or not isinstance(self.train_example_count, int)
643
+ or self.train_example_count < 0
644
+ ):
645
+ raise ValueError("GBDT train_example_count must be a non-negative integer")
646
+ _validate_abstention(self.min_gain, self.margin)
647
+ object.__setattr__(self, "outcome_models", MappingProxyType(models))
648
+ object.__setattr__(self, "release_by_family", MappingProxyType(releases))
649
+
650
+ @property
651
+ def model_hash(self) -> str:
652
+ return content_hash(self.to_artifact())
653
+
654
+ def predict_outcomes(self, features: SelectionFeatures) -> dict[SemanticFamily, float]:
655
+ vector = features.as_vector()
656
+ return {family: model.predict(vector) for family, model in self.outcome_models.items()}
657
+
658
+ def predict_gains(self, features: SelectionFeatures) -> dict[SemanticFamily, float]:
659
+ outcomes = self.predict_outcomes(features)
660
+ no_op = outcomes[SemanticFamily.NO_OP]
661
+ return {family: outcomes[family] - no_op for family in self.release_by_family}
662
+
663
+ def select(
664
+ self,
665
+ state: FrozenAgentState,
666
+ features: SelectionFeatures,
667
+ *,
668
+ eligible_families: Collection[SemanticFamily] | None = None,
669
+ ) -> SelectionDecision:
670
+ del state
671
+ return _decision_from_gains(
672
+ self.predict_gains(features),
673
+ self.release_by_family,
674
+ eligible_families=eligible_families,
675
+ min_gain=self.min_gain,
676
+ margin=self.margin,
677
+ label="GBDT T-learner",
678
+ )
679
+
680
+ def to_artifact(self) -> dict[str, Any]:
681
+ return {
682
+ "schema_version": 2,
683
+ "selector_kind": "gbdt_t_learner",
684
+ "outcome_models": {
685
+ family.value: model.to_artifact() for family, model in self.outcome_models.items()
686
+ },
687
+ "release_by_family": {
688
+ family.value: release for family, release in self.release_by_family.items()
689
+ },
690
+ "fit": {
691
+ "sklearn_version": self.sklearn_version,
692
+ "n_estimators": self.n_estimators,
693
+ "max_depth": self.max_depth,
694
+ "min_samples_leaf": self.min_samples_leaf,
695
+ "random_state": self.random_state,
696
+ },
697
+ "abstention": {"min_gain": self.min_gain, "margin": self.margin},
698
+ "provenance": {
699
+ "train_id_set_digest": self.train_id_set_digest,
700
+ "train_example_count": self.train_example_count,
701
+ },
702
+ }
703
+
704
+ @classmethod
705
+ def from_artifact(cls, value: Mapping[str, Any]) -> GBDTTLearnerSelector:
706
+ try:
707
+ artifact = _strict_artifact_mapping(
708
+ value,
709
+ expected_keys={
710
+ "schema_version",
711
+ "selector_kind",
712
+ "outcome_models",
713
+ "release_by_family",
714
+ "fit",
715
+ "abstention",
716
+ "provenance",
717
+ },
718
+ label="GBDT selector",
719
+ )
720
+ schema_version = _strict_json_integer(
721
+ artifact["schema_version"],
722
+ label="schema_version",
723
+ )
724
+ if (
725
+ schema_version != 2
726
+ or artifact["selector_kind"] != "gbdt_t_learner"
727
+ or not isinstance(artifact["selector_kind"], str)
728
+ ):
729
+ raise ValueError("unsupported GBDT selector artifact schema")
730
+ raw_models = _strict_string_keyed_mapping(
731
+ artifact["outcome_models"],
732
+ label="outcome_models",
733
+ )
734
+ raw_releases = _strict_string_keyed_mapping(
735
+ artifact["release_by_family"],
736
+ label="release_by_family",
737
+ )
738
+ fit = _strict_artifact_mapping(
739
+ artifact["fit"],
740
+ expected_keys={
741
+ "sklearn_version",
742
+ "n_estimators",
743
+ "max_depth",
744
+ "min_samples_leaf",
745
+ "random_state",
746
+ },
747
+ label="GBDT fit",
748
+ )
749
+ abstention = _strict_artifact_mapping(
750
+ artifact["abstention"],
751
+ expected_keys={"min_gain", "margin"},
752
+ label="GBDT abstention",
753
+ )
754
+ provenance = _strict_artifact_mapping(
755
+ artifact["provenance"],
756
+ expected_keys={"train_id_set_digest", "train_example_count"},
757
+ label="GBDT provenance",
758
+ )
759
+ models: dict[SemanticFamily, FrozenGBDTOutcomeModel] = {}
760
+ for family, model in raw_models.items():
761
+ if not isinstance(model, Mapping):
762
+ raise TypeError("GBDT outcome model entries must be mappings")
763
+ models[SemanticFamily(family)] = FrozenGBDTOutcomeModel.from_artifact(model)
764
+ releases: dict[SemanticFamily, str] = {}
765
+ for family, release in raw_releases.items():
766
+ if not isinstance(release, str) or not release.strip():
767
+ raise TypeError("GBDT release IDs must be non-empty strings")
768
+ releases[SemanticFamily(family)] = release
769
+ sklearn_version = fit["sklearn_version"]
770
+ if not isinstance(sklearn_version, str) or not sklearn_version.strip():
771
+ raise TypeError("GBDT sklearn_version must be a non-empty string")
772
+ train_id_set_digest = provenance["train_id_set_digest"]
773
+ if not isinstance(train_id_set_digest, str):
774
+ raise TypeError("GBDT train_id_set_digest must be a string")
775
+ return cls(
776
+ models,
777
+ releases,
778
+ sklearn_version=sklearn_version,
779
+ n_estimators=_strict_json_integer(
780
+ fit["n_estimators"],
781
+ label="n_estimators",
782
+ ),
783
+ max_depth=_strict_json_integer(fit["max_depth"], label="max_depth"),
784
+ min_samples_leaf=_strict_json_integer(
785
+ fit["min_samples_leaf"],
786
+ label="min_samples_leaf",
787
+ ),
788
+ random_state=_strict_json_integer(
789
+ fit["random_state"],
790
+ label="random_state",
791
+ ),
792
+ min_gain=_strict_json_number(
793
+ abstention["min_gain"],
794
+ label="min_gain",
795
+ ),
796
+ margin=_strict_json_number(abstention["margin"], label="margin"),
797
+ train_id_set_digest=train_id_set_digest,
798
+ train_example_count=_strict_json_integer(
799
+ provenance["train_example_count"],
800
+ label="train_example_count",
801
+ ),
802
+ )
803
+ except (KeyError, TypeError, ValueError) as exc:
804
+ raise ValueError("invalid GBDT selector artifact") from exc
805
+
806
+
807
+ def fit_gbdt_t_learner(
808
+ examples: Sequence[SelectorExample],
809
+ *,
810
+ release_by_family: Mapping[SemanticFamily, str],
811
+ n_estimators: int = 50,
812
+ learning_rate: float = 0.05,
813
+ max_depth: int = 2,
814
+ min_samples_leaf: int = 2,
815
+ random_state: int = 0,
816
+ ) -> GBDTTLearnerSelector:
817
+ """Fit sklearn GBDTs lazily, then detach into safe pure-Python artifacts."""
818
+
819
+ try:
820
+ import sklearn
821
+ from sklearn.ensemble import GradientBoostingRegressor
822
+ except ImportError as exc: # pragma: no cover - exercised in a clean optional-dep env
823
+ raise ImportError(
824
+ "GBDT selector fitting requires `pip install agentinputlab[selectors]`"
825
+ ) from exc
826
+ rows = _training_rows(examples, label="GBDT T-learner")
827
+ releases = _validated_release_mapping(release_by_family)
828
+ for name, value in (
829
+ ("n_estimators", n_estimators),
830
+ ("max_depth", max_depth),
831
+ ("min_samples_leaf", min_samples_leaf),
832
+ ):
833
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
834
+ raise ValueError(f"GBDT {name} must be a positive integer")
835
+ if not _finite(learning_rate) or learning_rate <= 0:
836
+ raise ValueError("GBDT learning_rate must be finite and positive")
837
+ if isinstance(random_state, bool) or not isinstance(random_state, int):
838
+ raise ValueError("GBDT random_state must be an integer")
839
+ models: dict[SemanticFamily, FrozenGBDTOutcomeModel] = {}
840
+ for family in (SemanticFamily.NO_OP, *releases):
841
+ supported = tuple(example for example in rows if family in example.utilities)
842
+ if len(supported) < 2:
843
+ raise ValueError(f"GBDT T-learner needs at least two outcomes for {family.value}")
844
+ estimator = GradientBoostingRegressor(
845
+ n_estimators=n_estimators,
846
+ learning_rate=learning_rate,
847
+ max_depth=max_depth,
848
+ min_samples_leaf=min_samples_leaf,
849
+ random_state=random_state,
850
+ loss="squared_error",
851
+ )
852
+ estimator.fit(
853
+ [example.features.as_vector() for example in supported],
854
+ [float(example.utilities[family]) for example in supported],
855
+ )
856
+ models[family] = _freeze_sklearn_gbdt(estimator)
857
+ return GBDTTLearnerSelector(
858
+ models,
859
+ releases,
860
+ sklearn_version=str(sklearn.__version__),
861
+ n_estimators=n_estimators,
862
+ max_depth=max_depth,
863
+ min_samples_leaf=min_samples_leaf,
864
+ random_state=random_state,
865
+ train_id_set_digest=_selector_id_set_digest(rows, required_split="train"),
866
+ train_example_count=len(rows),
867
+ )
868
+
869
+
870
+ @dataclass(frozen=True, slots=True)
871
+ class CostRiskAwareSelector:
872
+ """Two-stage wrapper that applies frozen cost and hard-risk guardrails."""
873
+
874
+ base_selector: UtilityGainProvider
875
+ release_by_family: Mapping[SemanticFamily, str]
876
+ cost_by_family: Mapping[SemanticFamily, float]
877
+ risk_by_family: Mapping[SemanticFamily, float]
878
+ max_cost: float
879
+ max_risk: float
880
+ cost_weight: float = 0.0
881
+ risk_weight: float = 0.0
882
+ min_gain: float = 0.0
883
+ margin: float = 0.0
884
+
885
+ def __post_init__(self) -> None:
886
+ if not isinstance(self.base_selector, UtilityGainProvider):
887
+ raise ValueError(
888
+ "cost/risk wrapper requires an explicit UtilityGainProvider base model"
889
+ )
890
+ _selector_hash(self.base_selector)
891
+ releases = _validated_release_mapping(self.release_by_family)
892
+ declared_releases = getattr(self.base_selector, "release_by_family", None)
893
+ if isinstance(declared_releases, Mapping) and set(declared_releases) != set(releases):
894
+ raise ValueError("utility-gain provider and guardrail release families must match")
895
+ costs = _validated_family_values(
896
+ self.cost_by_family,
897
+ families=set(releases),
898
+ label="cost",
899
+ upper=None,
900
+ )
901
+ risks = _validated_family_values(
902
+ self.risk_by_family,
903
+ families=set(releases),
904
+ label="risk",
905
+ upper=1.0,
906
+ )
907
+ if not _finite(self.max_cost) or self.max_cost < 0:
908
+ raise ValueError("max_cost must be finite and non-negative")
909
+ if not _finite(self.max_risk) or not 0 <= self.max_risk <= 1:
910
+ raise ValueError("max_risk must be finite and in [0, 1]")
911
+ if not _finite(self.cost_weight) or self.cost_weight < 0:
912
+ raise ValueError("cost_weight must be finite and non-negative")
913
+ if not _finite(self.risk_weight) or self.risk_weight < 0:
914
+ raise ValueError("risk_weight must be finite and non-negative")
915
+ _validate_abstention(self.min_gain, self.margin)
916
+ object.__setattr__(self, "release_by_family", MappingProxyType(releases))
917
+ object.__setattr__(self, "cost_by_family", MappingProxyType(costs))
918
+ object.__setattr__(self, "risk_by_family", MappingProxyType(risks))
919
+
920
+ @property
921
+ def model_hash(self) -> str:
922
+ return content_hash(
923
+ {
924
+ "selector": "cost_risk_aware",
925
+ "base_selector_hash": _selector_hash(self.base_selector),
926
+ "release_by_family": {
927
+ family.value: release for family, release in self.release_by_family.items()
928
+ },
929
+ "cost_by_family": {
930
+ family.value: value for family, value in self.cost_by_family.items()
931
+ },
932
+ "risk_by_family": {
933
+ family.value: value for family, value in self.risk_by_family.items()
934
+ },
935
+ "guards": {
936
+ "max_cost": self.max_cost,
937
+ "max_risk": self.max_risk,
938
+ "cost_weight": self.cost_weight,
939
+ "risk_weight": self.risk_weight,
940
+ "min_gain": self.min_gain,
941
+ "margin": self.margin,
942
+ },
943
+ }
944
+ )
945
+
946
+ def select(
947
+ self,
948
+ state: FrozenAgentState,
949
+ features: SelectionFeatures,
950
+ *,
951
+ eligible_families: Collection[SemanticFamily] | None = None,
952
+ ) -> SelectionDecision:
953
+ eligible = _eligible_actions(eligible_families) & set(self.release_by_family)
954
+ base = self.base_selector.select(
955
+ state,
956
+ features,
957
+ eligible_families=eligible,
958
+ )
959
+ gains = _validated_predicted_gains(
960
+ self.base_selector.predict_gains(features),
961
+ expected_families=set(self.release_by_family),
962
+ )
963
+ adjusted: dict[SemanticFamily, float] = {}
964
+ safe: dict[SemanticFamily, float] = {}
965
+ for family in self.release_by_family:
966
+ raw = gains[family]
967
+ value = (
968
+ raw
969
+ - self.cost_weight * self.cost_by_family[family]
970
+ - self.risk_weight * self.risk_by_family[family]
971
+ )
972
+ adjusted[family] = value
973
+ if (
974
+ family in eligible
975
+ and self.cost_by_family[family] <= self.max_cost
976
+ and self.risk_by_family[family] <= self.max_risk
977
+ ):
978
+ safe[family] = value
979
+ scores = {family.value: value for family, value in adjusted.items()}
980
+ scores[SemanticFamily.NO_OP.value] = 0.0
981
+ if base.family is SemanticFamily.NO_OP:
982
+ return _no_op("base selector abstained before safety guards", scores)
983
+ if not safe:
984
+ return _no_op("no eligible family passed cost/risk guards", scores)
985
+ ordered = sorted(safe.items(), key=lambda item: (-item[1], item[0].value))
986
+ family, gain = ordered[0]
987
+ runner_up = max(0.0, ordered[1][1] if len(ordered) > 1 else 0.0)
988
+ if gain <= self.min_gain or gain - runner_up <= self.margin:
989
+ return _no_op("adjusted gain or margin gate not met", scores)
990
+ return SelectionDecision(
991
+ family=family,
992
+ release_id=self.release_by_family[family],
993
+ selection_score=gain,
994
+ rationale=f"selected cost/risk-adjusted utility winner: {family.value}",
995
+ scores=scores,
996
+ )
997
+
998
+
999
+ def fit_cost_risk_selector(
1000
+ base_selector: UtilityGainProvider,
1001
+ examples: Sequence[SelectorExample],
1002
+ *,
1003
+ release_by_family: Mapping[SemanticFamily, str],
1004
+ max_cost: float,
1005
+ max_risk: float,
1006
+ cost_weight: float = 0.0,
1007
+ risk_weight: float = 0.0,
1008
+ ) -> CostRiskAwareSelector:
1009
+ """Estimate static family cost/risk priors from discovery ``train`` rows."""
1010
+
1011
+ if not isinstance(base_selector, UtilityGainProvider):
1012
+ raise ValueError("cost/risk wrapper requires an explicit UtilityGainProvider base model")
1013
+ rows = _training_rows(examples, label="cost/risk selector")
1014
+ releases = _validated_release_mapping(release_by_family)
1015
+ costs: dict[SemanticFamily, float] = {}
1016
+ risks: dict[SemanticFamily, float] = {}
1017
+ for family in releases:
1018
+ family_costs = [example.costs[family] for example in rows if family in example.costs]
1019
+ family_risks = [example.risks[family] for example in rows if family in example.risks]
1020
+ if not family_costs or not family_risks:
1021
+ raise ValueError(f"cost/risk selector has no train priors for {family.value}")
1022
+ costs[family] = fmean(family_costs)
1023
+ risks[family] = fmean(family_risks)
1024
+ return CostRiskAwareSelector(
1025
+ base_selector,
1026
+ releases,
1027
+ costs,
1028
+ risks,
1029
+ max_cost=max_cost,
1030
+ max_risk=max_risk,
1031
+ cost_weight=cost_weight,
1032
+ risk_weight=risk_weight,
1033
+ )
1034
+
1035
+
1036
+ @dataclass(frozen=True, slots=True)
1037
+ class SelectorCalibrationResult:
1038
+ selector: Selector
1039
+ evaluation: PolicyEvaluation
1040
+ candidates_considered: int
1041
+
1042
+
1043
+ def calibrate_selector_abstention(
1044
+ selector: Selector,
1045
+ examples: Sequence[SelectorExample],
1046
+ *,
1047
+ gain_grid: Sequence[float],
1048
+ margin_grid: Sequence[float],
1049
+ max_harm_rate: float,
1050
+ ) -> SelectorCalibrationResult:
1051
+ """Freeze gain/margin gates using calibration rows, never test outcomes."""
1052
+
1053
+ rows = _calibration_rows(examples)
1054
+ if (
1055
+ not is_dataclass(selector)
1056
+ or not hasattr(selector, "min_gain")
1057
+ or not hasattr(selector, "margin")
1058
+ ):
1059
+ raise TypeError("selector does not expose calibratable min_gain and margin fields")
1060
+ if not 0 <= max_harm_rate <= 1:
1061
+ raise ValueError("max_harm_rate must be in [0, 1]")
1062
+ gains = _canonical_calibration_grid(gain_grid, label="gain")
1063
+ margins = _canonical_calibration_grid(
1064
+ margin_grid,
1065
+ label="margin",
1066
+ lower=0.0,
1067
+ )
1068
+ candidates: list[tuple[PolicyEvaluation, Selector]] = []
1069
+ considered = 0
1070
+ for gain in gains:
1071
+ for margin in margins:
1072
+ considered += 1
1073
+ candidate = replace(selector, min_gain=gain, margin=margin)
1074
+ if not isinstance(candidate, Selector): # pragma: no cover - dataclass invariant
1075
+ raise TypeError("calibrated object no longer satisfies Selector")
1076
+ evaluation = evaluate_selector(candidate, rows, required_split="calibration")
1077
+ if evaluation.harm_rate <= max_harm_rate:
1078
+ candidates.append((evaluation, candidate))
1079
+ return _best_calibration(candidates, considered)
1080
+
1081
+
1082
+ def calibrate_cost_risk_guardrails(
1083
+ selector: CostRiskAwareSelector,
1084
+ examples: Sequence[SelectorExample],
1085
+ *,
1086
+ max_cost_grid: Sequence[float],
1087
+ max_risk_grid: Sequence[float],
1088
+ gain_grid: Sequence[float],
1089
+ margin_grid: Sequence[float],
1090
+ max_harm_rate: float,
1091
+ ) -> SelectorCalibrationResult:
1092
+ """Freeze cost, risk, gain, and margin gates on calibration rows only."""
1093
+
1094
+ rows = _calibration_rows(examples)
1095
+ costs = _canonical_calibration_grid(
1096
+ max_cost_grid,
1097
+ label="max_cost",
1098
+ lower=0.0,
1099
+ )
1100
+ risks = _canonical_calibration_grid(
1101
+ max_risk_grid,
1102
+ label="max_risk",
1103
+ lower=0.0,
1104
+ upper=1.0,
1105
+ )
1106
+ gains = _canonical_calibration_grid(gain_grid, label="gain")
1107
+ margins = _canonical_calibration_grid(
1108
+ margin_grid,
1109
+ label="margin",
1110
+ lower=0.0,
1111
+ )
1112
+ if not 0 <= max_harm_rate <= 1:
1113
+ raise ValueError("max_harm_rate must be in [0, 1]")
1114
+ candidates: list[tuple[PolicyEvaluation, Selector]] = []
1115
+ considered = 0
1116
+ for max_cost in costs:
1117
+ for max_risk in risks:
1118
+ for gain in gains:
1119
+ for margin in margins:
1120
+ considered += 1
1121
+ candidate = replace(
1122
+ selector,
1123
+ max_cost=max_cost,
1124
+ max_risk=max_risk,
1125
+ min_gain=gain,
1126
+ margin=margin,
1127
+ )
1128
+ evaluation = evaluate_selector(
1129
+ candidate,
1130
+ rows,
1131
+ required_split="calibration",
1132
+ )
1133
+ if evaluation.harm_rate <= max_harm_rate:
1134
+ candidates.append((evaluation, candidate))
1135
+ return _best_calibration(candidates, considered)
1136
+
1137
+
1138
+ def _best_calibration(
1139
+ candidates: Sequence[tuple[PolicyEvaluation, Selector]],
1140
+ considered: int,
1141
+ ) -> SelectorCalibrationResult:
1142
+ if not candidates:
1143
+ raise ValueError("no calibration candidate satisfies the harm constraint")
1144
+ evaluation, selector = max(
1145
+ candidates,
1146
+ key=lambda item: (
1147
+ item[0].mean_policy_value,
1148
+ item[0].coverage,
1149
+ -item[0].harm_rate,
1150
+ -float(item[1].max_cost) if isinstance(item[1], CostRiskAwareSelector) else 0.0,
1151
+ -float(item[1].max_risk) if isinstance(item[1], CostRiskAwareSelector) else 0.0,
1152
+ -float(item[1].min_gain),
1153
+ -float(item[1].margin),
1154
+ ),
1155
+ )
1156
+ return SelectorCalibrationResult(selector, evaluation, considered)
1157
+
1158
+
1159
+ def _training_rows(
1160
+ examples: Sequence[SelectorExample],
1161
+ *,
1162
+ label: str,
1163
+ ) -> tuple[SelectorExample, ...]:
1164
+ rows = tuple(examples)
1165
+ if not rows:
1166
+ raise ValueError(f"{label} requires training examples")
1167
+ if any(not isinstance(example, SelectorExample) for example in rows):
1168
+ raise TypeError(f"{label} examples must be SelectorExample instances")
1169
+ if any(example.split != "train" for example in rows):
1170
+ raise ValueError(f"{label} only accepts split='train' examples")
1171
+ ids = [example.example_id for example in rows]
1172
+ if len(ids) != len(set(ids)):
1173
+ raise ValueError(f"{label} requires unique example IDs")
1174
+ return rows
1175
+
1176
+
1177
+ def _calibration_rows(examples: Sequence[SelectorExample]) -> tuple[SelectorExample, ...]:
1178
+ rows = tuple(examples)
1179
+ if not rows or any(not isinstance(example, SelectorExample) for example in rows):
1180
+ raise ValueError("selector calibration requires SelectorExample rows")
1181
+ if any(example.split != "calibration" for example in rows):
1182
+ raise ValueError("selector calibration only accepts split='calibration' examples")
1183
+ ids = [example.example_id for example in rows]
1184
+ if len(ids) != len(set(ids)):
1185
+ raise ValueError("selector calibration requires unique example IDs")
1186
+ return rows
1187
+
1188
+
1189
+ def _canonical_calibration_grid(
1190
+ values: Sequence[float],
1191
+ *,
1192
+ label: str,
1193
+ lower: float | None = None,
1194
+ upper: float | None = None,
1195
+ ) -> tuple[float, ...]:
1196
+ raw = tuple(values)
1197
+ if not raw:
1198
+ raise ValueError(f"{label} grid must be non-empty")
1199
+ normalized: list[float] = []
1200
+ for value in raw:
1201
+ if not _finite(value):
1202
+ raise ValueError(f"{label} grid must contain finite numeric values")
1203
+ number = float(value)
1204
+ if lower is not None and number < lower:
1205
+ raise ValueError(f"{label} grid values must be at least {lower:g}")
1206
+ if upper is not None and number > upper:
1207
+ raise ValueError(f"{label} grid values must be at most {upper:g}")
1208
+ normalized.append(number)
1209
+ return tuple(sorted(set(normalized)))
1210
+
1211
+
1212
+ def _validated_release_mapping(
1213
+ value: Mapping[SemanticFamily, str],
1214
+ ) -> dict[SemanticFamily, str]:
1215
+ copied = dict(value)
1216
+ if not copied:
1217
+ raise ValueError("selector requires at least one release mapping")
1218
+ if any(
1219
+ family not in _ACTIONS or not isinstance(release, str) or not release.strip()
1220
+ for family, release in copied.items()
1221
+ ):
1222
+ raise ValueError("release mappings require intervention-family keys and IDs")
1223
+ return copied
1224
+
1225
+
1226
+ def _validated_family_values(
1227
+ value: Mapping[SemanticFamily, float],
1228
+ *,
1229
+ families: set[SemanticFamily],
1230
+ label: str,
1231
+ upper: float | None,
1232
+ ) -> dict[SemanticFamily, float]:
1233
+ copied = dict(value)
1234
+ if set(copied) != families:
1235
+ raise ValueError(f"{label} priors must exactly match release families")
1236
+ if any(
1237
+ not _finite(item) or item < 0 or (upper is not None and item > upper)
1238
+ for item in copied.values()
1239
+ ):
1240
+ bound = f" in [0, {upper:g}]" if upper is not None else " and non-negative"
1241
+ raise ValueError(f"{label} priors must be finite{bound}")
1242
+ return {family: float(item) for family, item in copied.items()}
1243
+
1244
+
1245
+ def _validated_predicted_gains(
1246
+ value: Mapping[SemanticFamily, float],
1247
+ *,
1248
+ expected_families: set[SemanticFamily],
1249
+ ) -> dict[SemanticFamily, float]:
1250
+ if not isinstance(value, Mapping):
1251
+ raise TypeError("utility-gain provider must return a mapping")
1252
+ copied = dict(value)
1253
+ if set(copied) != expected_families or any(
1254
+ not isinstance(family, SemanticFamily) or family is SemanticFamily.NO_OP
1255
+ for family in copied
1256
+ ):
1257
+ raise ValueError("utility-gain provider outputs must exactly match release families")
1258
+ if any(not _finite(gain) for gain in copied.values()):
1259
+ raise ValueError("utility-gain provider outputs must be finite numbers")
1260
+ return {family: float(gain) for family, gain in copied.items()}
1261
+
1262
+
1263
+ def _validate_abstention(min_gain: float, margin: float) -> None:
1264
+ if not _finite(min_gain):
1265
+ raise ValueError("selector min_gain must be finite")
1266
+ if not _finite(margin) or margin < 0:
1267
+ raise ValueError("selector margin must be finite and non-negative")
1268
+
1269
+
1270
+ def _validate_logistic_config(l2: float, learning_rate: float, iterations: int) -> None:
1271
+ if not _finite(l2) or l2 < 0:
1272
+ raise ValueError("logistic l2 must be finite and non-negative")
1273
+ if not _finite(learning_rate) or learning_rate <= 0:
1274
+ raise ValueError("logistic learning_rate must be finite and positive")
1275
+ if isinstance(iterations, bool) or not isinstance(iterations, int) or iterations < 1:
1276
+ raise ValueError("logistic iterations must be a positive integer")
1277
+
1278
+
1279
+ def _decision_from_gains(
1280
+ gains: Mapping[SemanticFamily, float],
1281
+ release_by_family: Mapping[SemanticFamily, str],
1282
+ *,
1283
+ eligible_families: Collection[SemanticFamily] | None,
1284
+ min_gain: float,
1285
+ margin: float,
1286
+ label: str,
1287
+ ) -> SelectionDecision:
1288
+ eligible = _eligible_actions(eligible_families)
1289
+ scores = {family.value: value for family, value in gains.items()}
1290
+ scores[SemanticFamily.NO_OP.value] = 0.0
1291
+ ordered = sorted(
1292
+ ((family, gain) for family, gain in gains.items() if family in eligible),
1293
+ key=lambda item: (-item[1], item[0].value),
1294
+ )
1295
+ if not ordered:
1296
+ return _no_op("no modeled intervention family is eligible", scores)
1297
+ family, gain = ordered[0]
1298
+ runner_up = max(0.0, ordered[1][1] if len(ordered) > 1 else 0.0)
1299
+ if gain <= min_gain or gain - runner_up <= margin:
1300
+ return _no_op(f"{label} gain or margin gate not met", scores)
1301
+ return SelectionDecision(
1302
+ family=family,
1303
+ release_id=release_by_family[family],
1304
+ selection_score=gain,
1305
+ rationale=f"selected {label} winner: {family.value}",
1306
+ scores=scores,
1307
+ )
1308
+
1309
+
1310
+ def _no_op(rationale: str, scores: Mapping[str, float]) -> SelectionDecision:
1311
+ return SelectionDecision(
1312
+ family=SemanticFamily.NO_OP,
1313
+ release_id="baseline.no_op",
1314
+ selection_score=0.0,
1315
+ rationale=f"abstained: {rationale}",
1316
+ scores=scores,
1317
+ )
1318
+
1319
+
1320
+ def _eligible_actions(
1321
+ eligible_families: Collection[SemanticFamily] | None,
1322
+ ) -> set[SemanticFamily]:
1323
+ if eligible_families is None:
1324
+ return set(_ACTIONS)
1325
+ eligible = tuple(eligible_families)
1326
+ if any(not isinstance(family, SemanticFamily) for family in eligible):
1327
+ raise ValueError("eligible_families must contain SemanticFamily values")
1328
+ return {family for family in eligible if family in _ACTIONS}
1329
+
1330
+
1331
+ def _selector_hash(selector: Selector) -> str:
1332
+ declared = getattr(selector, "model_hash", None)
1333
+ if isinstance(declared, str) and len(declared) == 64:
1334
+ return declared
1335
+ try:
1336
+ return content_hash(selector)
1337
+ except TypeError as exc:
1338
+ raise ValueError("base selector must be frozen and canonically hashable") from exc
1339
+
1340
+
1341
+ def _euclidean(left: Sequence[float], right: Sequence[float]) -> float:
1342
+ return math.sqrt(math.fsum((one - two) ** 2 for one, two in zip(left, right, strict=True)))
1343
+
1344
+
1345
+ def _fit_ridge(
1346
+ matrix: Sequence[tuple[float, ...]],
1347
+ labels: Sequence[float],
1348
+ *,
1349
+ ridge: float,
1350
+ ) -> tuple[float, ...]:
1351
+ width = len(matrix[0])
1352
+ gram = [[0.0 for _ in range(width)] for _ in range(width)]
1353
+ rhs = [0.0 for _ in range(width)]
1354
+ for row, label in zip(matrix, labels, strict=True):
1355
+ for left in range(width):
1356
+ rhs[left] += row[left] * label
1357
+ for right in range(width):
1358
+ gram[left][right] += row[left] * row[right]
1359
+ gram[0][0] += ridge * 1e-3
1360
+ for index in range(1, width):
1361
+ gram[index][index] += ridge
1362
+ return tuple(_solve_linear_system(gram, rhs))
1363
+
1364
+
1365
+ def _solve_linear_system(matrix: list[list[float]], rhs: list[float]) -> list[float]:
1366
+ size = len(rhs)
1367
+ augmented = [[*row, value] for row, value in zip(matrix, rhs, strict=True)]
1368
+ for column in range(size):
1369
+ pivot = max(range(column, size), key=lambda row: abs(augmented[row][column]))
1370
+ if abs(augmented[pivot][column]) < 1e-12:
1371
+ raise ValueError("T-learner design matrix is singular; increase ridge")
1372
+ augmented[column], augmented[pivot] = augmented[pivot], augmented[column]
1373
+ scale = augmented[column][column]
1374
+ augmented[column] = [value / scale for value in augmented[column]]
1375
+ for row in range(size):
1376
+ if row == column:
1377
+ continue
1378
+ factor = augmented[row][column]
1379
+ augmented[row] = [
1380
+ value - factor * pivot_value
1381
+ for value, pivot_value in zip(augmented[row], augmented[column], strict=True)
1382
+ ]
1383
+ return [augmented[row][-1] for row in range(size)]
1384
+
1385
+
1386
+ def _freeze_sklearn_gbdt(estimator: Any) -> FrozenGBDTOutcomeModel:
1387
+ try:
1388
+ initial_value = float(estimator.init_.constant_.ravel()[0])
1389
+ trees = []
1390
+ for row in estimator.estimators_:
1391
+ fitted = row[0].tree_
1392
+ trees.append(
1393
+ FrozenRegressionTree(
1394
+ tuple(int(value) for value in fitted.children_left.tolist()),
1395
+ tuple(int(value) for value in fitted.children_right.tolist()),
1396
+ tuple(int(value) for value in fitted.feature.tolist()),
1397
+ tuple(float(value) for value in fitted.threshold.tolist()),
1398
+ tuple(float(value) for value in fitted.value[:, 0, 0].tolist()),
1399
+ )
1400
+ )
1401
+ return FrozenGBDTOutcomeModel(initial_value, float(estimator.learning_rate), tuple(trees))
1402
+ except (AttributeError, IndexError, TypeError, ValueError) as exc:
1403
+ raise ValueError("unsupported sklearn GBDT fitted structure") from exc
1404
+
1405
+
1406
+ def _fit_logistic(
1407
+ matrix: Sequence[tuple[float, ...]],
1408
+ labels: Sequence[float],
1409
+ *,
1410
+ l2: float,
1411
+ learning_rate: float,
1412
+ iterations: int,
1413
+ ) -> tuple[float, ...]:
1414
+ width = len(matrix[0])
1415
+ weights = [0.0] * width
1416
+ count = len(matrix)
1417
+ for _ in range(iterations):
1418
+ gradient = [0.0] * width
1419
+ for row, label in zip(matrix, labels, strict=True):
1420
+ error = _sigmoid(math.fsum(a * b for a, b in zip(weights, row, strict=True))) - label
1421
+ for index, value in enumerate(row):
1422
+ gradient[index] += error * value / count
1423
+ for index in range(1, width):
1424
+ gradient[index] += l2 * weights[index]
1425
+ for index in range(width):
1426
+ weights[index] -= learning_rate * gradient[index]
1427
+ return tuple(weights)
1428
+
1429
+
1430
+ def _sigmoid(value: float) -> float:
1431
+ if value >= 0:
1432
+ inverse = math.exp(-value)
1433
+ return 1.0 / (1.0 + inverse)
1434
+ exponent = math.exp(value)
1435
+ return exponent / (1.0 + exponent)
1436
+
1437
+
1438
+ def _strict_artifact_mapping(
1439
+ value: object,
1440
+ *,
1441
+ expected_keys: set[str],
1442
+ label: str,
1443
+ ) -> Mapping[str, Any]:
1444
+ mapping = _strict_string_keyed_mapping(value, label=label)
1445
+ if set(mapping) != expected_keys:
1446
+ raise ValueError(f"{label} artifact keys do not match the canonical schema")
1447
+ return mapping
1448
+
1449
+
1450
+ def _strict_string_keyed_mapping(
1451
+ value: object,
1452
+ *,
1453
+ label: str,
1454
+ ) -> Mapping[str, Any]:
1455
+ if not isinstance(value, Mapping):
1456
+ raise TypeError(f"{label} must be a JSON object")
1457
+ if any(type(key) is not str for key in value):
1458
+ raise TypeError(f"{label} keys must be JSON strings")
1459
+ return value
1460
+
1461
+
1462
+ def _strict_json_array(value: object, *, label: str) -> list[Any]:
1463
+ if not isinstance(value, list):
1464
+ raise TypeError(f"{label} must be a JSON array")
1465
+ return value
1466
+
1467
+
1468
+ def _strict_json_integer(value: object, *, label: str) -> int:
1469
+ if type(value) is not int:
1470
+ raise TypeError(f"{label} must be a JSON integer")
1471
+ return value
1472
+
1473
+
1474
+ def _strict_json_number(value: object, *, label: str) -> float:
1475
+ if not _finite(value):
1476
+ raise TypeError(f"{label} must be a finite JSON number")
1477
+ return float(value)
1478
+
1479
+
1480
+ def _valid_sha256(value: object) -> bool:
1481
+ return (
1482
+ isinstance(value, str)
1483
+ and len(value) == 64
1484
+ and all(character in "0123456789abcdef" for character in value)
1485
+ )
1486
+
1487
+
1488
+ def _finite(value: object) -> bool:
1489
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
1490
+ return False
1491
+ try:
1492
+ return math.isfinite(value)
1493
+ except OverflowError:
1494
+ return False
1495
+
1496
+
1497
+ __all__ = [
1498
+ "CostRiskAwareSelector",
1499
+ "FrozenGBDTOutcomeModel",
1500
+ "FrozenRegressionTree",
1501
+ "GBDTTLearnerSelector",
1502
+ "KNNTrainingRow",
1503
+ "KNNUtilitySelector",
1504
+ "LogisticTLearnerSelector",
1505
+ "RidgeTLearnerSelector",
1506
+ "SelectorCalibrationResult",
1507
+ "calibrate_cost_risk_guardrails",
1508
+ "calibrate_selector_abstention",
1509
+ "fit_cost_risk_selector",
1510
+ "fit_gbdt_t_learner",
1511
+ "fit_knn_selector",
1512
+ "fit_logistic_t_learner",
1513
+ "fit_ridge_t_learner",
1514
+ ]