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,208 @@
1
+ """Shared run-provenance and logging-wiring helpers used by both
2
+ ``VLDiagnoseLoop`` (loop.py) and ``AutoDiagnoseLoop`` (legacy.py).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from typing import Any
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def _attach_run_logger(run_logger: Any, *agents: Any) -> None:
14
+ """Point each agent's ``run_logger`` at *run_logger* when it is unset.
15
+
16
+ Lets the probe / stats generators emit ``tool_codegen`` events without the
17
+ caller having to wire the logger into every agent by hand. Respects an
18
+ explicitly-set logger (only fills in ``None``).
19
+ """
20
+ if run_logger is None:
21
+ return
22
+ for agent in agents:
23
+ if agent is not None and getattr(agent, "run_logger", None) is None:
24
+ try:
25
+ agent.run_logger = run_logger
26
+ except Exception: # noqa: BLE001 - never let logging wiring break a run
27
+ pass
28
+
29
+
30
+ def _data_provenance(data: Any) -> dict[str, Any]:
31
+ """Fingerprint the case batch + tally its labels for the ``run_start`` record.
32
+
33
+ Returns ``{"data_fingerprint": <12-hex>, "label_distribution": {...}}`` — the
34
+ fingerprint is a SHA-1 over each case's ``id`` (or its prompt when no id),
35
+ order-independent, so the same batch always hashes the same regardless of
36
+ iteration order, and a different batch (even same size) hashes differently.
37
+ Best-effort: returns ``{}`` if *data* isn't iterable.
38
+ """
39
+ import hashlib
40
+ from collections import Counter
41
+
42
+ try:
43
+ cases = list(data)
44
+ except Exception: # noqa: BLE001
45
+ return {}
46
+
47
+ keys: list[str] = []
48
+ labels: Counter = Counter()
49
+ for c in cases:
50
+ cid = getattr(c, "id", None)
51
+ if not cid:
52
+ prompt = getattr(getattr(c, "inputs", None), "prompt", None)
53
+ cid = str(prompt) if prompt is not None else repr(c)
54
+ keys.append(str(cid))
55
+ label = getattr(c, "label", None)
56
+ labels[getattr(label, "name", "UNKNOWN")] += 1
57
+
58
+ out: dict[str, Any] = {}
59
+ if keys:
60
+ digest = hashlib.sha1("\n".join(sorted(keys)).encode("utf-8")).hexdigest()
61
+ out["data_fingerprint"] = digest[:12]
62
+ if labels:
63
+ out["label_distribution"] = dict(labels)
64
+ return out
65
+
66
+
67
+ def _run_config(loop: Any, data: Any, *, loop_name: str) -> dict[str, Any]:
68
+ """Build the ``run_start`` provenance dict from a loop instance + its data.
69
+
70
+ Robust to both loop types and missing agents — every field is best-effort so
71
+ a partially-configured loop still produces a useful config record.
72
+ """
73
+ cfg: dict[str, Any] = {"loop": loop_name}
74
+ try:
75
+ cfg["model"] = repr(loop.model)
76
+ except Exception: # noqa: BLE001
77
+ pass
78
+ cfg["max_cycles"] = getattr(loop, "max_cycles", None)
79
+ cfg["token_budget"] = getattr(loop, "token_budget", None)
80
+ cfg["analysis_only"] = getattr(loop, "analysis_only", None)
81
+ try:
82
+ cfg["n_cases"] = len(data)
83
+ except Exception: # noqa: BLE001
84
+ pass
85
+ # Dataset provenance: a stable fingerprint over the cases plus their label
86
+ # breakdown. n_cases alone says how many; this says *which* (so two runs
87
+ # can be confirmed to use the same batch) and the base failure rate the
88
+ # whole diagnosis is conditioned on — both essential to interpret the run.
89
+ _data_prov = _data_provenance(data)
90
+ if _data_prov:
91
+ cfg.update(_data_prov)
92
+ # How the batch was partitioned before anything ran. ``n_cases`` above is
93
+ # the explore partition (``data`` has already been narrowed to it); these
94
+ # say whether a confirm / test partition was withheld and at what fraction,
95
+ # so a report can tell a two-way run from a train/val/test one.
96
+ for attr in ("confirm_split", "test_split", "confirm_split_seed"):
97
+ value = getattr(loop, attr, None)
98
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
99
+ cfg[attr] = value
100
+
101
+ protocol = getattr(loop, "protocol", None)
102
+ if protocol is not None:
103
+ cfg["protocol"] = {
104
+ "description": getattr(protocol, "description", ""),
105
+ "task_domain": getattr(protocol, "task_domain", None),
106
+ }
107
+
108
+ # Judge — read off the M3 agent (or its lazy default) when present.
109
+ diag_agent = getattr(loop, "diagnosis_agent", None)
110
+ judge = getattr(diag_agent, "judge", None) if diag_agent is not None else None
111
+ if judge is not None:
112
+ cfg["judge"] = repr(judge)
113
+
114
+ # Coder — the M5 surgery writer's CLI provider/model, when configured.
115
+ surgery = getattr(loop, "surgery_agent", None)
116
+ writer = getattr(surgery, "_writer", None) if surgery is not None else None
117
+ cli = getattr(getattr(writer, "_cfg", None), "cli_agent", None)
118
+ if cli is not None:
119
+ provider = getattr(cli, "provider", None)
120
+ model = getattr(cli, "model", "")
121
+ if provider:
122
+ cfg["coder"] = f"{provider}:{model}" if model else provider
123
+
124
+ # Whether tool synthesis (codegen) is enabled on either agent.
125
+ cfg["allow_codegen"] = bool(
126
+ getattr(getattr(loop, "probe_agent", None), "allow_codegen", False)
127
+ or getattr(getattr(loop, "stats_agent", None), "_allow_codegen", False)
128
+ or getattr(getattr(loop, "fix_agent", None), "_allow_codegen", False)
129
+ )
130
+ # Whether the in-cycle explore step (free-form EDA beside M2) is configured —
131
+ # so a run with explorer notes in M3 is distinguishable from one without.
132
+ explorer = getattr(loop, "explorer", None)
133
+ if explorer is not None:
134
+ cli = getattr(explorer, "_cli_config", None)
135
+ provider = getattr(cli, "provider", None)
136
+ model = getattr(cli, "model", "")
137
+ cfg["explorer"] = (
138
+ (f"{provider}:{model}" if model else provider) if provider
139
+ else type(explorer).__name__
140
+ )
141
+ return cfg
142
+
143
+
144
+ def _log_generated_tools(run_logger: Any, cycle: int, module: str, agent: Any) -> None:
145
+ """Snapshot an agent's active generated-tool registry for *cycle*.
146
+
147
+ Reads ``_generated_probes`` (list of ``(generator, probe)`` tuples) for the
148
+ probe agent or ``_generated_tools`` (list of tools) for the stats agent and
149
+ forwards the bare tool objects to :meth:`RunLogger.log_tool_registry`.
150
+ Best-effort: silently does nothing for agents without those attributes.
151
+ """
152
+ if agent is None:
153
+ return
154
+ raw = getattr(agent, "_generated_probes", None)
155
+ if raw is None:
156
+ raw = getattr(agent, "_generated_tools", None)
157
+ if not raw:
158
+ return
159
+ tools = [t[1] if isinstance(t, tuple) else t for t in raw]
160
+ try:
161
+ run_logger.log_tool_registry(cycle, module, tools)
162
+ except Exception as exc: # noqa: BLE001
163
+ logger.debug("log_tool_registry failed: %s", exc)
164
+
165
+
166
+ def _coerce_explore_context(value: "Any | None") -> "Any | None":
167
+ """Normalise an explore_report argument into an ExploreContext or None.
168
+
169
+ Accepts an ``ExploreContext``, a report dict (e.g. ``fused_report.json``),
170
+ or ``None``. Never raises — a malformed value degrades to ``None``."""
171
+ if value is None:
172
+ return None
173
+ try:
174
+ from evalrx.eval_agent.stages.diagnosis import ExploreContext
175
+
176
+ if isinstance(value, ExploreContext):
177
+ return None if value.is_empty else value
178
+ if isinstance(value, dict):
179
+ return ExploreContext.from_report(value)
180
+ except Exception as exc: # explore context is optional, never fatal
181
+ logger.warning("could not build ExploreContext from explore_report: %s", exc)
182
+ return None
183
+
184
+
185
+ # ---------------------------------------------------------------------------
186
+ # Internal adapter: convert HypothesisTestResult → InterventionResult-like
187
+ # object for RunLogger.log_surgery (which expects an InterventionResult).
188
+ # ---------------------------------------------------------------------------
189
+
190
+ def _make_intervention_result_from_test(tr: "Any") -> Any:
191
+ """Wrap a HypothesisTestResult as a minimal InterventionResult."""
192
+ from evalrx.eval_agent.stages.surgery import InterventionResult
193
+
194
+ return InterventionResult(
195
+ hypothesis=tr.hypothesis,
196
+ status=tr.status,
197
+ fixed=False,
198
+ evidence={
199
+ "m4_test_name": tr.test_name,
200
+ "m4_effect_size": tr.effect_size,
201
+ "m4_confidence": tr.confidence,
202
+ "m4_protocol_consistent": tr.is_consistent_with_protocol,
203
+ "m4_verdict": tr.verdict,
204
+ "m4_evidence_grade": tr.evidence_grade,
205
+ **tr.evidence,
206
+ },
207
+ confidence_score=tr.confidence,
208
+ )
@@ -0,0 +1,56 @@
1
+ """Stage implementations for the M1–M4 diagnostic pipeline.
2
+
3
+ Each module is a self-contained stage that can be injected into either loop:
4
+
5
+ AutoDiagnoseLoop uses M1 → M2 → M3 → M5
6
+ VLDiagnoseLoop uses M1 → M2 → M3 → M4 (inner), M5 called post-loop
7
+
8
+ M1 — Probe (analyzer selection + execution)
9
+ probe.py StrategyProbe detect model kind; rank analyzers by failure-mode hints
10
+ probe_agent.py ProbeAgent execute ranked analyzers; route to Docker if needed;
11
+ protocol-guided via LLM analyzer selection
12
+ protocol.py ExperimentProtocol user's NL description of what to investigate;
13
+ ProbingSchema records which analyzers ran and why (M2/M4 trace)
14
+
15
+ Data — Case discovery / labeling
16
+ case_discovery.py CaseDiscoveryAgent run candidate prompts through the model,
17
+ store observed outputs, and label PASS/FAIL
18
+
19
+ M2 — Analysis (statistical interpretation)
20
+ Lives in ``evalrx.analysis`` (not this package) so it is usable standalone:
21
+ analysis_module.py AnalysisModule threshold rules + VLM derived metrics → AnalysisReport
22
+ stats_agent.py StatsAnalysisAgent protocol-aware extension of AnalysisModule;
23
+ adds LLM-written conclusion + evidence chain
24
+ StatsAnalysisReport AnalysisReport subclass — adds conclusion,
25
+ evidence_chain, qualitative_findings, protocol
26
+ stats_tool_agent.py StatsToolAgent selects/runs deterministic exploratory
27
+ stats tools and visualization specs
28
+ stats_tools.py STATS_TOOL_CATALOG signal/label association, McNemar+e-value,
29
+ Friedman, single-rate e-value, rank corr
30
+ stats_tool_generator.py StatsToolGenerator LLM/CLI writes a new stats script when
31
+ no catalog tool fits
32
+
33
+ M3 — Diagnosis (hypothesis generation)
34
+ diagnosis.py DiagnosisAgent LLM judge reads AnalysisReport → Hypothesis list;
35
+ falls back to threshold-derived hypotheses on NO_ISSUE
36
+
37
+ M5 — Surgery (fix proposal + execution)
38
+ surgery.py SurgeryAgent four strategies: verify_fn → analyzer_params →
39
+ ExperimentWriter → label correlation
40
+ experiment_writer.py ExperimentWriter multi-phase LLM/CLI agent writes + runs
41
+ diagnostic Python projects in a sandbox
42
+
43
+ M4 — Hypothesis testing (statistical + protocol consistency gate)
44
+ hypothesis_tester.py HypothesisTester per-case signal extraction → fail-rate comparison
45
+ → optional LLM protocol consistency check;
46
+ stopping_criteria_met() returns True when at
47
+ least one hypothesis is SUPPORTED + consistent
48
+
49
+ Cross-stage shared types (top level, not in this package):
50
+ hypothesis.py Hypothesis, HypothesisStatus used by M3, M4, M5
51
+
52
+ Shared CLI-agent runtime (evalrx.agent_runtime, not in this package):
53
+ codegen/ CodegenRunner shared CLI code-generation orchestration
54
+ providers/ CLI coding-provider adapters (agy/codex/claude/etc.)
55
+ judges/ CLI-backed judge model wrappers
56
+ """
@@ -0,0 +1,293 @@
1
+ """Case discovery and labeling for automated VL diagnosis loops.
2
+
3
+ This stage turns candidate prompts into labeled cases by running the target
4
+ model, storing its observed answer, and scoring the result. It is intentionally
5
+ small: dataset loading and prompt generation can live outside this class, while
6
+ M4 receives the PASS/FAIL labels it needs for statistical testing.
7
+ """
8
+
9
+ import json
10
+ import re
11
+ from dataclasses import dataclass, field
12
+ from typing import TYPE_CHECKING, Any, Callable, Iterable
13
+
14
+ from evalrx.core.case import CaseBatch, FailureCase, Label
15
+ from evalrx.eval_agent.prompts.case_discovery import _JUDGE_PROMPT
16
+
17
+ if TYPE_CHECKING:
18
+ from evalrx.core.model import Model
19
+ from evalrx.eval_agent.stages.protocol import ExperimentProtocol
20
+
21
+
22
+ ScoreFn = Callable[[FailureCase, str], Label | bool | str]
23
+
24
+
25
+ @dataclass
26
+ class CaseDiscoveryReport:
27
+ """Summary of one discovery pass."""
28
+
29
+ cases: CaseBatch
30
+ n_pass: int = 0
31
+ n_fail: int = 0
32
+ n_unknown: int = 0
33
+ errors: list[str] = field(default_factory=list)
34
+
35
+ @property
36
+ def has_m4_groups(self) -> bool:
37
+ """True when M4 has both failure and control examples."""
38
+ return self.n_fail > 0 and self.n_pass > 0
39
+
40
+
41
+ class CaseDiscoveryAgent:
42
+ """Run candidate cases through a model and label PASS/FAIL outcomes.
43
+
44
+ Args:
45
+ scorer: Deterministic scoring function. It receives the
46
+ case and observed answer and returns ``Label``, bool,
47
+ or a label string.
48
+ judge: Optional LLM judge used when no scorer is supplied.
49
+ generation_kwargs: Extra kwargs forwarded to ``model.generate``.
50
+ include_unknown: Keep UNKNOWN cases in the returned batch.
51
+ concurrency: How many cases may be generated at once. Only the
52
+ ``model.generate`` calls fan out; scoring and
53
+ labelling stay sequential and in input order, so the
54
+ report is identical whatever this is set to.
55
+
56
+ Values above 1 are honoured ONLY for an API-backed
57
+ handle, following the same rule as
58
+ :func:`evalrx.models.agent.run_batch`: HTTP is
59
+ reentrant, a local backend sharing one GPU is not.
60
+ Against a local ``vllm serve`` this is the
61
+ difference between one request in flight and a
62
+ batch -- the server can only batch what it has been
63
+ given.
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ scorer: ScoreFn | None = None,
69
+ judge: "Model | None" = None,
70
+ generation_kwargs: dict[str, Any] | None = None,
71
+ include_unknown: bool = True,
72
+ concurrency: int = 1,
73
+ ) -> None:
74
+ self.scorer = scorer
75
+ self.judge = judge
76
+ self.generation_kwargs = generation_kwargs or {}
77
+ self.include_unknown = include_unknown
78
+ self.concurrency = max(1, int(concurrency))
79
+
80
+ def discover(
81
+ self,
82
+ model: "Model",
83
+ candidates: Iterable[FailureCase] | CaseBatch,
84
+ *,
85
+ protocol: "ExperimentProtocol | None" = None,
86
+ ) -> CaseDiscoveryReport:
87
+ """Return a labeled batch created from candidate cases."""
88
+ labeled: list[FailureCase] = []
89
+ errors: list[str] = []
90
+ counts = {Label.PASS: 0, Label.FAIL: 0, Label.UNKNOWN: 0}
91
+ cases = list(candidates)
92
+
93
+ for case, observed, gen_error in self._generate(model, cases):
94
+ try:
95
+ if gen_error is not None:
96
+ raise gen_error
97
+ case.observed = observed
98
+ label, reason = self._score_with_reason(case, str(observed), protocol)
99
+ except Exception as exc: # noqa: BLE001 - discovery should continue
100
+ observed = ""
101
+ label = Label.UNKNOWN
102
+ reason = "model generation or scoring raised an exception"
103
+ errors.append(f"{case.id}: {exc}")
104
+ case.metadata["discovery_error"] = str(exc)
105
+
106
+ case.label = label
107
+ case.metadata["discovery_observed"] = str(observed)
108
+ case.metadata["discovery_label"] = label.value
109
+ case.metadata["discovery_reason"] = reason
110
+ counts[label] += 1
111
+
112
+ if label != Label.UNKNOWN or self.include_unknown:
113
+ labeled.append(case)
114
+
115
+ return CaseDiscoveryReport(
116
+ cases=CaseBatch(labeled),
117
+ n_pass=counts[Label.PASS],
118
+ n_fail=counts[Label.FAIL],
119
+ n_unknown=counts[Label.UNKNOWN],
120
+ errors=errors,
121
+ )
122
+
123
+ def _generate(
124
+ self, model: "Model", cases: list[FailureCase]
125
+ ) -> "list[tuple[FailureCase, Any, Exception | None]]":
126
+ """``(case, observed, error)`` per case, in input order.
127
+
128
+ The exception is carried rather than raised so the caller keeps its one
129
+ place that turns any failure into an UNKNOWN label -- a case whose
130
+ generation failed and a case whose scoring failed are the same
131
+ observation, and both must leave the batch intact.
132
+ """
133
+ workers = self._workers(model, len(cases))
134
+ if workers <= 1:
135
+ out = []
136
+ for case in cases:
137
+ try:
138
+ out.append((case, model.generate(case.inputs, **self.generation_kwargs), None))
139
+ except Exception as exc: # noqa: BLE001
140
+ out.append((case, None, exc))
141
+ return out
142
+
143
+ from concurrent.futures import ThreadPoolExecutor
144
+
145
+ def _one(case: FailureCase):
146
+ try:
147
+ return case, model.generate(case.inputs, **self.generation_kwargs), None
148
+ except Exception as exc: # noqa: BLE001
149
+ return case, None, exc
150
+
151
+ with ThreadPoolExecutor(max_workers=workers) as pool:
152
+ return list(pool.map(_one, cases))
153
+
154
+ def _workers(self, model: "Model", n_cases: int) -> int:
155
+ """Concurrency actually allowed for this handle.
156
+
157
+ A local backend is silently forced back to 1 rather than warned about:
158
+ unlike ``run_batch``, nothing here asked for parallelism per case --
159
+ the caller set one number for a whole run that may mix handles, and a
160
+ warning per discovery pass would be noise about a setting that is
161
+ behaving correctly.
162
+ """
163
+ if self.concurrency <= 1:
164
+ return 1
165
+ from evalrx.models.backends.api import APIModel
166
+
167
+ # A deployed-pipeline wrapper (fix_tools.SpecPipelineModel) is exactly
168
+ # as reentrant as the handle it drives — judge it by its inner model.
169
+ model = getattr(model, "inner_model", model)
170
+ # InstrumentedModel is also a transparent proxy; preserve the API
171
+ # handle's configured discovery concurrency while recording each call.
172
+ model = getattr(model, "_model", model)
173
+ if not isinstance(model, APIModel):
174
+ return 1
175
+ return min(self.concurrency, max(1, n_cases))
176
+
177
+ def _score_with_reason(
178
+ self,
179
+ case: FailureCase,
180
+ observed: str,
181
+ protocol: "ExperimentProtocol | None",
182
+ ) -> tuple[Label, str]:
183
+ if self.scorer is not None:
184
+ label = _coerce_label(self.scorer(case, observed))
185
+ return label, "scored by injected scorer"
186
+ if self.judge is not None:
187
+ judged, reason = self._judge_score(case, observed, protocol)
188
+ if judged != Label.UNKNOWN:
189
+ return judged, reason
190
+ fallback = _heuristic_score(case.expected, observed)
191
+ return fallback, f"judge returned UNKNOWN; heuristic fallback produced {fallback.value}"
192
+ label = _heuristic_score(case.expected, observed)
193
+ return label, "scored by expected-answer heuristic"
194
+
195
+ def _judge_score(
196
+ self,
197
+ case: FailureCase,
198
+ observed: str,
199
+ protocol: "ExperimentProtocol | None",
200
+ ) -> tuple[Label, str]:
201
+ protocol_text = protocol.description if protocol is not None else ""
202
+ success_criteria = protocol.success_criteria if protocol is not None else ""
203
+ prompt = _JUDGE_PROMPT.format(
204
+ protocol=protocol_text,
205
+ success_criteria=success_criteria,
206
+ prompt=case.inputs.prompt,
207
+ expected=case.expected,
208
+ observed=observed,
209
+ )
210
+ raw = self.judge.generate(prompt) # type: ignore[union-attr]
211
+ text = str(raw).strip()
212
+ parsed = _parse_judge_json(text)
213
+ if parsed is not None:
214
+ return parsed
215
+ first_lines = "\n".join(text.splitlines()[:5]).upper()
216
+ if "PASS" in first_lines and "FAIL" not in first_lines:
217
+ return Label.PASS, "judge text contained PASS"
218
+ if "FAIL" in first_lines and "PASS" not in first_lines:
219
+ return Label.FAIL, "judge text contained FAIL"
220
+ first = text.splitlines()[0].strip().upper() if text else ""
221
+ if first.startswith("PASS"):
222
+ return Label.PASS, "judge first line was PASS"
223
+ if first.startswith("FAIL"):
224
+ return Label.FAIL, "judge first line was FAIL"
225
+ return Label.UNKNOWN, "judge output did not contain a parseable label"
226
+
227
+
228
+ def _coerce_label(value: Label | bool | str) -> Label:
229
+ if isinstance(value, Label):
230
+ return value
231
+ if isinstance(value, bool):
232
+ return Label.PASS if value else Label.FAIL
233
+ text = str(value).strip().lower()
234
+ if text in {"pass", "passed", "true", "correct", "ok"}:
235
+ return Label.PASS
236
+ if text in {"fail", "failed", "false", "incorrect", "wrong"}:
237
+ return Label.FAIL
238
+ return Label.UNKNOWN
239
+
240
+
241
+ def _parse_judge_json(text: str) -> tuple[Label, str] | None:
242
+ raw = text.strip()
243
+ if raw.startswith("```"):
244
+ raw = re.sub(r"^```\w*\n?", "", raw)
245
+ raw = re.sub(r"\n?```\s*$", "", raw)
246
+ match = re.search(r"\{.*\}", raw, flags=re.DOTALL)
247
+ if not match:
248
+ return None
249
+ try:
250
+ payload = json.loads(match.group())
251
+ except json.JSONDecodeError:
252
+ return None
253
+ label = _coerce_label(payload.get("label", "unknown"))
254
+ reason = str(payload.get("reason", "judge returned structured label"))
255
+ return label, reason
256
+
257
+
258
+ def _heuristic_score(expected: Any, observed: str) -> Label:
259
+ """Score simple expected-answer rubrics without an LLM judge.
260
+
261
+ Supported expected formats:
262
+ - string: substring match after normalization
263
+ - list/tuple/set: every item must appear
264
+ - dict: ``all_of``, ``any_of``, and ``none_of`` substring constraints
265
+ """
266
+ if expected is None:
267
+ return Label.UNKNOWN
268
+
269
+ obs = _normalize(observed)
270
+ if isinstance(expected, str):
271
+ return Label.PASS if _normalize(expected) in obs else Label.FAIL
272
+
273
+ if isinstance(expected, (list, tuple, set)):
274
+ terms = [_normalize(x) for x in expected]
275
+ return Label.PASS if all(t in obs for t in terms if t) else Label.FAIL
276
+
277
+ if isinstance(expected, dict):
278
+ all_of = [_normalize(x) for x in expected.get("all_of", [])]
279
+ any_of = [_normalize(x) for x in expected.get("any_of", [])]
280
+ none_of = [_normalize(x) for x in expected.get("none_of", [])]
281
+ if any(t and t in obs for t in none_of):
282
+ return Label.FAIL
283
+ if all_of and not all(t in obs for t in all_of if t):
284
+ return Label.FAIL
285
+ if any_of and not any(t in obs for t in any_of if t):
286
+ return Label.FAIL
287
+ return Label.PASS
288
+
289
+ return Label.UNKNOWN
290
+
291
+
292
+ def _normalize(text: Any) -> str:
293
+ return re.sub(r"\s+", " ", str(text).lower()).strip()