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,499 @@
1
+ """L3b fix executors — internals-**modifying** repairs (the sandbox boundary).
2
+
3
+ This module holds only what genuinely cannot be handed to a sandboxed coding
4
+ agent: pre-audited, parameterised primitives that **write** to the forward pass.
5
+
6
+ * **L3a (internals, read)** is intentionally NOT here at all. Reading attention
7
+ needs no privileged model handle, so the capability is exposed to sandboxed
8
+ coded pipelines via ``model_attend()`` (see :mod:`fix_pipeline`), and the
9
+ agent writes its own peak-find → crop → re-ask scaffold. The host-side
10
+ capture that backs ``model_attend()`` is
11
+ :func:`~evalrx.analyzers.attention.relative_attn.attention_heatmap` — a
12
+ generic attention reducer that lives with the analyzers (one reduction shared
13
+ with the white-box probe path), not in this fix module. (An earlier
14
+ ``attention_guided_crop`` primitive was removed for the same reason — it
15
+ duplicated what the coded path already writes.)
16
+ * **L3b (internals, write)**: pre-audited intervention primitives that modify the
17
+ forward pass — **never** free codegen against the model handle, because
18
+ arbitrary hook code with the raw model object cannot be sandboxed. The judge
19
+ selects and parameterises; it never authors these. v1 ships *visual embedding
20
+ boost*: a forward hook on the input-embedding layer scaling image-token
21
+ embeddings by ``gamma`` (architecture-agnostic for HF VLMs whose image tokens
22
+ are placeholder ids in ``input_ids``). Attention-map editing needs
23
+ per-architecture hooks and joins this registry later.
24
+
25
+ L4 (parameter space): :class:`FinetuneSpec` captures a complete fine-tune
26
+ recipe (dataset construction generalising the verified hypothesis, method,
27
+ target, evaluation protocol incl. a regression battery). v1 of the executor
28
+ (:func:`run_lora_repair`) executes exactly one recipe shape — ``method="lora"``
29
+ on ``target="llm"`` — and refuses everything else with a descriptive reason
30
+ rather than guessing:
31
+
32
+ * ``dataset_recipe`` (free natural-language text) is **never interpreted**.
33
+ There is no safe path from an arbitrary judge-written string to executable
34
+ data synthesis without another judge call producing code — the same
35
+ structured-output path whose failures cost three real bugs elsewhere in
36
+ this project (see the ``vlm_paper_benchmark`` example's README). Training
37
+ data is instead a fixed, defensible default: diagnosis-split failing cases
38
+ as ``(prompt, image) -> gold answer`` SFT pairs, plus a sample of
39
+ diagnosis-split *passing* cases as anti-forgetting ballast — supplied by
40
+ the caller via a pool that must be disjoint from whatever batch is being
41
+ validated (see :class:`~.fix_agent.FixAgent`'s ``finetune_pool``).
42
+ * ``method in {"sft", "full"}`` and ``target in {"vision_encoder",
43
+ "projector"}`` are recorded (for the escalation decision) but not
44
+ executed — the former needs a real SFT training loop rather than a
45
+ handful of LoRA steps, the latter needs per-architecture module-naming
46
+ conventions this project has no way to validate without a training run
47
+ per architecture. Both are TODO.
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ import logging
53
+ from contextlib import contextmanager
54
+ from dataclasses import dataclass, field
55
+ from typing import TYPE_CHECKING, Any, Callable, Optional
56
+
57
+ from evalrx.eval_agent.stages.fix_tiers import FixTier
58
+ from evalrx.eval_agent.stages.fix_tools import score_to_bool
59
+
60
+ if TYPE_CHECKING:
61
+ from evalrx.core.case import CaseBatch, FailureCase
62
+ from evalrx.core.model import Model
63
+
64
+ logger = logging.getLogger(__name__)
65
+
66
+ ScoreFn = Callable[["FailureCase", str], Optional[bool]]
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # L3b — visual embedding boost (forward-hook intervention)
71
+ # ---------------------------------------------------------------------------
72
+
73
+ def _resolve_hf(model: "Model"):
74
+ """Underlying (HF model, image_token_id) or (None, None) when unavailable."""
75
+ hf = getattr(model, "_hf", None)
76
+ hf_model = hf[0] if isinstance(hf, tuple) and hf else None
77
+ if hf_model is None:
78
+ return None, None
79
+ cfg = getattr(hf_model, "config", None)
80
+ for attr in ("image_token_id", "image_token_index"):
81
+ tid = getattr(cfg, attr, None)
82
+ if tid is not None:
83
+ return hf_model, int(tid)
84
+ return None, None
85
+
86
+
87
+ def boost_available(model: "Model") -> bool:
88
+ return _resolve_hf(model)[0] is not None
89
+
90
+
91
+ @contextmanager
92
+ def visual_embedding_boost(model: "Model", gamma: float = 1.5):
93
+ """Scale image-token embeddings by *gamma* for every forward inside the block."""
94
+ hf_model, image_token_id = _resolve_hf(model)
95
+ if hf_model is None:
96
+ raise RuntimeError(
97
+ "visual_embedding_boost: backend internals unavailable "
98
+ "(needs a loaded hf_local model exposing image_token_id)"
99
+ )
100
+ embedding = hf_model.get_input_embeddings()
101
+ gamma = float(gamma)
102
+
103
+ def _hook(module, args, output):
104
+ input_ids = args[0]
105
+ mask = input_ids == image_token_id
106
+ if mask.any():
107
+ output = output.clone()
108
+ output[mask] = output[mask] * gamma
109
+ return output
110
+
111
+ handle = embedding.register_forward_hook(_hook)
112
+ try:
113
+ yield
114
+ finally:
115
+ handle.remove()
116
+
117
+
118
+ def run_visual_embedding_boost(
119
+ model: "Model",
120
+ cases: "CaseBatch",
121
+ score_fn: ScoreFn,
122
+ params: "dict[str, Any] | None" = None,
123
+ ) -> "dict[str, Optional[bool]]":
124
+ """Generate every case under the boost hook; score against the rubric."""
125
+ gamma = float((params or {}).get("gamma", 1.5))
126
+ scores: "dict[str, Optional[bool]]" = {}
127
+ try:
128
+ with visual_embedding_boost(model, gamma=gamma):
129
+ for case in cases:
130
+ try:
131
+ out = str(model.generate(case.inputs))
132
+ except Exception as exc:
133
+ logger.debug("boosted generate failed on %s: %s", case.id, exc)
134
+ scores[case.id] = None
135
+ continue
136
+ scores[case.id] = score_to_bool(score_fn(case, out))
137
+ except RuntimeError as exc:
138
+ logger.warning("visual_embedding_boost unavailable: %s", exc)
139
+ return {c.id: None for c in cases}
140
+ return scores
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # Primitive registry (judge selects + parameterises; code is pre-audited)
145
+ # ---------------------------------------------------------------------------
146
+
147
+ @dataclass(frozen=True)
148
+ class InternalsPrimitive:
149
+ """A host-side, pre-audited L3 intervention primitive."""
150
+
151
+ name: str
152
+ tier: FixTier
153
+ description: str
154
+ params_hint: str
155
+ available: "Callable[[Model], bool]"
156
+ run: "Callable[[Model, CaseBatch, ScoreFn, dict | None], dict[str, Optional[bool]]]"
157
+
158
+
159
+ #: Pre-audited internals-WRITE primitives only. Reads (L3a) are not here — the
160
+ #: agent authors them against the ``model_attend()`` bridge (see module docstring).
161
+ INTERNALS_PRIMITIVES: "dict[str, InternalsPrimitive]" = {
162
+ "visual_embedding_boost": InternalsPrimitive(
163
+ name="visual_embedding_boost",
164
+ tier=FixTier.L3B_INTERNALS_WRITE,
165
+ description="scale image-token embeddings by gamma via a forward hook "
166
+ "(amplifies visual evidence against language priors)",
167
+ params_hint='{"gamma": float > 1 (default 1.5)}',
168
+ available=boost_available,
169
+ run=run_visual_embedding_boost,
170
+ ),
171
+ }
172
+
173
+
174
+ def primitives_catalog_text(model: "Model", max_tier: FixTier) -> str:
175
+ """Render available primitives (≤ *max_tier*, supported by *model*)."""
176
+ lines = []
177
+ for prim in INTERNALS_PRIMITIVES.values():
178
+ if prim.tier <= max_tier and prim.available(model):
179
+ lines.append(f"- {prim.name} [{prim.tier.label}]: {prim.description} "
180
+ f"[params: {prim.params_hint}]")
181
+ return "\n".join(lines)
182
+
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # L4 — parameter space: recipe dataclass + v1 LoRA executor
186
+ # ---------------------------------------------------------------------------
187
+
188
+ @dataclass
189
+ class FinetuneSpec:
190
+ """A complete L4 fine-tune recipe.
191
+
192
+ Executed by :func:`run_lora_repair` when ``method == "lora"`` and
193
+ ``target == "llm"`` and the caller supplied a ``finetune_pool``; recorded
194
+ without running otherwise (TODO: SFT training loop for ``method="sft"``/
195
+ ``"full"``; per-architecture module-naming for ``target="vision_encoder"``/
196
+ ``"projector"``). :class:`~.fix_agent.FixAgent` always records the recipe
197
+ as a candidate either way, so the escalation decision has something
198
+ concrete to act on even when it wasn't executed.
199
+
200
+ Attributes:
201
+ dataset_recipe: How to build training data that *generalises* the
202
+ verified mechanism (never just the failing cases).
203
+ method: Training method, e.g. ``"lora"`` / ``"sft"``.
204
+ target: Component to tune, e.g. ``"vision_encoder"`` /
205
+ ``"llm"`` / ``"projector"`` / ``"full"``.
206
+ eval_protocol: How the tuned model must be validated — held-out
207
+ repair effect AND no-regression on passing cases.
208
+ rationale: Why parameter-space change is the minimum effective
209
+ intervention for the hypothesis.
210
+ """
211
+
212
+ dataset_recipe: str
213
+ method: str = "lora"
214
+ target: str = "llm"
215
+ eval_protocol: str = (
216
+ "paired McNemar on held-out failures + regression battery on all "
217
+ "baseline-passing cases"
218
+ )
219
+ rationale: str = ""
220
+ metadata: "dict[str, Any]" = field(default_factory=dict)
221
+
222
+ def to_dict(self) -> "dict[str, Any]":
223
+ return {
224
+ "dataset_recipe": self.dataset_recipe,
225
+ "method": self.method,
226
+ "target": self.target,
227
+ "eval_protocol": self.eval_protocol,
228
+ "rationale": self.rationale,
229
+ "metadata": self.metadata,
230
+ }
231
+
232
+
233
+ @dataclass
234
+ class LoraRepairResult:
235
+ """Outcome of one L4 LoRA train-and-validate cycle.
236
+
237
+ Mirrors :class:`~.fix_pipeline.CodedPipelineResult`'s shape (``scores``/
238
+ ``ok``/``error``) so the caller's ``exec_error`` accounting is identical
239
+ across coded and fine-tune candidates.
240
+ """
241
+
242
+ scores: "dict[str, Optional[bool]]" = field(default_factory=dict)
243
+ ok: bool = False
244
+ error: str = ""
245
+
246
+
247
+ def lora_available() -> bool:
248
+ """Whether the optional ``peft`` dependency (``pip install evalrx[finetune]``)
249
+ is installed. ``peft`` hard-depends on ``transformers``, so this single
250
+ check also covers that."""
251
+ try:
252
+ import peft # noqa: F401
253
+ except ImportError:
254
+ return False
255
+ return True
256
+
257
+
258
+ def _lora_target_modules(hf_model: Any) -> "str | list[str]":
259
+ """Scope LoRA to the language-model component (v1 only ever executes
260
+ ``target="llm"``).
261
+
262
+ When the model exposes a ``language_model`` submodule (the same
263
+ convention :func:`~.hf_local.HFLocalModel.generate_ifcd` and
264
+ ``generate_pai`` already use for "the LLM half of a VLM"), a regex
265
+ anchored to that dotted prefix is used — plain name-suffix matching
266
+ (``q_proj``/``k_proj``/...) is not safe here, because CLIP-style vision
267
+ towers commonly use the *same* projection names, and a suffix-only match
268
+ would silently pull the vision encoder into a candidate whose contract
269
+ promises ``target="llm"`` only. Falls back to plain suffix matching for a
270
+ text-only backbone, where there is no vision tower to leak into.
271
+ """
272
+ suffixes = ("q_proj", "k_proj", "v_proj", "o_proj")
273
+ if getattr(hf_model, "language_model", None) is not None:
274
+ return r"language_model\..*\.(" + "|".join(suffixes) + ")"
275
+ return list(suffixes)
276
+
277
+
278
+ def _build_sft_examples(
279
+ train_cases: "CaseBatch", max_examples: int
280
+ ) -> "list[tuple[FailureCase, str]]":
281
+ """Diagnosis-split-only SFT pairs — never interprets ``dataset_recipe``
282
+ (see module docstring). Failing cases (gold answer as the SFT target)
283
+ teach the correction; a matched sample of already-passing cases is
284
+ anti-forgetting ballast. The caller (:class:`~.fix_agent.FixAgent`) is
285
+ responsible for ``train_cases`` being disjoint from whatever batch is
286
+ being validated — this function has no way to detect contamination.
287
+ """
288
+ from evalrx.core.case import Label
289
+
290
+ def _usable(case: "FailureCase") -> bool:
291
+ return case.expected not in (None, "") and bool(str(case.inputs.prompt or "").strip())
292
+
293
+ fail = [(c, str(c.expected)) for c in train_cases if c.label == Label.FAIL and _usable(c)]
294
+ passed = [(c, str(c.expected)) for c in train_cases if c.label == Label.PASS and _usable(c)]
295
+ if not fail:
296
+ return []
297
+ budget = max(1, int(max_examples))
298
+ n_fail = min(len(fail), budget)
299
+ n_pass = min(len(passed), max(0, budget - n_fail))
300
+ return fail[:n_fail] + passed[:n_pass]
301
+
302
+
303
+ def _encode_sft_example(
304
+ processor: Any, prompt: str, image: Any, target_text: str, chat_template_kwargs: "dict[str, Any]"
305
+ ) -> "dict[str, Any]":
306
+ """One teacher-forced training example: everything up to (and including)
307
+ the prompt is masked to ``-100``; loss applies only to the target-answer
308
+ tokens.
309
+
310
+ Uses the processor's chat template when available (production VLM/LLM
311
+ processors); falls back to plain ``prompt + " " + target`` concatenation
312
+ otherwise (simple tokenizers, InstructBLIP-style processors with no chat
313
+ template — its ``Question:``/``Answer:`` framing is not reproduced here,
314
+ a v1 simplification). The prompt/target boundary is found by tokenizing
315
+ the prompt-only prefix separately and using its length as the mask
316
+ cutoff: an approximation (BPE merges spanning the boundary can shift a
317
+ token or two) accepted as good enough for v1.
318
+ """
319
+ tok = getattr(processor, "tokenizer", processor)
320
+ chat_template = getattr(tok, "chat_template", None)
321
+ images = None
322
+ if image is not None:
323
+ images = list(image) if isinstance(image, (list, tuple)) else [image]
324
+
325
+ if chat_template:
326
+ content: Any = (
327
+ [{"type": "image"} for _ in images] + [{"type": "text", "text": prompt}]
328
+ if images else prompt
329
+ )
330
+ prefix_text = processor.apply_chat_template(
331
+ [{"role": "user", "content": content}],
332
+ add_generation_prompt=True, tokenize=False, **chat_template_kwargs,
333
+ )
334
+ full_text = processor.apply_chat_template(
335
+ [
336
+ {"role": "user", "content": content},
337
+ {"role": "assistant", "content": target_text},
338
+ ],
339
+ add_generation_prompt=False, tokenize=False, **chat_template_kwargs,
340
+ )
341
+ if images:
342
+ prefix_enc = processor(text=[prefix_text], images=images, return_tensors="pt")
343
+ full_enc = processor(text=[full_text], images=images, return_tensors="pt")
344
+ else:
345
+ prefix_enc = tok(prefix_text, return_tensors="pt")
346
+ full_enc = tok(full_text, return_tensors="pt")
347
+ else:
348
+ prefix_enc = tok(prompt, return_tensors="pt")
349
+ full_enc = tok(prompt + " " + target_text, return_tensors="pt")
350
+
351
+ prefix_len = prefix_enc["input_ids"].shape[1]
352
+ labels = full_enc["input_ids"].clone()
353
+ labels[:, :prefix_len] = -100
354
+ enc = dict(full_enc)
355
+ enc["labels"] = labels
356
+ enc.pop("token_type_ids", None) # some VLM processors emit this; generate()/forward() reject it
357
+ return enc
358
+
359
+
360
+ def run_lora_repair(
361
+ model: "Model",
362
+ train_cases: "CaseBatch | None",
363
+ val_cases: "CaseBatch",
364
+ spec_payload: "dict[str, Any]",
365
+ score_fn: ScoreFn,
366
+ *,
367
+ max_train_examples: int = 16,
368
+ max_steps: int = 30,
369
+ lr: float = 1e-3,
370
+ lora_r: int = 8,
371
+ lora_alpha: int = 16,
372
+ ) -> LoraRepairResult:
373
+ """Train a LoRA adapter on ``train_cases`` (diagnosis-split only — MUST be
374
+ disjoint from ``val_cases``, the batch being validated) and generate on
375
+ ``val_cases`` with it applied, so the caller's existing McNemar + e-value
376
+ machinery validates L4 exactly like every other tier — the
377
+ ``eval_protocol`` field's "held-out repair effect" half is satisfied by
378
+ that comparison; the "regression battery on baseline-passing cases" half
379
+ is ``n_broken`` on whatever baseline-passing cases the caller's ``data``
380
+ batch includes, which :meth:`~.fix_agent.FixAgent._validate` already
381
+ computes. No second validation path is built here.
382
+
383
+ Executes exactly one recipe shape (``method="lora"``, ``target="llm"``);
384
+ everything else returns ``ok=False`` with a descriptive ``error`` (see
385
+ module docstring). LoRA is spliced into ``model``'s underlying HF module
386
+ *in place* (via ``peft.get_peft_model``) and unloaded again in a
387
+ ``finally`` — the model is back to its exact pre-call state (verified by
388
+ this module's tests) whether or not this call ever reaches generation.
389
+ """
390
+ method = str(spec_payload.get("method", "lora")).strip().lower()
391
+ target = str(spec_payload.get("target", "llm")).strip().lower()
392
+ if method != "lora":
393
+ return LoraRepairResult(
394
+ error=f"only method='lora' is executed; method={method!r} is TODO "
395
+ "(needs a real SFT training loop, not a handful of LoRA steps)"
396
+ )
397
+ if target != "llm":
398
+ return LoraRepairResult(
399
+ error=f"only target='llm' is executed; target={target!r} is TODO "
400
+ "(needs per-architecture module-naming this project has no way to "
401
+ "validate without a training run per architecture)"
402
+ )
403
+ if not lora_available():
404
+ return LoraRepairResult(error="peft is not installed (pip install evalrx[finetune])")
405
+ if train_cases is None or len(train_cases) == 0:
406
+ return LoraRepairResult(
407
+ error="no finetune_pool configured — FixAgent(finetune_pool=...) needs a "
408
+ "diagnosis-only CaseBatch, never the validation split"
409
+ )
410
+ # NOT _resolve_hf(): that helper also requires an image_token_id (it backs
411
+ # the vision-only visual_embedding_boost primitive) and would reject any
412
+ # text-only backbone -- exactly the fallback target_modules case below.
413
+ hf = getattr(model, "_hf", None)
414
+ hf_model = hf[0] if isinstance(hf, tuple) and len(hf) == 2 else None
415
+ processor = hf[1] if isinstance(hf, tuple) and len(hf) == 2 else None
416
+ if hf_model is None or processor is None:
417
+ return LoraRepairResult(
418
+ error="backend internals unavailable (needs a loaded hf_local model + processor)"
419
+ )
420
+
421
+ examples = _build_sft_examples(train_cases, max_train_examples)
422
+ if not examples:
423
+ return LoraRepairResult(
424
+ error="finetune_pool has no failing case with a known gold answer to train on"
425
+ )
426
+
427
+ import torch
428
+ from peft import LoraConfig, TaskType, get_peft_model
429
+
430
+ chat_template_kwargs = dict(getattr(getattr(model, "spec", None), "chat_template_kwargs", {}) or {})
431
+ target_modules = _lora_target_modules(hf_model)
432
+ lora_cfg = LoraConfig(
433
+ r=lora_r, lora_alpha=lora_alpha, lora_dropout=0.0,
434
+ target_modules=target_modules, task_type=TaskType.CAUSAL_LM,
435
+ )
436
+ was_training = hf_model.training
437
+ device = next(hf_model.parameters()).device
438
+ # get_peft_model() itself raises (before injecting anything, so hf_model
439
+ # is untouched) when target_modules matches zero layers -- e.g. a VLM
440
+ # whose language_model uses non-LLaMA-style projection names. That must
441
+ # not propagate as an uncaught exception out of one candidate's
442
+ # validation and abort the whole FixAgent run.
443
+ try:
444
+ peft_model = get_peft_model(hf_model, lora_cfg)
445
+ except Exception as exc:
446
+ return LoraRepairResult(
447
+ error=f"no matching linear layers for target_modules={target_modules!r} "
448
+ f"on this architecture: {exc}"
449
+ )
450
+ try:
451
+ trainable = [p for p in peft_model.parameters() if p.requires_grad]
452
+ if not trainable: # defensive belt-and-braces; get_peft_model already guards this
453
+ return LoraRepairResult(
454
+ error=f"no matching linear layers for target_modules={target_modules!r} "
455
+ "on this architecture"
456
+ )
457
+ peft_model.train()
458
+ opt = torch.optim.AdamW(trainable, lr=lr)
459
+ n_steps_run = 0
460
+ for step in range(max_steps):
461
+ case, target_text = examples[step % len(examples)]
462
+ try:
463
+ enc = _encode_sft_example(
464
+ processor, str(case.inputs.prompt or ""), getattr(case.inputs, "image", None),
465
+ target_text, chat_template_kwargs,
466
+ )
467
+ except Exception as exc:
468
+ logger.debug("run_lora_repair: encode failed on %s: %s", case.id, exc)
469
+ continue
470
+ enc = {k: (v.to(device) if hasattr(v, "to") else v) for k, v in enc.items()}
471
+ opt.zero_grad()
472
+ try:
473
+ out = peft_model(**enc)
474
+ if out.loss is None:
475
+ continue
476
+ out.loss.backward()
477
+ opt.step()
478
+ n_steps_run += 1
479
+ except Exception as exc:
480
+ logger.debug("run_lora_repair: training step failed on %s: %s", case.id, exc)
481
+ continue
482
+ if n_steps_run == 0:
483
+ return LoraRepairResult(error="every training step failed to encode/run (see debug log)")
484
+
485
+ peft_model.eval()
486
+ scores: "dict[str, Optional[bool]]" = {}
487
+ with torch.no_grad():
488
+ for case in val_cases:
489
+ try:
490
+ out = str(model.generate(case.inputs))
491
+ except Exception as exc:
492
+ logger.debug("run_lora_repair: generate failed on %s: %s", case.id, exc)
493
+ scores[case.id] = None
494
+ continue
495
+ scores[case.id] = score_to_bool(score_fn(case, out))
496
+ return LoraRepairResult(scores=scores, ok=True)
497
+ finally:
498
+ peft_model.unload()
499
+ hf_model.train(was_training)