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
evalrx/core/spec.py ADDED
@@ -0,0 +1,173 @@
1
+ """ModelSpec — backend-orthogonal, per-model identity knowledge.
2
+
3
+ The key separation this module enforces:
4
+
5
+ * **capabilities come from the BACKEND** (api / vllm_offline / hf_local),
6
+ * **model identity comes from the SPEC** (this file).
7
+
8
+ One ``ModelSpec`` for, say, Qwen3-VL drives an API call, a vLLM batch-scoring
9
+ run, *and* an HF-eager attention capture — only the capability set differs.
10
+ ``compose(spec, backend)`` (see :mod:`evalrx.models.compose`) combines the
11
+ two into a :class:`~evalrx.core.model.Model`.
12
+
13
+ This module is intentionally **dependency-free** (no torch / transformers), so it
14
+ imports on the light, pure-API install. Module *paths* here are HINTS only: the
15
+ white-box backend RESOLVES them at load time against ``named_modules()`` rather
16
+ than trusting them, because they drift across transformers point-releases and
17
+ per-checkpoint wrappers (the doubled-``.model.`` / Llama4-no-``.model`` / v5
18
+ fused-experts traps). Likewise token ids and merge sizes are read from the live
19
+ ``config`` / processor, never hard-coded as values.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass, field
25
+ from enum import Enum
26
+ from typing import Optional
27
+
28
+
29
+ class AttnSemantics(str, Enum):
30
+ """What an attention capture actually MEANS for this architecture.
31
+
32
+ Prevents an attention analyzer from blindly assuming a dense ``[B,H,Q,K]``
33
+ token-attention matrix when the model uses a compressed/fused attention.
34
+ """
35
+
36
+ STANDARD = "standard" # eager yields a real [B, H, Q, K] token matrix
37
+ MLA_LATENT = "mla_latent" # DeepSeek / Kimi: materialised in decompressed latent head space
38
+ NONE = "none" # no usable weights (e.g. fused-only vision tower)
39
+ UNVERIFIED = "unverified" # e.g. Step3 MFA — probe tensor shape before trusting
40
+ #: Hybrid stacks (Qwen3.5, Qwen3-Next): most layers are linear/SSM attention
41
+ #: with NO query-key matrix, and only every k-th layer is full attention.
42
+ #: Each captured tensor is a real [H, Q, K], but the returned list is SHORTER
43
+ #: than the model's depth and its positions are not layer numbers — read
44
+ #: ``config.text_config.layer_types`` to map them back. Anything that
45
+ #: composes across the stack (attention rollout) is measuring a partial path.
46
+ HYBRID_SPARSE = "hybrid_sparse"
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class ModulePaths:
51
+ """Dotted-attribute HINTS for hooking, relative to the loaded top object.
52
+
53
+ ``"{i}"`` marks the decoder-layer index. These are *starting points* for
54
+ runtime resolution, not ground truth — the hf_local backend discovers the
55
+ real decoder-layer ``ModuleList`` and derives the rest relative to it.
56
+ """
57
+
58
+ decoder_layers: Optional[str] = None # hint, e.g. "model.language_model.layers"
59
+ self_attn: str = "self_attn" # submodule within a decoder layer
60
+ mlp: str = "mlp" # "mlp" | "feed_forward"; dense vs MoE differ
61
+ vision_tower: Optional[str] = None # VLM only; hint, e.g. "model.visual"
62
+ vision_blocks: Optional[str] = None # e.g. ".encoder.blocks" — varies a lot per tower
63
+ router: Optional[str] = None # MoE routing submodule (relative to layer)
64
+ experts: Optional[str] = None # MoE experts; ModuleList OR fused param (v5)
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class VisionSpec:
69
+ """VLM-only knowledge for locating image tokens and rebuilding the patch grid.
70
+
71
+ Every field is a HINT or a *config attribute name* — never a baked value —
72
+ because image-token ids differ across checkpoints (GLM-4.5V 151363 vs
73
+ GLM-4.1V 151343) and configs name them differently (``image_token_id`` /
74
+ ``image_token_index`` / ``media_placeholder_token_id``).
75
+ """
76
+
77
+ # name of the config attribute holding the per-patch placeholder token id:
78
+ image_token_id_attr: str = "image_token_id"
79
+ # name of the config path holding the spatial merge size (None -> not applicable):
80
+ merge_size_attr: Optional[str] = "vision_config.spatial_merge_size"
81
+ grid_source: str = "grid_thw" # "grid_thw" | "grid_hw" | "fixed"
82
+ fixed_tokens_per_tile: Optional[int] = None
83
+ prefer_mm_token_type_ids: bool = True # cleanest source of truth when emitted
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class AudioSpec:
88
+ """Omni-only knowledge for locating audio tokens (Stage-2 white-box capture).
89
+
90
+ Symmetric to :class:`VisionSpec`: every field is a HINT or a *config attribute
91
+ name*, never a baked value, because audio placeholder ids and tower paths drift
92
+ across checkpoints (Qwen3-Omni's ``thinker.audio_tower`` vs others). Its mere
93
+ presence on a spec declares the ``"audio"`` modality; the white-box audio-token
94
+ map that mirrors :class:`~evalrx.core.tokentype.TokenTypeMap` is Stage-2.
95
+ """
96
+
97
+ # name of the config attribute holding the per-frame audio placeholder token id:
98
+ audio_token_id_attr: str = "audio_token_id"
99
+ audio_tower: Optional[str] = None # hooking HINT, e.g. "thinker.audio_tower"
100
+ # Qwen3-Omni: whether a video's own audio track is fed alongside its frames
101
+ # (kept consistent between preprocessing and generation by the processor).
102
+ use_audio_in_video: bool = True
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class ModelSpec:
107
+ """Per-model identity, reused across every backend.
108
+
109
+ Holds NO capabilities — those are the backend's. Modality is declared by the
110
+ *components present*, not a class fork: ``vision`` adds ``"image"``, ``audio``
111
+ adds ``"audio"``, ``video`` adds ``"video"`` (an omni model simply carries more
112
+ than one). Pure-LLM specs leave them unset (no TokenTypeMap). Closed models
113
+ set ``api_only``.
114
+ """
115
+
116
+ key: str # registry key, e.g. "qwen3-vl-8b-instruct"
117
+ family: str # "qwen3_vl" | "deepseek_v3" | ...
118
+ model_type: str # config.model_type (used to branch)
119
+ hf_repo: str = "" # "" for closed/api-only models
120
+ auto_class: str = "AutoModelForCausalLM" # or "AutoModelForImageTextToText"
121
+ trust_remote_code: bool = False
122
+ min_transformers: str = "4.51.0"
123
+ processor_class: str = "AutoProcessor" # "AutoTokenizer" for text-only
124
+ chat_template_kwargs: dict = field(default_factory=dict)
125
+ is_moe: bool = False
126
+ is_reasoning: bool = False # emits <think>...</think>
127
+ tool_calling: bool = False # chat template renders tools -> grants TOOL_CALLS on local backends
128
+ eager_required_for_attn: bool = True
129
+ attn_semantics: AttnSemantics = AttnSemantics.STANDARD
130
+ module_paths: Optional[ModulePaths] = None # None -> rely fully on discovery
131
+ vision: Optional[VisionSpec] = None # not None -> "image" modality
132
+ audio: Optional[AudioSpec] = None # not None -> "audio" modality (omni)
133
+ video: bool = False # True -> "video" modality (omni)
134
+ api_only: bool = False # closed weights -> only the api backend
135
+ caveats: tuple[str, ...] = () # honest, human-readable gotchas
136
+
137
+ @property
138
+ def is_vlm(self) -> bool:
139
+ """Carries a vision tower — drives the image-token forward path."""
140
+ return self.vision is not None
141
+
142
+ @property
143
+ def is_omni(self) -> bool:
144
+ """Handles audio and/or video on top of text (and usually image)."""
145
+ return self.audio is not None or self.video
146
+
147
+ @property
148
+ def needs_multimodal_encode(self) -> bool:
149
+ """True when the backend must route through the (image/video/audio) encoder.
150
+
151
+ Distinct from ``is_vlm`` alone so an audio-only spec (``vision is
152
+ None``, e.g. the Qwen3-Omni captioner or a Qwen2-Audio spec) still
153
+ takes the multimodal encode path in the backend instead of falling
154
+ through to the plain-text tokenizer and silently dropping its audio.
155
+ """
156
+ return self.vision is not None or self.audio is not None
157
+
158
+ @property
159
+ def modalities(self) -> frozenset[str]:
160
+ """Modalities derived from the components present (analyzers match on this)."""
161
+ mods = {"text"}
162
+ if self.vision is not None:
163
+ mods.add("image")
164
+ if self.audio is not None:
165
+ mods.add("audio")
166
+ if self.video:
167
+ mods.add("video")
168
+ return frozenset(mods)
169
+
170
+ def __str__(self) -> str: # pragma: no cover - cosmetic
171
+ kind = "Omni" if self.is_omni else ("VLM" if self.is_vlm else "LLM")
172
+ tag = " api-only" if self.api_only else ""
173
+ return f"ModelSpec({self.key!r}, {kind}, family={self.family}{tag})"
@@ -0,0 +1,165 @@
1
+ """TokenTypeMap — the shared VLM primitive that locates image tokens + the patch grid.
2
+
3
+ Every white-box VLM analyzer (Grad-CAM, relative-attention, MM-SHAP,
4
+ image-token logit-lens, "what's in the image") needs to know which sequence
5
+ positions are image-patch tokens and how they map to a spatial grid. Building
6
+ that is genuinely model-specific (dynamic resolution + spatial-merge), so it is
7
+ centralised here and driven by a :class:`~evalrx.core.spec.VisionSpec`.
8
+
9
+ Design notes that this encodes:
10
+ * prefer the processor's ``mm_token_type_ids`` when emitted (cleanest), else
11
+ match ``input_ids == config.<image_token_id_attr>`` — the id is read from the
12
+ live config (GLM-4.5V 151363 vs GLM-4.1V 151343), never baked in;
13
+ * the grid is read from processor metadata per ``grid_source``:
14
+ grid_thw -> image_grid_thw (PRE-merge patch units) -> post = (h//m, w//m) [Qwen/GLM]
15
+ grid_hw -> image_grid_hws (pre-merge) -> //m [Kimi]
16
+ fixed -> a fixed token budget per tile [Gemma/Step]
17
+ * merge size is read from config (``vision_config.spatial_merge_size``), not literal.
18
+
19
+ This module is torch-tolerant: it accepts torch tensors OR plain lists, importing
20
+ torch nowhere — so it unit-tests on the light install with synthetic inputs.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import math
26
+ from dataclasses import dataclass, field
27
+ from typing import Any, Optional
28
+
29
+
30
+ def _flat_ids(x: Any) -> list[int]:
31
+ """Flatten input_ids (torch tensor / np / nested list / 1-D list) to a 1-D int list."""
32
+ if x is None:
33
+ return []
34
+ if hasattr(x, "flatten") and hasattr(x, "tolist"): # torch / numpy tensor
35
+ return [int(v) for v in x.flatten().tolist()]
36
+ if isinstance(x, (list, tuple)):
37
+ if x and isinstance(x[0], (list, tuple)): # [[...]] -> first row
38
+ return [int(v) for v in x[0]]
39
+ return [int(v) for v in x]
40
+ return [int(v) for v in list(x)]
41
+
42
+
43
+ def _rows2(x: Any) -> list[list[int]]:
44
+ """Coerce an (n, k) grid spec (tensor / nested list) to a list of int rows."""
45
+ if x is None:
46
+ return []
47
+ if hasattr(x, "tolist"):
48
+ x = x.tolist()
49
+ rows = []
50
+ for row in x:
51
+ rows.append([int(v) for v in (row if isinstance(row, (list, tuple)) else [row])])
52
+ return rows
53
+
54
+
55
+ def _resolve(obj: Any, dotted: Optional[str]) -> Any:
56
+ """Resolve a dotted attribute path against a config object; None on miss."""
57
+ if obj is None or not dotted:
58
+ return None
59
+ cur = obj
60
+ for part in dotted.split("."):
61
+ cur = getattr(cur, part, None)
62
+ if cur is None:
63
+ return None
64
+ return cur
65
+
66
+
67
+ def _enc_get(enc: Any, key: str) -> Any:
68
+ """Read a key from a processor BatchFeature / dict."""
69
+ if enc is None:
70
+ return None
71
+ if hasattr(enc, "get"):
72
+ try:
73
+ return enc.get(key)
74
+ except Exception:
75
+ pass
76
+ return getattr(enc, key, None)
77
+
78
+
79
+ @dataclass
80
+ class TokenTypeMap:
81
+ """Where the image tokens are, and how they map to a spatial grid.
82
+
83
+ Attributes:
84
+ seq_len: total sequence length.
85
+ image_pos: sequence positions that are image-patch tokens (flat, in order).
86
+ text_pos: the remaining (text/control) positions.
87
+ grids: per image, the POST-merge grid ``(t, h, w)``.
88
+ patches: aligned with ``image_pos`` — ``(image_idx, t, row, col)`` for each.
89
+ image_token_id: the placeholder id used (if matched by id).
90
+ """
91
+
92
+ seq_len: int
93
+ image_pos: list[int] = field(default_factory=list)
94
+ text_pos: list[int] = field(default_factory=list)
95
+ grids: list[tuple[int, int, int]] = field(default_factory=list)
96
+ patches: list[tuple[int, int, int, int]] = field(default_factory=list)
97
+ image_token_id: Optional[int] = None
98
+
99
+ @property
100
+ def n_images(self) -> int:
101
+ return len(self.grids)
102
+
103
+ @property
104
+ def has_image(self) -> bool:
105
+ return len(self.image_pos) > 0
106
+
107
+
108
+ def build_token_type_map(input_ids: Any, enc: Any, config: Any, vision_spec: Any) -> TokenTypeMap:
109
+ """Construct a :class:`TokenTypeMap` from a forward's encoding + model config + VisionSpec."""
110
+ ids = _flat_ids(input_ids)
111
+ seq = len(ids)
112
+
113
+ # 1. locate image positions
114
+ img_id = _resolve(config, vision_spec.image_token_id_attr) if config is not None else None
115
+ mm = _enc_get(enc, "mm_token_type_ids")
116
+ if getattr(vision_spec, "prefer_mm_token_type_ids", True) and mm is not None:
117
+ mm_flat = _flat_ids(mm)
118
+ image_pos = [i for i, v in enumerate(mm_flat) if v == 1]
119
+ elif img_id is not None:
120
+ image_pos = [i for i, t in enumerate(ids) if t == img_id]
121
+ else:
122
+ image_pos = []
123
+ img_set = set(image_pos)
124
+ text_pos = [i for i in range(seq) if i not in img_set]
125
+
126
+ # 2. per-image POST-merge grid
127
+ merge = _resolve(config, getattr(vision_spec, "merge_size_attr", None)) or 1
128
+ merge = int(merge) if merge else 1
129
+ grids: list[tuple[int, int, int]] = []
130
+ source = getattr(vision_spec, "grid_source", "grid_thw")
131
+ if source == "grid_thw":
132
+ for row in _rows2(_enc_get(enc, "image_grid_thw")):
133
+ t, h, w = (row + [1, 1, 1])[:3]
134
+ grids.append((int(t), max(int(h) // merge, 1), max(int(w) // merge, 1)))
135
+ elif source == "grid_hw":
136
+ for row in _rows2(_enc_get(enc, "image_grid_hws")):
137
+ h, w = (row + [1, 1])[:2]
138
+ grids.append((1, max(int(h) // merge, 1), max(int(w) // merge, 1)))
139
+ elif source == "fixed":
140
+ per = getattr(vision_spec, "fixed_tokens_per_tile", None)
141
+ if per:
142
+ n_tiles = max(len(image_pos) // per, 1) if image_pos else 0
143
+ side = int(math.isqrt(per))
144
+ for _ in range(n_tiles):
145
+ grids.append((1, side, side) if side * side == per else (1, 1, per))
146
+
147
+ # 3. map each image position to (image_idx, t, row, col), row-major
148
+ patches: list[tuple[int, int, int, int]] = []
149
+ k = 0
150
+ for img_idx, (t, h, w) in enumerate(grids):
151
+ for ti in range(t):
152
+ for r in range(h):
153
+ for c in range(w):
154
+ if k < len(image_pos):
155
+ patches.append((img_idx, ti, r, c))
156
+ k += 1
157
+
158
+ return TokenTypeMap(
159
+ seq_len=seq,
160
+ image_pos=image_pos,
161
+ text_pos=text_pos,
162
+ grids=grids,
163
+ patches=patches,
164
+ image_token_id=int(img_id) if isinstance(img_id, int) else None,
165
+ )
evalrx/core/tool.py ADDED
@@ -0,0 +1,92 @@
1
+ """Tool / ToolCall / ChatTurn — the value types the agent loop speaks.
2
+
3
+ Backend-agnostic and torch-free. A :class:`Tool` is defined ONCE (name +
4
+ JSON-schema params + a python callable) and works on every backend: its
5
+ ``to_openai_schema()`` is accepted both by OpenAI-compatible APIs and by
6
+ transformers' ``apply_chat_template(tools=...)``. A :class:`ChatTurn` is the
7
+ uniform transport from a model handle back to the codec/agent: ``text`` plus, for
8
+ native-tool-calling backends, the backend's raw structured ``tool_calls``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, Callable, Optional
15
+
16
+
17
+ @dataclass
18
+ class Tool:
19
+ """A callable tool the agent may invoke.
20
+
21
+ ``parameters`` is a JSON-schema object describing the args (the standard
22
+ OpenAI ``function.parameters`` shape).
23
+ """
24
+
25
+ name: str
26
+ description: str = ""
27
+ parameters: dict = field(default_factory=lambda: {"type": "object", "properties": {}})
28
+ fn: Optional[Callable[..., Any]] = None # the actual implementation (executor calls it)
29
+
30
+ def to_openai_schema(self) -> dict:
31
+ """OpenAI tool schema — also accepted by HF ``apply_chat_template(tools=)``."""
32
+ return {
33
+ "type": "function",
34
+ "function": {
35
+ "name": self.name,
36
+ "description": self.description,
37
+ "parameters": self.parameters,
38
+ },
39
+ }
40
+
41
+
42
+ @dataclass
43
+ class ToolCall:
44
+ """A normalised tool invocation decoded from a model's output."""
45
+
46
+ name: str
47
+ args: dict = field(default_factory=dict)
48
+ id: Optional[str] = None
49
+
50
+ def to_dict(self) -> dict:
51
+ return {"name": self.name, "args": self.args, "id": self.id}
52
+
53
+
54
+ @dataclass
55
+ class ToolResult:
56
+ """A structured tool return value: model-visible text + optional artifacts.
57
+
58
+ ``text`` is what goes back to the model as the tool observation; ``images``
59
+ (PIL images or paths) are re-injected into the conversation as a follow-up
60
+ multimodal message so vision models can look at what the tool produced
61
+ (zoom crops, annotated views). ``meta`` is host-side detail (saved paths,
62
+ coordinate mode, timings) recorded on the trajectory step, never shown to
63
+ the model. Tools may still return a plain value; ``str()`` is the
64
+ observation either way.
65
+ """
66
+
67
+ text: str = ""
68
+ images: list = field(default_factory=list)
69
+ meta: dict = field(default_factory=dict)
70
+
71
+ def __str__(self) -> str:
72
+ return self.text
73
+
74
+
75
+ @dataclass
76
+ class ChatTurn:
77
+ """One model turn in an agent loop: assistant text + optional native tool calls.
78
+
79
+ ``raw_tool_calls`` holds the BACKEND-NATIVE structured calls (e.g. OpenAI's
80
+ ``[{"id","function":{"name","arguments"}}]``) when the backend does native
81
+ tool-calling; it is ``None`` for template-based backends, where the call is
82
+ embedded in ``text`` and the codec parses it out.
83
+
84
+ ``usage`` carries per-turn accounting when the backend reports it
85
+ (``{"prompt_tokens": ..., "completion_tokens": ...}``); the agent loop
86
+ copies it into the step's ``span`` so trajectories carry cost evidence.
87
+ """
88
+
89
+ text: str = ""
90
+ raw_tool_calls: Optional[list] = None
91
+ finish_reason: Optional[str] = None
92
+ usage: Optional[dict] = None
@@ -0,0 +1,41 @@
1
+ """Dataset loaders → CaseBatch, plus simple answer verifiers.
2
+
3
+ Text QA: ``LLMQADataset``. Image+text QA: ``VLMQADataset`` and the
4
+ ``TextVQASizeDataset`` / ``Spatial457Dataset`` / ``VQARADDataset`` benchmarks.
5
+ ``PureQADataset`` is a back-compat alias of ``LLMQADataset``.
6
+ """
7
+
8
+ from evalrx.datasets.base import (
9
+ Dataset,
10
+ cases_from_records,
11
+ contains_answer,
12
+ exact_match,
13
+ normalize,
14
+ read_jsonl,
15
+ )
16
+ from evalrx.datasets.gui_os import GUIOSDataset
17
+ from evalrx.datasets.llm_qa import LLMQADataset, PureQADataset
18
+ from evalrx.datasets.vlm_qa import (
19
+ Spatial457Dataset,
20
+ TextVQASizeDataset,
21
+ VLMQADataset,
22
+ VQARADDataset,
23
+ )
24
+ from evalrx.datasets.web_search_qa import WebSearchQADataset
25
+
26
+ __all__ = [
27
+ "Dataset",
28
+ "LLMQADataset",
29
+ "VLMQADataset",
30
+ "TextVQASizeDataset",
31
+ "Spatial457Dataset",
32
+ "VQARADDataset",
33
+ "PureQADataset",
34
+ "WebSearchQADataset",
35
+ "GUIOSDataset",
36
+ "cases_from_records",
37
+ "read_jsonl",
38
+ "exact_match",
39
+ "contains_answer",
40
+ "normalize",
41
+ ]
@@ -0,0 +1,68 @@
1
+ """Dataset base — loaders produce a CaseBatch; plus simple answer verifiers.
2
+
3
+ A dataset is just a ``load() -> CaseBatch`` of :class:`FailureCase` (the unit
4
+ analyzers consume). ``cases_from_records`` maps plain dict records (question /
5
+ answer / image / extra metadata) into cases; the verifiers give A/B strategies a
6
+ ``case -> bool success`` signal without pulling in a heavy metric library.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from abc import ABC, abstractmethod
13
+ from pathlib import Path
14
+ from typing import Iterable
15
+
16
+ from evalrx.core.case import CaseBatch, FailureCase, Inputs, Provenance, Source
17
+
18
+
19
+ # ---------------- verifiers ----------------
20
+ def normalize(s) -> str:
21
+ return " ".join(str(s).strip().lower().split())
22
+
23
+
24
+ def exact_match(prediction, gold) -> bool:
25
+ return normalize(prediction) == normalize(gold)
26
+
27
+
28
+ def contains_answer(prediction, gold) -> bool:
29
+ """True if the (normalised) gold answer appears in the prediction."""
30
+ return bool(str(gold).strip()) and normalize(gold) in normalize(prediction)
31
+
32
+
33
+ # ---------------- record -> cases ----------------
34
+ def cases_from_records(
35
+ records: Iterable[dict],
36
+ *,
37
+ prompt_key: str = "question",
38
+ answer_key: str = "answer",
39
+ image_key: str = "image",
40
+ tags: set[str] | None = None,
41
+ source: Source = Source.DATASET,
42
+ ) -> CaseBatch:
43
+ """Build a :class:`CaseBatch` from dict records; the full record is kept in metadata."""
44
+ out = CaseBatch()
45
+ for rec in records:
46
+ out.append(
47
+ FailureCase(
48
+ inputs=Inputs(prompt=str(rec.get(prompt_key, "")), image=rec.get(image_key)),
49
+ expected=rec.get(answer_key),
50
+ tags=set(tags or ()),
51
+ provenance=Provenance(source=source),
52
+ metadata=dict(rec),
53
+ )
54
+ )
55
+ return out
56
+
57
+
58
+ def read_jsonl(path: str | Path) -> list[dict]:
59
+ with open(path) as f:
60
+ return [json.loads(line) for line in f if line.strip()]
61
+
62
+
63
+ class Dataset(ABC):
64
+ """A benchmark loader: ``load() -> CaseBatch``."""
65
+
66
+ @abstractmethod
67
+ def load(self) -> CaseBatch: # pragma: no cover - interface
68
+ ...
@@ -0,0 +1,52 @@
1
+ """GUI / OS task loader → CaseBatch (agent tasks scored on final env state).
2
+
3
+ Records carry a ``goal`` (the instruction), an optional ``screenshot`` image, and
4
+ an ``expected_state`` (the annotated goal state — τ-bench style, compared to the
5
+ final env state rather than string-matching an answer). Tagged ``gui_os``;
6
+ these cases are typically driven by an Agent and analysed at the trajectory level.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from evalrx.core.case import CaseBatch
12
+ from evalrx.datasets.base import Dataset, cases_from_records, read_jsonl
13
+
14
+ _SAMPLE = [
15
+ {"goal": "Open Settings and enable dark mode.", "expected_state": {"dark_mode": True}, "app": "settings"},
16
+ {"goal": "Create a new file named notes.txt on the desktop.",
17
+ "expected_state": {"file_exists": "~/Desktop/notes.txt"}, "app": "files"},
18
+ ]
19
+
20
+
21
+ class GUIOSDataset(Dataset):
22
+ """GUI / OS-level agent tasks (goal + optional screenshot + expected state)."""
23
+
24
+ def __init__(
25
+ self,
26
+ records: list[dict] | None = None,
27
+ path: str | None = None,
28
+ *,
29
+ prompt_key: str = "goal",
30
+ image_key: str = "screenshot",
31
+ ) -> None:
32
+ self._records = records
33
+ self._path = path
34
+ self._keys = dict(prompt_key=prompt_key, answer_key="expected_state", image_key=image_key)
35
+
36
+ @classmethod
37
+ def from_records(cls, records: list[dict], **keys) -> "GUIOSDataset":
38
+ return cls(records=records, **keys)
39
+
40
+ @classmethod
41
+ def from_jsonl(cls, path: str, **keys) -> "GUIOSDataset":
42
+ return cls(path=path, **keys)
43
+
44
+ @classmethod
45
+ def sample(cls) -> "GUIOSDataset":
46
+ return cls(records=_SAMPLE)
47
+
48
+ def load(self) -> CaseBatch:
49
+ records = self._records if self._records is not None else (
50
+ read_jsonl(self._path) if self._path else _SAMPLE
51
+ )
52
+ return cases_from_records(records, tags={"gui_os"}, **self._keys)
@@ -0,0 +1,57 @@
1
+ """LLM (text) QA loader → CaseBatch.
2
+
3
+ Text-only question answering. Source-agnostic: in-memory records, a JSONL path,
4
+ or the built-in sample. ``expected`` carries the gold answer; pair it with
5
+ :func:`evalrx.datasets.base.contains_answer` to turn a model output into a
6
+ success bool for A/B comparisons. (For image+text QA use :mod:`evalrx.datasets.vlm_qa`.)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from evalrx.core.case import CaseBatch
12
+ from evalrx.datasets.base import Dataset, cases_from_records, read_jsonl
13
+
14
+ _SAMPLE = [
15
+ {"question": "What is the capital of France?", "answer": "Paris", "difficulty": "easy"},
16
+ {"question": "What is 17 multiplied by 3?", "answer": "51", "difficulty": "easy"},
17
+ {"question": "Who wrote Romeo and Juliet?", "answer": "Shakespeare", "difficulty": "easy"},
18
+ {"question": "What is the chemical symbol for gold?", "answer": "Au", "difficulty": "medium"},
19
+ ]
20
+
21
+
22
+ class LLMQADataset(Dataset):
23
+ """Text question-answering loader."""
24
+
25
+ def __init__(
26
+ self,
27
+ records: list[dict] | None = None,
28
+ path: str | None = None,
29
+ *,
30
+ prompt_key: str = "question",
31
+ answer_key: str = "answer",
32
+ ) -> None:
33
+ self._records = records
34
+ self._path = path
35
+ self._keys = dict(prompt_key=prompt_key, answer_key=answer_key)
36
+
37
+ @classmethod
38
+ def from_records(cls, records: list[dict], **keys) -> "LLMQADataset":
39
+ return cls(records=records, **keys)
40
+
41
+ @classmethod
42
+ def from_jsonl(cls, path: str, **keys) -> "LLMQADataset":
43
+ return cls(path=path, **keys)
44
+
45
+ @classmethod
46
+ def sample(cls) -> "LLMQADataset":
47
+ return cls(records=_SAMPLE)
48
+
49
+ def load(self) -> CaseBatch:
50
+ records = self._records if self._records is not None else (
51
+ read_jsonl(self._path) if self._path else _SAMPLE
52
+ )
53
+ return cases_from_records(records, tags={"llm_qa"}, **self._keys)
54
+
55
+
56
+ # Back-compat: the old combined name. Prefer LLMQADataset (text) / VLMQADataset (image+text).
57
+ PureQADataset = LLMQADataset
@@ -0,0 +1,12 @@
1
+ """Back-compat shim — ``pure_qa`` was split into ``llm_qa`` (text) and ``vlm_qa`` (image+text).
2
+
3
+ ``PureQADataset`` remains importable here as an alias of
4
+ :class:`~evalrx.datasets.llm_qa.LLMQADataset`. Prefer the explicit names:
5
+ ``LLMQADataset`` for text QA, ``VLMQADataset`` / ``Spatial457Dataset`` for image+text.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from evalrx.datasets.llm_qa import LLMQADataset, PureQADataset
11
+
12
+ __all__ = ["PureQADataset", "LLMQADataset"]