evalrx 0.1.2__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 (339) hide show
  1. evalrx/__init__.py +139 -0
  2. evalrx/agent_assets/__init__.py +2 -0
  3. evalrx/agent_assets/skills/README.md +28 -0
  4. evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
  5. evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
  6. evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
  7. evalrx/agent_assets/skills/nature-figure/README.md +412 -0
  8. evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
  9. evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
  10. evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
  11. evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
  12. evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
  13. evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
  14. evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
  15. evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
  16. evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
  17. evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
  18. evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
  19. evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
  20. evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
  21. evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
  22. evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
  23. evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
  24. evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
  25. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
  26. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
  27. evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
  28. evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
  29. evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
  30. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
  31. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
  32. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
  33. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
  34. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
  35. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
  36. evalrx/agent_assets/skills.py +27 -0
  37. evalrx/agent_runtime/__init__.py +78 -0
  38. evalrx/agent_runtime/_docker_runner.py +89 -0
  39. evalrx/agent_runtime/cli_runtime.py +103 -0
  40. evalrx/agent_runtime/cli_transcript.py +138 -0
  41. evalrx/agent_runtime/cli_types.py +68 -0
  42. evalrx/agent_runtime/codegen/__init__.py +5 -0
  43. evalrx/agent_runtime/codegen/runner.py +94 -0
  44. evalrx/agent_runtime/experiment_harness.py +117 -0
  45. evalrx/agent_runtime/factory.py +102 -0
  46. evalrx/agent_runtime/json_shape.py +44 -0
  47. evalrx/agent_runtime/judges/__init__.py +28 -0
  48. evalrx/agent_runtime/judges/agy.py +179 -0
  49. evalrx/agent_runtime/judges/autodetect.py +135 -0
  50. evalrx/agent_runtime/judges/claude.py +159 -0
  51. evalrx/agent_runtime/judges/codex.py +120 -0
  52. evalrx/agent_runtime/providers/__init__.py +21 -0
  53. evalrx/agent_runtime/providers/antigravity.py +31 -0
  54. evalrx/agent_runtime/providers/base.py +145 -0
  55. evalrx/agent_runtime/providers/claude_code.py +49 -0
  56. evalrx/agent_runtime/providers/codex.py +37 -0
  57. evalrx/agent_runtime/providers/gemini_cli.py +26 -0
  58. evalrx/agent_runtime/providers/kimi_cli.py +27 -0
  59. evalrx/agent_runtime/providers/opencode.py +27 -0
  60. evalrx/agent_runtime/providers/registry.py +58 -0
  61. evalrx/agent_runtime/sandbox.py +517 -0
  62. evalrx/agent_runtime/skill_audit.py +143 -0
  63. evalrx/agent_runtime/skills/__init__.py +19 -0
  64. evalrx/agent_runtime/skills/installer.py +68 -0
  65. evalrx/agent_runtime/skills/prompt_policy.py +86 -0
  66. evalrx/agent_runtime/skills/resolver.py +19 -0
  67. evalrx/analysis/__init__.py +132 -0
  68. evalrx/analysis/adjudicate.py +154 -0
  69. evalrx/analysis/analysis_module.py +361 -0
  70. evalrx/analysis/api.py +171 -0
  71. evalrx/analysis/case_studio.py +651 -0
  72. evalrx/analysis/cli.py +114 -0
  73. evalrx/analysis/dashboard.py +350 -0
  74. evalrx/analysis/eval_case_matrix.py +118 -0
  75. evalrx/analysis/eval_viz_theme.py +833 -0
  76. evalrx/analysis/explore_run.py +333 -0
  77. evalrx/analysis/explorer.py +1276 -0
  78. evalrx/analysis/failure_modes.py +607 -0
  79. evalrx/analysis/fused_pipeline.py +489 -0
  80. evalrx/analysis/holdout.py +300 -0
  81. evalrx/analysis/hypothesis_agent.py +230 -0
  82. evalrx/analysis/narration.py +177 -0
  83. evalrx/analysis/operationalize.py +442 -0
  84. evalrx/analysis/plain_language.py +42 -0
  85. evalrx/analysis/planner.py +283 -0
  86. evalrx/analysis/probe_search.py +203 -0
  87. evalrx/analysis/profile.py +268 -0
  88. evalrx/analysis/prompts/__init__.py +0 -0
  89. evalrx/analysis/prompts/explorer.py +417 -0
  90. evalrx/analysis/prompts/failure_modes.py +33 -0
  91. evalrx/analysis/prompts/holdout.py +27 -0
  92. evalrx/analysis/prompts/hypothesis_agent.py +78 -0
  93. evalrx/analysis/prompts/run_codebase.py +47 -0
  94. evalrx/analysis/prompts/stats_agent.py +72 -0
  95. evalrx/analysis/prompts/stats_tool_generator.py +43 -0
  96. evalrx/analysis/result_marker.py +47 -0
  97. evalrx/analysis/run_codebase.py +242 -0
  98. evalrx/analysis/run_view.py +205 -0
  99. evalrx/analysis/stage_views.py +93 -0
  100. evalrx/analysis/stats_agent.py +944 -0
  101. evalrx/analysis/stats_tool_agent.py +261 -0
  102. evalrx/analysis/stats_tool_generator.py +415 -0
  103. evalrx/analysis/stats_tools.py +1153 -0
  104. evalrx/analysis/trajectory_records.py +193 -0
  105. evalrx/analysis/workbench.py +431 -0
  106. evalrx/analyzers/__init__.py +42 -0
  107. evalrx/analyzers/agent/__init__.py +25 -0
  108. evalrx/analyzers/agent/counterfactual.py +84 -0
  109. evalrx/analyzers/agent/first_error_judge.py +96 -0
  110. evalrx/analyzers/agent/ignored_obs.py +81 -0
  111. evalrx/analyzers/agent/loop_detect.py +79 -0
  112. evalrx/analyzers/agent/reliability.py +165 -0
  113. evalrx/analyzers/agent/tool_shap.py +225 -0
  114. evalrx/analyzers/agent/trajectory_rubric.py +168 -0
  115. evalrx/analyzers/attention/__init__.py +19 -0
  116. evalrx/analyzers/attention/relative_attn.py +610 -0
  117. evalrx/analyzers/attention/rollout.py +73 -0
  118. evalrx/analyzers/attention/sink.py +56 -0
  119. evalrx/analyzers/attention/summary.py +190 -0
  120. evalrx/analyzers/attribution/__init__.py +6 -0
  121. evalrx/analyzers/attribution/generic_attn.py +31 -0
  122. evalrx/analyzers/attribution/gradcam.py +30 -0
  123. evalrx/analyzers/base.py +12 -0
  124. evalrx/analyzers/geometry/__init__.py +6 -0
  125. evalrx/analyzers/geometry/cka.py +70 -0
  126. evalrx/analyzers/geometry/linear_probe.py +157 -0
  127. evalrx/analyzers/hallucination/__init__.py +9 -0
  128. evalrx/analyzers/hallucination/chair.py +78 -0
  129. evalrx/analyzers/hallucination/opera.py +29 -0
  130. evalrx/analyzers/hallucination/pope.py +119 -0
  131. evalrx/analyzers/hallucination/selfcheck.py +155 -0
  132. evalrx/analyzers/hallucination/vcd.py +29 -0
  133. evalrx/analyzers/lens/__init__.py +7 -0
  134. evalrx/analyzers/lens/layer_contrast.py +133 -0
  135. evalrx/analyzers/lens/logit_lens.py +138 -0
  136. evalrx/analyzers/lens/tuned_lens.py +30 -0
  137. evalrx/analyzers/patching/__init__.py +5 -0
  138. evalrx/analyzers/patching/causal_trace.py +30 -0
  139. evalrx/analyzers/perturbation/__init__.py +23 -0
  140. evalrx/analyzers/perturbation/_shapley.py +54 -0
  141. evalrx/analyzers/perturbation/context_shap.py +174 -0
  142. evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
  143. evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
  144. evalrx/analyzers/perturbation/mm_shap.py +146 -0
  145. evalrx/analyzers/perturbation/modality_ablation.py +196 -0
  146. evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
  147. evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
  148. evalrx/analyzers/perturbation/rise.py +94 -0
  149. evalrx/analyzers/perturbation/vl_shap.py +102 -0
  150. evalrx/analyzers/reasoning/__init__.py +33 -0
  151. evalrx/analyzers/reasoning/_text.py +328 -0
  152. evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
  153. evalrx/analyzers/reasoning/arith_audit.py +226 -0
  154. evalrx/analyzers/reasoning/contamination.py +214 -0
  155. evalrx/analyzers/reasoning/knowledge_split.py +253 -0
  156. evalrx/analyzers/reasoning/self_repair.py +246 -0
  157. evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
  158. evalrx/analyzers/reasoning/termination_audit.py +258 -0
  159. evalrx/analyzers/uncertainty/__init__.py +18 -0
  160. evalrx/analyzers/uncertainty/calibration.py +174 -0
  161. evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
  162. evalrx/analyzers/uncertainty/entropy.py +90 -0
  163. evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
  164. evalrx/analyzers/uncertainty/self_consistency.py +204 -0
  165. evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
  166. evalrx/cli.py +411 -0
  167. evalrx/config.py +77 -0
  168. evalrx/contract/__init__.py +179 -0
  169. evalrx/contract/common.py +452 -0
  170. evalrx/contract/emit.py +948 -0
  171. evalrx/contract/export.py +237 -0
  172. evalrx/contract/m1.py +325 -0
  173. evalrx/contract/m2.py +317 -0
  174. evalrx/contract/m3.py +165 -0
  175. evalrx/contract/m4.py +130 -0
  176. evalrx/contract/m5.py +292 -0
  177. evalrx/contract/methodology.py +76 -0
  178. evalrx/contract/pre_m1.py +58 -0
  179. evalrx/contract/typescript.py +140 -0
  180. evalrx/core/__init__.py +85 -0
  181. evalrx/core/analyzer.py +174 -0
  182. evalrx/core/capability.py +54 -0
  183. evalrx/core/case.py +443 -0
  184. evalrx/core/experiment.py +106 -0
  185. evalrx/core/model.py +198 -0
  186. evalrx/core/pipeline.py +42 -0
  187. evalrx/core/registry.py +142 -0
  188. evalrx/core/result.py +64 -0
  189. evalrx/core/spec.py +173 -0
  190. evalrx/core/tokentype.py +165 -0
  191. evalrx/core/tool.py +92 -0
  192. evalrx/datasets/__init__.py +41 -0
  193. evalrx/datasets/base.py +68 -0
  194. evalrx/datasets/gui_os.py +52 -0
  195. evalrx/datasets/llm_qa.py +57 -0
  196. evalrx/datasets/pure_qa.py +12 -0
  197. evalrx/datasets/vlm_qa.py +695 -0
  198. evalrx/datasets/web_search_qa.py +52 -0
  199. evalrx/eval_agent/__init__.py +341 -0
  200. evalrx/eval_agent/_tools.py +81 -0
  201. evalrx/eval_agent/ab_runner.py +50 -0
  202. evalrx/eval_agent/agentic/__init__.py +43 -0
  203. evalrx/eval_agent/agentic/actions.py +216 -0
  204. evalrx/eval_agent/agentic/board.py +107 -0
  205. evalrx/eval_agent/agentic/loop.py +190 -0
  206. evalrx/eval_agent/agentic/tools.py +538 -0
  207. evalrx/eval_agent/checkpoint.py +57 -0
  208. evalrx/eval_agent/cli_agent.py +59 -0
  209. evalrx/eval_agent/cli_skills.py +5 -0
  210. evalrx/eval_agent/evolution.py +396 -0
  211. evalrx/eval_agent/git_manager.py +215 -0
  212. evalrx/eval_agent/hypothesis.py +172 -0
  213. evalrx/eval_agent/label_quarantine.py +209 -0
  214. evalrx/eval_agent/legacy.py +530 -0
  215. evalrx/eval_agent/log_schema.py +497 -0
  216. evalrx/eval_agent/loop.py +2159 -0
  217. evalrx/eval_agent/loop_reports.py +116 -0
  218. evalrx/eval_agent/model_instrumentation.py +282 -0
  219. evalrx/eval_agent/narration.py +193 -0
  220. evalrx/eval_agent/nl_runner.py +460 -0
  221. evalrx/eval_agent/orchestrator.py +61 -0
  222. evalrx/eval_agent/preregister.py +93 -0
  223. evalrx/eval_agent/prompts/__init__.py +1 -0
  224. evalrx/eval_agent/prompts/agentic.py +46 -0
  225. evalrx/eval_agent/prompts/case_discovery.py +25 -0
  226. evalrx/eval_agent/prompts/diagnosis.py +125 -0
  227. evalrx/eval_agent/prompts/experiment_writer.py +265 -0
  228. evalrx/eval_agent/prompts/explore_step.py +37 -0
  229. evalrx/eval_agent/prompts/fix_agent.py +257 -0
  230. evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
  231. evalrx/eval_agent/prompts/nl_runner.py +38 -0
  232. evalrx/eval_agent/prompts/probe_agent.py +25 -0
  233. evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
  234. evalrx/eval_agent/prompts/probe_generator.py +35 -0
  235. evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
  236. evalrx/eval_agent/report.py +58 -0
  237. evalrx/eval_agent/run_context.py +354 -0
  238. evalrx/eval_agent/run_log.schema.json +1215 -0
  239. evalrx/eval_agent/run_logger_v2.py +1764 -0
  240. evalrx/eval_agent/run_metadata.py +208 -0
  241. evalrx/eval_agent/stages/__init__.py +56 -0
  242. evalrx/eval_agent/stages/case_discovery.py +293 -0
  243. evalrx/eval_agent/stages/diagnosis.py +1017 -0
  244. evalrx/eval_agent/stages/experiment_writer.py +1634 -0
  245. evalrx/eval_agent/stages/fix_agent.py +3916 -0
  246. evalrx/eval_agent/stages/fix_internals.py +499 -0
  247. evalrx/eval_agent/stages/fix_pipeline.py +725 -0
  248. evalrx/eval_agent/stages/fix_tiers.py +187 -0
  249. evalrx/eval_agent/stages/fix_tools.py +1034 -0
  250. evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
  251. evalrx/eval_agent/stages/probe.py +439 -0
  252. evalrx/eval_agent/stages/probe_agent.py +1079 -0
  253. evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
  254. evalrx/eval_agent/stages/probe_generator.py +326 -0
  255. evalrx/eval_agent/stages/probe_search_agent.py +106 -0
  256. evalrx/eval_agent/stages/protocol.py +112 -0
  257. evalrx/eval_agent/stages/repair_catalog.py +273 -0
  258. evalrx/eval_agent/stages/surgery.py +524 -0
  259. evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
  260. evalrx/eval_agent/store.py +231 -0
  261. evalrx/logging_utils.py +112 -0
  262. evalrx/models/__init__.py +161 -0
  263. evalrx/models/_discover.py +101 -0
  264. evalrx/models/agent.py +380 -0
  265. evalrx/models/backends/__init__.py +58 -0
  266. evalrx/models/backends/api.py +169 -0
  267. evalrx/models/backends/base.py +57 -0
  268. evalrx/models/backends/gemini_compat.py +579 -0
  269. evalrx/models/backends/hf_local.py +2074 -0
  270. evalrx/models/backends/openai_compat.py +301 -0
  271. evalrx/models/backends/vllm_offline.py +116 -0
  272. evalrx/models/base.py +24 -0
  273. evalrx/models/blackbox/__init__.py +4 -0
  274. evalrx/models/blackbox/agent.py +31 -0
  275. evalrx/models/blackbox/base.py +29 -0
  276. evalrx/models/blackbox/gemini.py +279 -0
  277. evalrx/models/blackbox/llm_api.py +17 -0
  278. evalrx/models/blackbox/vlm_api.py +17 -0
  279. evalrx/models/compose.py +66 -0
  280. evalrx/models/inference.py +88 -0
  281. evalrx/models/paper_methods/__init__.py +8 -0
  282. evalrx/models/paper_methods/aad.py +53 -0
  283. evalrx/models/paper_methods/ifcd.py +204 -0
  284. evalrx/models/paper_methods/pai.py +164 -0
  285. evalrx/models/paper_methods/tcd.py +202 -0
  286. evalrx/models/paper_methods/vcd.py +45 -0
  287. evalrx/models/paper_methods/vicrop.py +137 -0
  288. evalrx/models/toolcodec.py +143 -0
  289. evalrx/models/tools/__init__.py +20 -0
  290. evalrx/models/tools/perception.py +300 -0
  291. evalrx/models/tools/visual.py +174 -0
  292. evalrx/models/whitebox/__init__.py +26 -0
  293. evalrx/models/whitebox/agent.py +31 -0
  294. evalrx/models/whitebox/base.py +24 -0
  295. evalrx/models/whitebox/qwen.py +61 -0
  296. evalrx/models/whitebox/qwen2_5_omni.py +29 -0
  297. evalrx/models/whitebox/qwen2_audio.py +25 -0
  298. evalrx/models/whitebox/qwen_omni.py +53 -0
  299. evalrx/models/whitebox/qwen_vl.py +62 -0
  300. evalrx/observability/__init__.py +21 -0
  301. evalrx/observability/envelope.py +122 -0
  302. evalrx/observability/outbox.py +111 -0
  303. evalrx/observability/tracer.py +882 -0
  304. evalrx/reporting/__init__.py +28 -0
  305. evalrx/reporting/case_study.py +947 -0
  306. evalrx/reporting/compiler.py +587 -0
  307. evalrx/reporting/dynamic.py +1882 -0
  308. evalrx/reporting/html_report.py +2225 -0
  309. evalrx/reporting/langfuse_exporter.py +38 -0
  310. evalrx/reporting/langfuse_source.py +155 -0
  311. evalrx/reporting/model.py +151 -0
  312. evalrx/reporting/run_events.py +184 -0
  313. evalrx/reporting/server.py +557 -0
  314. evalrx/reporting/stages.py +58 -0
  315. evalrx/reporting/static_export.py +142 -0
  316. evalrx/reporting/web_dist/index.html +146 -0
  317. evalrx/specs.py +727 -0
  318. evalrx/stats/__init__.py +47 -0
  319. evalrx/stats/api.py +192 -0
  320. evalrx/stats/bootstrap.py +86 -0
  321. evalrx/stats/ebh.py +27 -0
  322. evalrx/stats/evalue.py +98 -0
  323. evalrx/stats/friedman.py +138 -0
  324. evalrx/stats/mcnemar.py +40 -0
  325. evalrx/stats/multiplicity.py +159 -0
  326. evalrx/stats/subset_sampling.py +55 -0
  327. evalrx/term_links.py +43 -0
  328. evalrx/viz/__init__.py +7 -0
  329. evalrx/viz/labels.py +77 -0
  330. evalrx/viz/prompts.py +39 -0
  331. evalrx/viz/renderer.py +590 -0
  332. evalrx/viz/schema.py +36 -0
  333. evalrx/viz/style.py +134 -0
  334. evalrx-0.1.2.dist-info/METADATA +532 -0
  335. evalrx-0.1.2.dist-info/RECORD +339 -0
  336. evalrx-0.1.2.dist-info/WHEEL +5 -0
  337. evalrx-0.1.2.dist-info/entry_points.txt +3 -0
  338. evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
  339. evalrx-0.1.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,524 @@
