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,279 @@
1
+ """Gemini API model — black-box GENERATE + native tool-calling via Google's Gen AI SDK.
2
+
3
+ Install the optional dependency::
4
+
5
+ pip install evalrx[gemini]
6
+
7
+ Two roles:
8
+
9
+ * judge — ``GeminiModel(api_key=...)`` as before (``generate()``), and
10
+ * **agent base** — ``chat(messages, tools=...)`` speaks the agent loop's
11
+ protocol with Gemini's NATIVE function calling (not an OpenAI-compat
12
+ bridge, whose tool-call shims are historically quirky). Images in
13
+ content-block messages are sent as inline PNG parts, so multimodal
14
+ tool loops (zoom crops fed back) work like on the other backends.
15
+
16
+ The loop threads history in OpenAI-ish form (assistant ``tool_calls`` /
17
+ ``role="tool"`` results, via :class:`~evalrx.models.toolcodec.OpenAIToolCodec`
18
+ — ``tool_call_style = "native"`` routes it); this module converts that history
19
+ to ``google-genai`` contents per call.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import os
26
+ from typing import Any, Optional
27
+
28
+ from evalrx.core.capability import Capability
29
+ from evalrx.core.case import Inputs
30
+ from evalrx.core.tool import ChatTurn
31
+ from evalrx.models.blackbox.base import BlackboxModel, Trace
32
+
33
+
34
+ def _png_bytes(image: Any) -> bytes:
35
+ """Encode a PIL image (or open a path) as PNG bytes."""
36
+ import io
37
+
38
+ if not (hasattr(image, "save") and hasattr(image, "mode")):
39
+ from PIL import Image
40
+
41
+ image = Image.open(image).convert("RGB")
42
+ buf = io.BytesIO()
43
+ image.save(buf, format="PNG")
44
+ return buf.getvalue()
45
+
46
+
47
+ _AUDIO_MIME = {"wav": "audio/wav", "mp3": "audio/mp3", "flac": "audio/flac",
48
+ "ogg": "audio/ogg", "m4a": "audio/mp4"}
49
+
50
+
51
+ def _audio_bytes(audio: Any) -> "tuple[bytes, str]":
52
+ """``(bytes, mime_type)`` for an audio path / raw bytes / waveform.
53
+
54
+ Reuses the endpoint path's encoder (a path keeps its own format; bytes are
55
+ taken as WAV; a waveform is written as 16-bit PCM WAV), so the two API
56
+ backends send the same bytes for the same case.
57
+ """
58
+ import base64
59
+
60
+ from evalrx.models.backends.openai_compat import _to_input_audio
61
+
62
+ encoded = _to_input_audio(audio)
63
+ return base64.b64decode(encoded["data"]), _AUDIO_MIME.get(encoded["format"], "audio/wav")
64
+
65
+
66
+ def _to_genai_contents(messages: list, types: Any) -> "tuple[Optional[str], list]":
67
+ """Convert loop messages (OpenAI-ish history + content blocks) into
68
+ ``(system_instruction, genai contents)``.
69
+
70
+ ``role="tool"`` results become ``function_response`` parts; the function
71
+ name comes from the preceding assistant turn's tool call (the loop is
72
+ single-call-per-turn, so pairing by order is exact).
73
+ """
74
+ system: Optional[str] = None
75
+ contents: list = []
76
+ last_call_name = ""
77
+ for msg in messages:
78
+ role = msg.get("role")
79
+ content = msg.get("content")
80
+ if role == "system":
81
+ system = content if isinstance(content, str) else str(content)
82
+ continue
83
+ if role == "tool":
84
+ contents.append(
85
+ types.Content(
86
+ role="user",
87
+ parts=[
88
+ types.Part(
89
+ function_response=types.FunctionResponse(
90
+ name=last_call_name or "tool",
91
+ response={"result": content},
92
+ )
93
+ )
94
+ ],
95
+ )
96
+ )
97
+ continue
98
+ if role == "assistant":
99
+ parts = []
100
+ if isinstance(content, str) and content:
101
+ parts.append(types.Part(text=content))
102
+ for tc in msg.get("tool_calls") or []:
103
+ fn = tc.get("function", tc)
104
+ args = fn.get("arguments", {})
105
+ if isinstance(args, str):
106
+ try:
107
+ args = json.loads(args) if args.strip() else {}
108
+ except json.JSONDecodeError:
109
+ args = {"_raw": args}
110
+ last_call_name = fn.get("name", "")
111
+ parts.append(
112
+ types.Part(
113
+ function_call=types.FunctionCall(name=last_call_name, args=args)
114
+ )
115
+ )
116
+ if parts:
117
+ contents.append(types.Content(role="model", parts=parts))
118
+ continue
119
+ # user message: plain text or content blocks (text / image)
120
+ parts = []
121
+ if isinstance(content, list):
122
+ for block in content:
123
+ if isinstance(block, dict) and block.get("type") == "image":
124
+ parts.append(
125
+ types.Part.from_bytes(
126
+ data=_png_bytes(block.get("image")), mime_type="image/png"
127
+ )
128
+ )
129
+ elif isinstance(block, dict) and block.get("type") == "audio":
130
+ data, mime = _audio_bytes(block.get("audio"))
131
+ parts.append(types.Part.from_bytes(data=data, mime_type=mime))
132
+ elif isinstance(block, dict) and block.get("type") == "text":
133
+ parts.append(types.Part(text=block.get("text", "")))
134
+ else:
135
+ parts.append(types.Part(text=content if isinstance(content, str) else str(content)))
136
+ contents.append(types.Content(role="user", parts=parts))
137
+ return system, contents
138
+
139
+
140
+ def _to_genai_tools(tools: Optional[list], types: Any) -> Optional[list]:
141
+ """OpenAI tool schemas -> genai Tool with FunctionDeclarations.
142
+
143
+ Prefers ``parameters_json_schema`` (raw JSON schema passthrough, newer
144
+ SDKs); falls back to ``parameters`` coercion on older ones.
145
+ """
146
+ if not tools:
147
+ return None
148
+ decls = []
149
+ for t in tools:
150
+ fn = t.get("function", t)
151
+ try:
152
+ decls.append(
153
+ types.FunctionDeclaration(
154
+ name=fn.get("name", ""),
155
+ description=fn.get("description", ""),
156
+ parameters_json_schema=fn.get("parameters"),
157
+ )
158
+ )
159
+ except Exception: # older SDK without parameters_json_schema
160
+ decls.append(
161
+ types.FunctionDeclaration(
162
+ name=fn.get("name", ""),
163
+ description=fn.get("description", ""),
164
+ parameters=fn.get("parameters"),
165
+ )
166
+ )
167
+ return [types.Tool(function_declarations=decls)]
168
+
169
+
170
+ class GeminiModel(BlackboxModel):
171
+ """Black-box wrapper around the Google Gemini API.
172
+
173
+ Args:
174
+ model_id: Gemini model name (default: ``"gemini-2.5-flash"``).
175
+ api_key: API key. Falls back to the ``GEMINI_API_KEY`` environment
176
+ variable when ``None``.
177
+ client: An injected ``genai.Client``-shaped object (tests, custom
178
+ transports); lazily created from *api_key* when ``None``.
179
+
180
+ Requires ``pip install google-genai`` (``evalrx[gemini]``).
181
+ """
182
+
183
+ capabilities = frozenset({Capability.GENERATE, Capability.TOOL_CALLS})
184
+ modalities = frozenset({"text", "image"})
185
+ tool_call_style = "native" # codec_for -> OpenAIToolCodec (structured calls)
186
+
187
+ def __init__(
188
+ self,
189
+ model_id: str = "gemini-2.5-flash",
190
+ api_key: str | None = None,
191
+ client: Any = None,
192
+ ) -> None:
193
+ super().__init__(
194
+ model_id=model_id,
195
+ api_key=api_key or os.getenv("GEMINI_API_KEY"),
196
+ )
197
+ self._client = client
198
+
199
+ # -- plumbing ------------------------------------------------------
200
+ def _genai(self):
201
+ try:
202
+ from google import genai
203
+ from google.genai import types
204
+ except ImportError as exc:
205
+ raise ImportError(
206
+ "GeminiModel requires the google-genai package. "
207
+ "Install it with: pip install 'evalrx[gemini]'"
208
+ ) from exc
209
+ if self._client is None:
210
+ if not self.api_key:
211
+ raise ValueError(
212
+ "No Gemini API key found. Pass api_key= or set GEMINI_API_KEY."
213
+ )
214
+ self._client = genai.Client(api_key=self.api_key)
215
+ return self._client, types
216
+
217
+ # -- interface -----------------------------------------------------
218
+ def generate(self, inputs: Any, **kwargs) -> str:
219
+ client, types = self._genai()
220
+ image = getattr(inputs, "image", None) if isinstance(inputs, Inputs) else None
221
+ prompt = inputs.prompt if isinstance(inputs, Inputs) else str(inputs)
222
+ contents: list = []
223
+ if image is not None:
224
+ contents.append(types.Part.from_bytes(data=_png_bytes(image), mime_type="image/png"))
225
+ contents.append(prompt)
226
+ response = client.models.generate_content(
227
+ model=self.model_id, contents=contents, **kwargs
228
+ )
229
+ return response.text or ""
230
+
231
+ def chat(self, messages: list, tools=None) -> ChatTurn:
232
+ """One tool-aware turn via native Gemini function calling."""
233
+ client, types = self._genai()
234
+ system, contents = _to_genai_contents(messages, types)
235
+ config = types.GenerateContentConfig(
236
+ system_instruction=system,
237
+ tools=_to_genai_tools(tools, types),
238
+ temperature=0.0, # diagnosed runs should be as deterministic as the API allows
239
+ )
240
+ response = client.models.generate_content(
241
+ model=self.model_id, contents=contents, config=config
242
+ )
243
+
244
+ texts: list[str] = []
245
+ raw_calls: list[dict] = []
246
+ candidates = getattr(response, "candidates", None) or []
247
+ parts = getattr(candidates[0].content, "parts", None) or [] if candidates else []
248
+ for part in parts:
249
+ if getattr(part, "text", None):
250
+ texts.append(part.text)
251
+ fc = getattr(part, "function_call", None)
252
+ if fc is not None:
253
+ raw_calls.append(
254
+ {
255
+ "id": getattr(fc, "id", None),
256
+ "function": {
257
+ "name": fc.name,
258
+ "arguments": json.dumps(dict(fc.args or {})),
259
+ },
260
+ }
261
+ )
262
+ um = getattr(response, "usage_metadata", None)
263
+ usage = (
264
+ {
265
+ "prompt_tokens": int(getattr(um, "prompt_token_count", 0) or 0),
266
+ "completion_tokens": int(getattr(um, "candidates_token_count", 0) or 0),
267
+ }
268
+ if um is not None
269
+ else None
270
+ )
271
+ return ChatTurn(text="".join(texts), raw_tool_calls=raw_calls or None, usage=usage)
272
+
273
+ def forward(self, inputs: Any, capture: set[Capability], spec: Any = None) -> Trace: # type: ignore[override]
274
+ raise NotImplementedError(
275
+ "GeminiModel is a black-box model; only generate()/chat() are available."
276
+ )
277
+
278
+ def __repr__(self) -> str:
279
+ return f"GeminiModel(model_id={self.model_id!r})"
@@ -0,0 +1,17 @@
1
+ """Black-box LLM via API (OpenAI, Anthropic, Google, etc.).
2
+
3
+ Planned for Stage 2.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from evalrx.models.blackbox.base import BlackboxModel
11
+
12
+
13
+ class BlackboxLLM(BlackboxModel):
14
+ """API-based LLM — no local weights, output-level capabilities only."""
15
+
16
+ def generate(self, inputs: Any, **kwargs) -> str:
17
+ raise NotImplementedError("BlackboxLLM is planned for Stage 2.")
@@ -0,0 +1,17 @@
1
+ """Black-box VLM via API (GPT-4V, Gemini Vision, etc.).
2
+
3
+ Planned for Stage 2.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from evalrx.models.blackbox.base import BlackboxModel
11
+
12
+
13
+ class BlackboxVLM(BlackboxModel):
14
+ """API-based vision-language model."""
15
+
16
+ def generate(self, inputs: Any, **kwargs) -> str:
17
+ raise NotImplementedError("BlackboxVLM is planned for Stage 2.")
@@ -0,0 +1,66 @@
1
+ """compose() — combine a ModelSpec with a Backend into a Model.
2
+
3
+ This is where the orthogonal decomposition becomes a single object:
4
+
5
+ handle = compose(spec, "hf_local", want={Capability.ATTENTION})
6
+
7
+ Capability negotiation happens HERE (before any weights load): if you ask for
8
+ ``ATTENTION`` against the ``api`` backend, you get a clear :class:`CapabilityError`
9
+ up front rather than a crash deep inside a hook. ``api_only`` specs (closed
10
+ weights) refuse every backend except ``api``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Iterable
16
+
17
+ from evalrx.core.capability import Capability, CapabilityError
18
+ from evalrx.models.backends import BACKENDS, Backend, RuntimeConfig
19
+
20
+
21
+ def negotiate(handle, want: Iterable[Capability], model_id: str, where: str):
22
+ """Raise ``CapabilityError`` if *handle* cannot provide every wanted capability.
23
+
24
+ Shared by :func:`compose` (spec/registry path) and :func:`evalrx.wrap`
25
+ (live-model path) so both negotiate capabilities identically.
26
+ """
27
+ missing = set(want) - set(handle.capabilities)
28
+ if missing:
29
+ raise CapabilityError(analyzer=where, model=model_id, missing=missing)
30
+ return handle
31
+
32
+
33
+ def compose(
34
+ spec,
35
+ backend: "str | Backend" = "hf_local",
36
+ runtime: RuntimeConfig | None = None,
37
+ want: Iterable[Capability] = (),
38
+ ):
39
+ """Build a Model for *spec* on *backend*, negotiating capabilities first.
40
+
41
+ Args:
42
+ spec: a :class:`~evalrx.core.spec.ModelSpec` (or a registry key str).
43
+ backend: backend name (``"api"`` / ``"hf_local"`` / ``"vllm_offline"``) or instance.
44
+ runtime: :class:`RuntimeConfig`; defaults applied if omitted.
45
+ want: capabilities the caller needs; raises if the backend can't provide them.
46
+ """
47
+ if isinstance(spec, str):
48
+ from evalrx.specs import get_spec
49
+
50
+ spec = get_spec(spec)
51
+ if isinstance(backend, str):
52
+ if backend not in BACKENDS:
53
+ raise KeyError(f"Unknown backend {backend!r}. Known: {sorted(BACKENDS)}")
54
+ backend = BACKENDS[backend]()
55
+ runtime = runtime or RuntimeConfig()
56
+
57
+ if spec.api_only and backend.kind != "api":
58
+ raise ValueError(
59
+ f"{spec.key!r} is api-only (closed weights); cannot use backend {backend.kind!r}."
60
+ )
61
+
62
+ # Build first (lazy — no weights load), then negotiate against the ACTUAL
63
+ # handle capabilities. This is precise for conditional caps like TOOL_CALLS,
64
+ # which depend on the model (chat template), not just the backend.
65
+ handle = backend.build(spec, runtime)
66
+ return negotiate(handle, want, model_id=spec.key, where=f"request@{backend.kind}")
@@ -0,0 +1,88 @@
1
+ """Infer a :class:`~evalrx.core.spec.ModelSpec` from a live, already-loaded model.
2
+
3
+ The curated path (``evalrx.load("qwen...")``) looks a spec up by registry key.
4
+ The public on-ramp (``evalrx.wrap(model, tokenizer)``) has no key — the user
5
+ brings their own loaded HF model — so we *infer* the same ``ModelSpec`` fields from
6
+ the live ``model.config`` and tokenizer instead. Both paths then feed the identical
7
+ :class:`~evalrx.models.backends.hf_local.HFLocalModel`, so every analyzer sees one
8
+ contract.
9
+
10
+ This module is torch-free: it only reads attributes off objects the caller already
11
+ constructed.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any
17
+
18
+ from evalrx.core.spec import AttnSemantics, ModelSpec
19
+
20
+
21
+ def _is_vlm(config: Any) -> bool:
22
+ """Best-effort detection of a vision-language config.
23
+
24
+ A text decoder-only config has no separate vision tower; VLM configs carry a
25
+ nested ``vision_config`` (Qwen-VL, LLaVA, GLM-V, …) or expose a distinct text
26
+ sub-config via ``get_text_config()``.
27
+ """
28
+ if getattr(config, "vision_config", None) is not None:
29
+ return True
30
+ if hasattr(config, "get_text_config"):
31
+ try:
32
+ text_cfg = config.get_text_config()
33
+ except Exception: # pragma: no cover - defensive
34
+ text_cfg = None
35
+ if text_cfg is not None and text_cfg is not config:
36
+ return True
37
+ return False
38
+
39
+
40
+ def _chat_template(tokenizer: Any) -> str:
41
+ """Return the tokenizer's chat template string (``""`` if none)."""
42
+ tok = getattr(tokenizer, "tokenizer", tokenizer) # processor -> inner tokenizer
43
+ return getattr(tok, "chat_template", None) or ""
44
+
45
+
46
+ def infer_spec(model: Any, tokenizer: Any) -> ModelSpec:
47
+ """Build a :class:`ModelSpec` describing an already-loaded HF causal LM.
48
+
49
+ Args:
50
+ model: an instantiated ``transformers`` decoder-only model.
51
+ tokenizer: its tokenizer (or processor).
52
+
53
+ Returns:
54
+ A ``ModelSpec`` carrying only the fields ``HFLocalModel`` reads at analysis
55
+ time (identity, ``tool_calling``, ``attn_semantics``). ``hf_repo`` is left
56
+ empty because the weights are already in memory — ``HFLocalModel`` never
57
+ calls ``load()`` on a wrapped model.
58
+
59
+ Raises:
60
+ NotImplementedError: if *model* looks like a VLM. ``wrap()`` supports text
61
+ decoder-only models in this pass; VLM forward-capture (image tokens +
62
+ TokenTypeMap) is Stage 2 — consistent with ``HFLocalModel.forward``.
63
+ """
64
+ config = getattr(model, "config", None)
65
+ if config is None:
66
+ raise ValueError(
67
+ "wrap() expects a transformers model with a .config; "
68
+ f"got {type(model).__name__} with none."
69
+ )
70
+ if _is_vlm(config):
71
+ raise NotImplementedError(
72
+ "wrap() supports text decoder-only models; VLM internals capture "
73
+ "(image tokens + TokenTypeMap) is Stage 2."
74
+ )
75
+
76
+ model_type = getattr(config, "model_type", None) or "unknown"
77
+ key = getattr(config, "_name_or_path", "") or model_type or "wrapped-model"
78
+ tool_calling = "tools" in _chat_template(tokenizer)
79
+
80
+ return ModelSpec(
81
+ key=key,
82
+ family=model_type,
83
+ model_type=model_type,
84
+ hf_repo="", # already in memory — never reloaded
85
+ tool_calling=tool_calling,
86
+ eager_required_for_attn=True,
87
+ attn_semantics=AttnSemantics.STANDARD,
88
+ )
@@ -0,0 +1,8 @@
1
+ """Architecture-aware executors for published model-health repairs.
2
+
3
+ These modules are deliberately small and explicit. A method is exposed here
4
+ only when its model access contract can be checked by the backend; an example
5
+ or benchmark runner must not silently relabel a prompt trick as the paper
6
+ method.
7
+ """
8
+
@@ -0,0 +1,53 @@
1
+ """Hugging Face adapter for AAD's released per-token contrastive sampler.
2
+
3
+ AAD (Hsu et al. 2025, arXiv:2506.07233, "Reducing Object Hallucination in
4
+ Large Audio-Language Models via Audio-Aware Decoding") contrasts the
5
+ with-audio next-token distribution against the SAME prompt with the audio
6
+ waveform replaced by silence (zeros), at every decode step:
7
+
8
+ logits' = (1 + alpha) * logit(y_t | audio, text, y<t)
9
+ - alpha * logit(y_t | silence, text, y<t)
10
+
11
+ No adaptive-plausibility cutoff -- unlike VCD, the released formula (and this
12
+ adapter, to stay faithful to it) does not mask sub-threshold candidates.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any
18
+
19
+
20
+ class AADLogitsProcessor:
21
+ """Contrast clean (real-audio) scores with a cached silent-audio pass.
22
+
23
+ Mirrors VCDLogitsProcessor's KV-cache reuse (the released reference
24
+ implementation reruns the full growing sequence every step; caching the
25
+ silent branch's own past_key_values gets the identical formula without
26
+ that O(n^2) cost).
27
+ """
28
+
29
+ def __init__(self, model: Any, silent_inputs: dict[str, Any], *, alpha: float) -> None:
30
+ self.model = model
31
+ self.silent_inputs = silent_inputs
32
+ self.alpha = float(alpha)
33
+ self._silent_output: Any = None
34
+
35
+ def __call__(self, input_ids: Any, scores: Any) -> Any:
36
+ import torch
37
+
38
+ with torch.no_grad():
39
+ if self._silent_output is None:
40
+ self._silent_output = self.model(
41
+ **self.silent_inputs,
42
+ use_cache=True,
43
+ return_dict=True,
44
+ )
45
+ else:
46
+ self._silent_output = self.model(
47
+ input_ids=input_ids[:, -1:],
48
+ past_key_values=self._silent_output.past_key_values,
49
+ use_cache=True,
50
+ return_dict=True,
51
+ )
52
+ silent_scores = self._silent_output.logits[:, -1, :]
53
+ return (1.0 + self.alpha) * scores - self.alpha * silent_scores