1
+ """M5 — SurgeryAgent: perform targeted interventions to verify hypotheses.
2
+
3
+ Given a hypothesis from the diagnosis agent, the surgery agent operates on the
4
+ model/data to check whether the hypothesized cause actually predicts the
5
+ observed failures. Three strategies are available (in priority order):
6
+
7
+ 1. **verify_fn** (injected) — caller supplies the full verification logic.
8
+ 2. **analyzer_params** (param sweep) — re-run named analyzers with modified
9
+ parameters and surface before/after findings for comparison.
10
+ 3. **Default** (label correlation) — look for per-case signals already present
11
+ in the analysis results (e.g. ``has_loop``, ``n_ignored``), split cases into
12
+ "signal" vs "no-signal" groups, and compare FAIL rates using a 10 % gap
13
+ threshold. Cases in the signal group can be filtered out to produce
14
+ ``new_data`` for the next M1 cycle.
15
+
16
+ Usage::
17
+
18
+ agent = SurgeryAgent()
19
+ iv = agent.operate(hypothesis, model, results, data)
20
+ print(iv.status, iv.fixed, iv.evidence)
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ from dataclasses import dataclass, field
27
+ from typing import TYPE_CHECKING, Any, Callable
28
+
29
+ from evalrx.core.case import CaseBatch, Label
30
+ from evalrx.eval_agent.hypothesis import Hypothesis, HypothesisStatus
31
+
32
+ if TYPE_CHECKING:
33
+ from evalrx.core.model import Model
34
+ from evalrx.core.result import Result
35
+
36
+
37
+ @dataclass
38
+ class InterventionResult:
39
+ """Output of :class:`SurgeryAgent.operate`.
40
+
41
+ Attributes:
42
+ hypothesis: The hypothesis under test.
43
+ status: Updated :class:`~evalrx.eval_agent.hypothesis.HypothesisStatus`.
44
+ fixed: ``True`` when the intervention completely separates failing from
45
+ passing cases (signal group always fails, control group never
46
+ fails). Signals the loop that the problem is resolved.
47
+ evidence: Supporting statistics or sweep findings.
48
+ new_data: When *status* is SUPPORTED, the cases **not** in the signal
49
+ group — the refined subset for the next M1 cycle.
50
+ confidence_score: Float in [0, 1] measuring overall evidence strength. Derived
51
+ from the combination of signal-vs-control gap, sample adequacy,
52
+ and control-group cleanliness. A binary ``fixed`` flag alone
53
+ cannot convey how strongly supported the finding is — e.g.
54
+ a 10 % gap on 3 cases is very different from a 90 % gap on 300.
55
+ evidence_dimensions: Breakdown of ``confidence_score`` by sub-criterion so the loop
56
+ and callers can make finer-grained decisions (e.g. raise a
57
+ INCONCLUSIVE when ``sample_adequacy`` is low even if the gap
58
+ looks large).
59
+ """
60
+
61
+ hypothesis: Hypothesis
62
+ status: HypothesisStatus
63
+ fixed: bool
64
+ evidence: dict[str, Any] = field(default_factory=dict)
65
+ new_data: CaseBatch | None = None
66
+ confidence_score: float = 0.0
67
+ evidence_dimensions: dict[str, float] = field(default_factory=dict)
68
+ # Rich experiment payload from the experiment-writer strategy (strategy 3):
69
+ # generated source files, run stdout/stderr, blueprint, verdict, the agent's
70
+ # intermediate thinking and the working directory. Kept separate from
71
+ # ``evidence`` (which is logged inline) so RunLogger.log_experiment can
72
+ # persist the heavy parts to disk instead of bloating the JSONL event.
73
+ # ``None`` for the passive strategies (label correlation / param sweep).
74
+ experiment: dict[str, Any] | None = None
75
+
76
+
77
+ # Keys that carry diagnostic meaning in per-case finding entries.
78
+ # Excluded from the signal scan because they are indices, not boolean flags.
79
+ def _serialize_cases(data: CaseBatch, image_dir: "Any | None" = None) -> str:
80
+ """Serialize *data* to a compact JSON string embeddable in a script.
81
+
82
+ Includes the GOLD (``expected``) and the graded GENERATION (``observed``).
83
+ Without them a whole class of hypothesis is untestable in the sandbox rather
84
+ than merely hard: M1 and M3 keep proposing "these labels are grading
85
+ artifacts" on free-response sets, and the judge cannot check a grading claim
86
+ with no gold to grade against. Observed live on qwen3.5-2b/minervamath,
87
+ where the experiment said so itself --
88
+
89
+ "cases.json carries only {prompt, label, id} -- no gold answer, no
90
+ stored output. So `gold_in_output` cannot be recomputed directly.
91
+ So the claim is tested on its MECHANISM instead"
92
+
93
+ -- substituted a resampling proxy, and returned SUPPORTED for a hypothesis
94
+ that regrading the full batch refutes outright (0 of 114 PASS labels wrong).
95
+
96
+ This is NOT the sandbox that runs a candidate fix. That one is
97
+ :func:`~evalrx.eval_agent.stages.fix_pipeline.cases_payload`, which is
98
+ id+prompt only on purpose, so a fix cannot be written against the answers.
99
+ The split matters: a DIAGNOSTIC needs the gold to diagnose grading, a REPAIR
100
+ must not see it. Widening this function does not widen that one.
101
+
102
+ When *image_dir* is provided, PIL images are saved as JPEG files there and
103
+ the path is included as ``image_path`` so the generated diagnostic script
104
+ (and codex) can load them.
105
+ """
106
+ from pathlib import Path as _Path
107
+
108
+ records = []
109
+ for case in data:
110
+ rec: dict[str, Any] = {"prompt": str(case.inputs)}
111
+ if getattr(case, "label", None) is not None:
112
+ rec["label"] = str(case.label)
113
+ if getattr(case, "id", None):
114
+ rec["id"] = case.id
115
+ expected = getattr(case, "expected", None)
116
+ if expected is not None:
117
+ rec["expected"] = expected if isinstance(
118
+ expected, (str, int, float, bool)) else str(expected)
119
+ observed = getattr(case, "observed", None)
120
+ if observed is not None:
121
+ rec["observed"] = str(observed)
122
+ meta = getattr(case, "metadata", {}) or {}
123
+ if meta:
124
+ rec["metadata"] = {k: v for k, v in meta.items()
125
+ if isinstance(v, (str, int, float, bool, type(None)))}
126
+
127
+ # Save image to disk so codex / the diagnostic script can read it
128
+ image = getattr(case.inputs, "image", None) if hasattr(case, "inputs") else None
129
+ if image is not None and image_dir is not None:
130
+ try:
131
+ img_dir = _Path(image_dir)
132
+ img_dir.mkdir(parents=True, exist_ok=True)
133
+ case_id = case.id or f"case_{len(records)}"
134
+ img_path = img_dir / f"{case_id}.jpg"
135
+ # PIL image
136
+ image.convert("RGB").save(str(img_path), format="JPEG")
137
+ rec["image_path"] = str(img_path)
138
+ except Exception:
139
+ pass # non-PIL or unsavable — skip silently
140
+
141
+ records.append(rec)
142
+ return json.dumps(records, indent=2)
143
+
144
+
145
+ _NON_SIGNAL_KEYS = frozenset(
146
+ {"sample_id", "id", "step", "first_error_step", "action", "judge_raw"}
147
+ )
148
+
149
+ # Minimum cases per group before sample_adequacy reaches 1.0
150
+ _ADEQUACY_SATURATION = 30
151
+
152
+
153
+ def _compute_confidence(
154
+ fail_signal: float,
155
+ fail_control: float,
156
+ n_signal: int,
157
+ n_control: int,
158
+ ) -> tuple[float, dict[str, float]]:
159
+ """Compute overall confidence and per-dimension breakdown.
160
+
161
+ Three dimensions, each in [0, 1]:
162
+ - ``evidence_gap``: normalised fail-rate difference (how much worse signal group is).
163
+ - ``sample_adequacy``: whether both groups are large enough to trust the gap.
164
+ - ``control_cleanliness``: how low the control fail-rate is (ideally 0).
165
+
166
+ ``confidence_score`` = geometric mean of all three so that a near-zero on
167
+ any single dimension collapses the overall score — a 90 % gap on 2 cases
168
+ should not produce high confidence.
169
+ """
170
+ evidence_gap = max(0.0, min(1.0, (fail_signal - fail_control) / 1.0))
171
+ n_min = min(n_signal, n_control) if n_control > 0 else n_signal
172
+ sample_adequacy = min(1.0, n_min / _ADEQUACY_SATURATION)
173
+ control_cleanliness = 1.0 - fail_control
174
+
175
+ dims = {
176
+ "evidence_gap": round(evidence_gap, 3),
177
+ "sample_adequacy": round(sample_adequacy, 3),
178
+ "control_cleanliness": round(control_cleanliness, 3),
179
+ }
180
+ # Geometric mean: penalises any dimension being near zero
181
+ product = evidence_gap * sample_adequacy * control_cleanliness
182
+ confidence = round(product ** (1.0 / 3.0), 3) if product > 0 else 0.0
183
+ return confidence, dims
184
+
185
+
186
+ def _extract_per_case_signals(results: dict[str, "Result"]) -> dict[str, bool]:
187
+ """Collect a per-case boolean signal from all per_case finding entries.
188
+
189
+ A case is marked as having a signal if any finding entry for it contains a
190
+ truthy numeric or boolean value (e.g. ``has_loop=True``, ``n_ignored=2``).
191
+ """
192
+ signal: dict[str, bool] = {}
193
+ for result in results.values():
194
+ for entry in result.findings.get("per_case", []):
195
+ cid = entry.get("sample_id") or entry.get("id", "")
196
+ if not cid:
197
+ continue
198
+ hit = any(
199
+ isinstance(v, (int, float, bool)) and bool(v)
200
+ for k, v in entry.items()
201
+ if k not in _NON_SIGNAL_KEYS
202
+ )
203
+ signal[cid] = signal.get(cid, False) or hit
204
+ return signal
205
+
206
+
207
+ class SurgeryAgent:
208
+ """Verify hypotheses by operating on the model or data.
209
+
210
+ Strategy selection (first match wins):
211
+
212
+ 1. ``verify_fn`` injected → full custom override.
213
+ 2. ``analyzer_params`` provided → param sweep (re-run named analyzers).
214
+ 3. ``judge`` provided → :class:`~evalrx.eval_agent.experiment_writer.ExperimentWriter`
215
+ writes a targeted Python script, runs it in a sandbox, and interprets
216
+ the ``verdict`` metric printed to stdout.
217
+ 4. Default → label-correlation analysis (passive, no code execution).
218
+
219
+ Args:
220
+ verify_fn: Custom verification callable.
221
+ analyzer_params: ``{analyzer_name: {param: value}}`` for param sweep.
222
+ judge: Any ``Model`` with ``Capability.GENERATE`` used by the
223
+ experiment writer to write diagnostic code. When ``None``
224
+ the writer is disabled and the agent falls back to label
225
+ correlation.
226
+ sandbox_dir: Directory for sandbox script files. A temp dir is
227
+ created automatically when ``None``. Sets up ONE
228
+ shared sandbox for the agent's lifetime — pass this
229
+ (or omit ``run_context``) to keep that legacy
230
+ behaviour even when a ``RunContext`` is given.
231
+ writer_config: :class:`~evalrx.eval_agent.experiment_writer.ExperimentWriterConfig`
232
+ controlling phases and limits.
233
+ run_context: Optional :class:`~evalrx.eval_agent.run_context.RunContext`.
234
+ When set (and *sandbox_dir* is not), each ``operate()``
235
+ call allocates its own self-contained *trial* under
236
+ ``experiments/`` (code + sandbox + record.md, see
237
+ :meth:`RunContext.new_trial`) instead of every M5
238
+ experiment sharing — and overwriting — one sandbox.
239
+ """
240
+
241
+ def __init__(
242
+ self,
243
+ verify_fn: Callable[
244
+ [Hypothesis, "Model", dict[str, "Result"], CaseBatch],
245
+ InterventionResult,
246
+ ]
247
+ | None = None,
248
+ analyzer_params: dict[str, dict[str, Any]] | None = None,
249
+ judge: "Model | None" = None,
250
+ sandbox_dir: "str | None" = None,
251
+ writer_config: "Any | None" = None,
252
+ run_context: "Any | None" = None,
253
+ ) -> None:
254
+ self.verify_fn = verify_fn
255
+ self.analyzer_params = analyzer_params or {}
256
+
257
+ self._writer = None
258
+ self._sandbox = None
259
+ self._run_context = run_context
260
+ if judge is not None:
261
+ from evalrx.eval_agent.stages.experiment_writer import (
262
+ ExperimentWriter,
263
+ ExperimentWriterConfig,
264
+ )
265
+
266
+ cfg = writer_config if isinstance(writer_config, ExperimentWriterConfig) \
267
+ else ExperimentWriterConfig()
268
+ self._writer = ExperimentWriter(judge=judge, config=cfg)
269
+ if run_context is None or sandbox_dir is not None:
270
+ # No RunContext (or an explicit override): one shared sandbox
271
+ # for the agent's lifetime, exactly as before.
272
+ from evalrx.agent_runtime.sandbox import ExperimentSandbox
273
+
274
+ self._sandbox = ExperimentSandbox(workdir=sandbox_dir)
275
+ # Else: each operate() call allocates its own trial + durable,
276
+ # non-cleaned-up sandbox — see _execute_experiment.
277
+
278
+ def operate(
279
+ self,
280
+ hypothesis: Hypothesis,
281
+ model: "Model",
282
+ results: dict[str, "Result"],
283
+ data: CaseBatch,
284
+ ) -> InterventionResult:
285
+ """Perform the intervention for *hypothesis* and return the outcome."""
286
+ if self.verify_fn is not None:
287
+ return self.verify_fn(hypothesis, model, results, data)
288
+ if self.analyzer_params:
289
+ return self._param_sweep(hypothesis, model, data)
290
+ if self._writer is not None:
291
+ return self._execute_experiment(hypothesis, model, data)
292
+ return self._correlate_with_labels(hypothesis, results, data)
293
+
294
+ # ------------------------------------------------------------------
295
+ # Strategy implementations
296
+ # ------------------------------------------------------------------
297
+
298
+ def _correlate_with_labels(
299
+ self,
300
+ hypothesis: Hypothesis,
301
+ results: dict[str, "Result"],
302
+ data: CaseBatch,
303
+ ) -> InterventionResult:
304
+ """Default: correlate per-case signals with PASS/FAIL labels."""
305
+ signal = _extract_per_case_signals(results)
306
+
307
+ if not signal:
308
+ return InterventionResult(
309
+ hypothesis=hypothesis,
310
+ status=HypothesisStatus.INCONCLUSIVE,
311
+ fixed=False,
312
+ evidence={"reason": "no per-case findings available to correlate"},
313
+ )
314
+
315
+ # Index by both the case UUID and the trajectory sample_id so that
316
+ # per-case entries (which use sample_id) can be matched.
317
+ labeled: dict[str, bool] = {}
318
+ for c in data:
319
+ if getattr(c, "label", None) is None:
320
+ continue
321
+ is_fail = c.label == Label.FAIL
322
+ labeled[c.id] = is_fail
323
+ traj = getattr(c, "trajectory", None)
324
+ if traj is not None:
325
+ labeled[getattr(traj, "sample_id", "")] = is_fail
326
+
327
+ if not labeled:
328
+ return InterventionResult(
329
+ hypothesis=hypothesis,
330
+ status=HypothesisStatus.INCONCLUSIVE,
331
+ fixed=False,
332
+ evidence={"reason": "no labeled cases to correlate with"},
333
+ )
334
+
335
+ with_signal = [labeled[cid] for cid, hit in signal.items() if hit and cid in labeled]
336
+ without_signal = [labeled[cid] for cid, hit in signal.items() if not hit and cid in labeled]
337
+
338
+ if not with_signal:
339
+ return InterventionResult(
340
+ hypothesis=hypothesis,
341
+ status=HypothesisStatus.REFUTED,
342
+ fixed=False,
343
+ evidence={"reason": "no cases match the hypothesis signal"},
344
+ )
345
+
346
+ fail_signal = sum(with_signal) / len(with_signal)
347
+ fail_control = sum(without_signal) / len(without_signal) if without_signal else 0.0
348
+
349
+ evidence = {
350
+ "n_with_signal": len(with_signal),
351
+ "n_without_signal": len(without_signal),
352
+ "fail_rate_signal": round(fail_signal, 3),
353
+ "fail_rate_control": round(fail_control, 3),
354
+ }
355
+
356
+ confidence, dims = _compute_confidence(
357
+ fail_signal, fail_control, len(with_signal), len(without_signal)
358
+ )
359
+
360
+ if fail_signal > fail_control + 0.10:
361
+ status = HypothesisStatus.SUPPORTED
362
+ fixed = fail_signal >= 1.0 and fail_control == 0.0
363
+ elif fail_signal < fail_control - 0.05:
364
+ status = HypothesisStatus.REFUTED
365
+ fixed = False
366
+ else:
367
+ status = HypothesisStatus.INCONCLUSIVE
368
+ fixed = False
369
+
370
+ new_data: CaseBatch | None = None
371
+ if status == HypothesisStatus.SUPPORTED:
372
+ new_data = CaseBatch([c for c in data if not signal.get(c.id, False)])
373
+
374
+ return InterventionResult(
375
+ hypothesis=hypothesis,
376
+ status=status,
377
+ fixed=fixed,
378
+ evidence=evidence,
379
+ new_data=new_data,
380
+ confidence_score=confidence,
381
+ evidence_dimensions=dims,
382
+ )
383
+
384
+ def _execute_experiment(
385
+ self,
386
+ hypothesis: Hypothesis,
387
+ model: "Model",
388
+ data: CaseBatch,
389
+ ) -> InterventionResult:
390
+ """M5 strategy 3: write + execute a targeted diagnostic script.
391
+
392
+ Mirrors ``researchclaw`` Stage-14 diagnosis + repair loop:
393
+
394
+ 1. :class:`~evalrx.eval_agent.experiment_writer.ExperimentWriter`
395
+ generates a self-contained Python script via the LLM judge.
396
+ 2. The script is run in a subprocess sandbox (exec-fix loop).
397
+ 3. ``verdict: 1.0`` → SUPPORTED; ``verdict: 0.0`` → REFUTED;
398
+ no verdict or crash → INCONCLUSIVE.
399
+ """
400
+ from evalrx.eval_agent.stages.experiment_writer import build_model_context
401
+
402
+ sandbox = self._sandbox
403
+ trial = None
404
+ if sandbox is None and self._run_context is not None:
405
+ from evalrx.agent_runtime.sandbox import ExperimentSandbox
406
+
407
+ label = getattr(hypothesis, "predicted_failure_mode", "") or "experiment"
408
+ trial = self._run_context.new_trial("experiments", label)
409
+ # cleanup=False: a successful run's script must stay on disk —
410
+ # it's the whole point of giving this experiment its own folder.
411
+ sandbox = ExperimentSandbox(workdir=str(trial.workspace), cleanup=False)
412
+
413
+ # The model under test is already resident in the parent process. A
414
+ # second evalrx.load() in M5 can duplicate tens of GB of weights
415
+ # and OOM (notably Qwen Omni). Generated diagnostics therefore operate
416
+ # on frozen case artifacts; fresh interventions remain host-run M4/fix
417
+ # work and an artifact-insufficient M5 result is INCONCLUSIVE.
418
+ model_context = build_model_context(model, allow_reconstruction=False)
419
+ # Save images alongside cases.json so codex can load them
420
+ image_dir = getattr(sandbox, "workdir", None)
421
+ cases_json = _serialize_cases(data, image_dir=image_dir)
422
+
423
+ writer_result = self._writer.write_and_run( # type: ignore[union-attr]
424
+ hypothesis=hypothesis,
425
+ model_context=model_context,
426
+ cases_json=cases_json,
427
+ sandbox=sandbox, # type: ignore[arg-type]
428
+ )
429
+
430
+ evidence: dict[str, Any] = {
431
+ "metrics": writer_result.metrics,
432
+ "returncode": writer_result.returncode,
433
+ "timed_out": writer_result.timed_out,
434
+ "llm_calls": writer_result.total_llm_calls,
435
+ "sandbox_runs": writer_result.total_sandbox_runs,
436
+ "validation_log": writer_result.validation_log,
437
+ }
438
+
439
+ # Full experiment payload for RunLogger.log_experiment — the generated
440
+ # script(s), the run's output, the agent's thinking, and the workspace.
441
+ experiment: dict[str, Any] = {
442
+ "module": "m5",
443
+ "provider": writer_result.provider,
444
+ "code": writer_result.code,
445
+ "files": writer_result.files,
446
+ "blueprint": writer_result.blueprint,
447
+ "stdout": writer_result.stdout,
448
+ "stderr": writer_result.stderr,
449
+ "verdict": writer_result.verdict,
450
+ "metrics": writer_result.metrics,
451
+ "returncode": writer_result.returncode,
452
+ "timed_out": writer_result.timed_out,
453
+ "validation_log": writer_result.validation_log,
454
+ "cli_raw_output": writer_result.cli_raw_output,
455
+ "cli_usage": writer_result.cli_usage,
456
+ "llm_calls": writer_result.total_llm_calls,
457
+ "sandbox_runs": writer_result.total_sandbox_runs,
458
+ "model_calls": writer_result.model_calls,
459
+ "workdir": writer_result.workdir,
460
+ # Self-contained trial folder (see RunContext.new_trial) — None
461
+ # when no RunContext is in play (legacy flat layout).
462
+ "trial_root": str(trial.root) if trial is not None else None,
463
+ }
464
+
465
+ # Crashed or timed out with no metrics → inconclusive
466
+ if not writer_result.ok and not writer_result.metrics:
467
+ return InterventionResult(
468
+ hypothesis=hypothesis,
469
+ status=HypothesisStatus.INCONCLUSIVE,
470
+ fixed=False,
471
+ evidence={**evidence, "reason": "script did not produce metrics"},
472
+ experiment=experiment,
473
+ )
474
+
475
+ verdict = writer_result.verdict
476
+ if verdict is None:
477
+ return InterventionResult(
478
+ hypothesis=hypothesis,
479
+ status=HypothesisStatus.INCONCLUSIVE,
480
+ fixed=False,
481
+ evidence={**evidence, "reason": "no verdict line in output"},
482
+ experiment=experiment,
483
+ )
484
+
485
+ if verdict >= 0.5:
486
+ status = HypothesisStatus.SUPPORTED
487
+ # "Fixed" means confidence is very high (verdict == 1.0 and high confidence)
488
+ confidence = writer_result.metrics.get("confidence", verdict)
489
+ fixed = verdict >= 1.0 and confidence >= 0.9
490
+ else:
491
+ status = HypothesisStatus.REFUTED
492
+ fixed = False
493
+
494
+ return InterventionResult(
495
+ hypothesis=hypothesis,
496
+ status=status,
497
+ fixed=fixed,
498
+ evidence=evidence,
499
+ experiment=experiment,
500
+ )
501
+
502
+ def _param_sweep(
503
+ self,
504
+ hypothesis: Hypothesis,
505
+ model: "Model",
506
+ data: CaseBatch,
507
+ ) -> InterventionResult:
508
+ """Re-run specified analyzers with modified parameters."""
509
+ from evalrx.eval_agent._tools import run_analysis
510
+
511
+ sweep: dict[str, Any] = {}
512
+ for analyzer_name, params in self.analyzer_params.items():
513
+ try:
514
+ result = run_analysis(model, analyzer_name, data, **params)
515
+ sweep[analyzer_name] = result.findings
516
+ except Exception as exc:
517
+ sweep[analyzer_name] = {"error": str(exc)}
518
+
519
+ return InterventionResult(
520
+ hypothesis=hypothesis,
521
+ status=HypothesisStatus.INCONCLUSIVE,
522
+ fixed=False,
523
+ evidence={"param_sweep": sweep},
524
+ )