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.
- evalrx/__init__.py +139 -0
- evalrx/agent_assets/__init__.py +2 -0
- evalrx/agent_assets/skills/README.md +28 -0
- evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
- evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
- evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
- evalrx/agent_assets/skills/nature-figure/README.md +412 -0
- evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
- evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
- evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
- evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
- evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
- evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
- evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
- evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
- evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
- evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
- evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
- evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
- evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
- evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
- evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
- evalrx/agent_assets/skills.py +27 -0
- evalrx/agent_runtime/__init__.py +78 -0
- evalrx/agent_runtime/_docker_runner.py +89 -0
- evalrx/agent_runtime/cli_runtime.py +103 -0
- evalrx/agent_runtime/cli_transcript.py +138 -0
- evalrx/agent_runtime/cli_types.py +68 -0
- evalrx/agent_runtime/codegen/__init__.py +5 -0
- evalrx/agent_runtime/codegen/runner.py +94 -0
- evalrx/agent_runtime/experiment_harness.py +117 -0
- evalrx/agent_runtime/factory.py +102 -0
- evalrx/agent_runtime/json_shape.py +44 -0
- evalrx/agent_runtime/judges/__init__.py +28 -0
- evalrx/agent_runtime/judges/agy.py +179 -0
- evalrx/agent_runtime/judges/autodetect.py +135 -0
- evalrx/agent_runtime/judges/claude.py +159 -0
- evalrx/agent_runtime/judges/codex.py +120 -0
- evalrx/agent_runtime/providers/__init__.py +21 -0
- evalrx/agent_runtime/providers/antigravity.py +31 -0
- evalrx/agent_runtime/providers/base.py +145 -0
- evalrx/agent_runtime/providers/claude_code.py +49 -0
- evalrx/agent_runtime/providers/codex.py +37 -0
- evalrx/agent_runtime/providers/gemini_cli.py +26 -0
- evalrx/agent_runtime/providers/kimi_cli.py +27 -0
- evalrx/agent_runtime/providers/opencode.py +27 -0
- evalrx/agent_runtime/providers/registry.py +58 -0
- evalrx/agent_runtime/sandbox.py +517 -0
- evalrx/agent_runtime/skill_audit.py +143 -0
- evalrx/agent_runtime/skills/__init__.py +19 -0
- evalrx/agent_runtime/skills/installer.py +68 -0
- evalrx/agent_runtime/skills/prompt_policy.py +86 -0
- evalrx/agent_runtime/skills/resolver.py +19 -0
- evalrx/analysis/__init__.py +132 -0
- evalrx/analysis/adjudicate.py +154 -0
- evalrx/analysis/analysis_module.py +361 -0
- evalrx/analysis/api.py +171 -0
- evalrx/analysis/case_studio.py +651 -0
- evalrx/analysis/cli.py +114 -0
- evalrx/analysis/dashboard.py +350 -0
- evalrx/analysis/eval_case_matrix.py +118 -0
- evalrx/analysis/eval_viz_theme.py +833 -0
- evalrx/analysis/explore_run.py +333 -0
- evalrx/analysis/explorer.py +1276 -0
- evalrx/analysis/failure_modes.py +607 -0
- evalrx/analysis/fused_pipeline.py +489 -0
- evalrx/analysis/holdout.py +300 -0
- evalrx/analysis/hypothesis_agent.py +230 -0
- evalrx/analysis/narration.py +177 -0
- evalrx/analysis/operationalize.py +442 -0
- evalrx/analysis/plain_language.py +42 -0
- evalrx/analysis/planner.py +283 -0
- evalrx/analysis/probe_search.py +203 -0
- evalrx/analysis/profile.py +268 -0
- evalrx/analysis/prompts/__init__.py +0 -0
- evalrx/analysis/prompts/explorer.py +417 -0
- evalrx/analysis/prompts/failure_modes.py +33 -0
- evalrx/analysis/prompts/holdout.py +27 -0
- evalrx/analysis/prompts/hypothesis_agent.py +78 -0
- evalrx/analysis/prompts/run_codebase.py +47 -0
- evalrx/analysis/prompts/stats_agent.py +72 -0
- evalrx/analysis/prompts/stats_tool_generator.py +43 -0
- evalrx/analysis/result_marker.py +47 -0
- evalrx/analysis/run_codebase.py +242 -0
- evalrx/analysis/run_view.py +205 -0
- evalrx/analysis/stage_views.py +93 -0
- evalrx/analysis/stats_agent.py +944 -0
- evalrx/analysis/stats_tool_agent.py +261 -0
- evalrx/analysis/stats_tool_generator.py +415 -0
- evalrx/analysis/stats_tools.py +1153 -0
- evalrx/analysis/trajectory_records.py +193 -0
- evalrx/analysis/workbench.py +431 -0
- evalrx/analyzers/__init__.py +42 -0
- evalrx/analyzers/agent/__init__.py +25 -0
- evalrx/analyzers/agent/counterfactual.py +84 -0
- evalrx/analyzers/agent/first_error_judge.py +96 -0
- evalrx/analyzers/agent/ignored_obs.py +81 -0
- evalrx/analyzers/agent/loop_detect.py +79 -0
- evalrx/analyzers/agent/reliability.py +165 -0
- evalrx/analyzers/agent/tool_shap.py +225 -0
- evalrx/analyzers/agent/trajectory_rubric.py +168 -0
- evalrx/analyzers/attention/__init__.py +19 -0
- evalrx/analyzers/attention/relative_attn.py +610 -0
- evalrx/analyzers/attention/rollout.py +73 -0
- evalrx/analyzers/attention/sink.py +56 -0
- evalrx/analyzers/attention/summary.py +190 -0
- evalrx/analyzers/attribution/__init__.py +6 -0
- evalrx/analyzers/attribution/generic_attn.py +31 -0
- evalrx/analyzers/attribution/gradcam.py +30 -0
- evalrx/analyzers/base.py +12 -0
- evalrx/analyzers/geometry/__init__.py +6 -0
- evalrx/analyzers/geometry/cka.py +70 -0
- evalrx/analyzers/geometry/linear_probe.py +157 -0
- evalrx/analyzers/hallucination/__init__.py +9 -0
- evalrx/analyzers/hallucination/chair.py +78 -0
- evalrx/analyzers/hallucination/opera.py +29 -0
- evalrx/analyzers/hallucination/pope.py +119 -0
- evalrx/analyzers/hallucination/selfcheck.py +155 -0
- evalrx/analyzers/hallucination/vcd.py +29 -0
- evalrx/analyzers/lens/__init__.py +7 -0
- evalrx/analyzers/lens/layer_contrast.py +133 -0
- evalrx/analyzers/lens/logit_lens.py +138 -0
- evalrx/analyzers/lens/tuned_lens.py +30 -0
- evalrx/analyzers/patching/__init__.py +5 -0
- evalrx/analyzers/patching/causal_trace.py +30 -0
- evalrx/analyzers/perturbation/__init__.py +23 -0
- evalrx/analyzers/perturbation/_shapley.py +54 -0
- evalrx/analyzers/perturbation/context_shap.py +174 -0
- evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
- evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
- evalrx/analyzers/perturbation/mm_shap.py +146 -0
- evalrx/analyzers/perturbation/modality_ablation.py +196 -0
- evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
- evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
- evalrx/analyzers/perturbation/rise.py +94 -0
- evalrx/analyzers/perturbation/vl_shap.py +102 -0
- evalrx/analyzers/reasoning/__init__.py +33 -0
- evalrx/analyzers/reasoning/_text.py +328 -0
- evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
- evalrx/analyzers/reasoning/arith_audit.py +226 -0
- evalrx/analyzers/reasoning/contamination.py +214 -0
- evalrx/analyzers/reasoning/knowledge_split.py +253 -0
- evalrx/analyzers/reasoning/self_repair.py +246 -0
- evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
- evalrx/analyzers/reasoning/termination_audit.py +258 -0
- evalrx/analyzers/uncertainty/__init__.py +18 -0
- evalrx/analyzers/uncertainty/calibration.py +174 -0
- evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
- evalrx/analyzers/uncertainty/entropy.py +90 -0
- evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
- evalrx/analyzers/uncertainty/self_consistency.py +204 -0
- evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
- evalrx/cli.py +411 -0
- evalrx/config.py +77 -0
- evalrx/contract/__init__.py +179 -0
- evalrx/contract/common.py +452 -0
- evalrx/contract/emit.py +948 -0
- evalrx/contract/export.py +237 -0
- evalrx/contract/m1.py +325 -0
- evalrx/contract/m2.py +317 -0
- evalrx/contract/m3.py +165 -0
- evalrx/contract/m4.py +130 -0
- evalrx/contract/m5.py +292 -0
- evalrx/contract/methodology.py +76 -0
- evalrx/contract/pre_m1.py +58 -0
- evalrx/contract/typescript.py +140 -0
- evalrx/core/__init__.py +85 -0
- evalrx/core/analyzer.py +174 -0
- evalrx/core/capability.py +54 -0
- evalrx/core/case.py +443 -0
- evalrx/core/experiment.py +106 -0
- evalrx/core/model.py +198 -0
- evalrx/core/pipeline.py +42 -0
- evalrx/core/registry.py +142 -0
- evalrx/core/result.py +64 -0
- evalrx/core/spec.py +173 -0
- evalrx/core/tokentype.py +165 -0
- evalrx/core/tool.py +92 -0
- evalrx/datasets/__init__.py +41 -0
- evalrx/datasets/base.py +68 -0
- evalrx/datasets/gui_os.py +52 -0
- evalrx/datasets/llm_qa.py +57 -0
- evalrx/datasets/pure_qa.py +12 -0
- evalrx/datasets/vlm_qa.py +695 -0
- evalrx/datasets/web_search_qa.py +52 -0
- evalrx/eval_agent/__init__.py +341 -0
- evalrx/eval_agent/_tools.py +81 -0
- evalrx/eval_agent/ab_runner.py +50 -0
- evalrx/eval_agent/agentic/__init__.py +43 -0
- evalrx/eval_agent/agentic/actions.py +216 -0
- evalrx/eval_agent/agentic/board.py +107 -0
- evalrx/eval_agent/agentic/loop.py +190 -0
- evalrx/eval_agent/agentic/tools.py +538 -0
- evalrx/eval_agent/checkpoint.py +57 -0
- evalrx/eval_agent/cli_agent.py +59 -0
- evalrx/eval_agent/cli_skills.py +5 -0
- evalrx/eval_agent/evolution.py +396 -0
- evalrx/eval_agent/git_manager.py +215 -0
- evalrx/eval_agent/hypothesis.py +172 -0
- evalrx/eval_agent/label_quarantine.py +209 -0
- evalrx/eval_agent/legacy.py +530 -0
- evalrx/eval_agent/log_schema.py +497 -0
- evalrx/eval_agent/loop.py +2159 -0
- evalrx/eval_agent/loop_reports.py +116 -0
- evalrx/eval_agent/model_instrumentation.py +282 -0
- evalrx/eval_agent/narration.py +193 -0
- evalrx/eval_agent/nl_runner.py +460 -0
- evalrx/eval_agent/orchestrator.py +61 -0
- evalrx/eval_agent/preregister.py +93 -0
- evalrx/eval_agent/prompts/__init__.py +1 -0
- evalrx/eval_agent/prompts/agentic.py +46 -0
- evalrx/eval_agent/prompts/case_discovery.py +25 -0
- evalrx/eval_agent/prompts/diagnosis.py +125 -0
- evalrx/eval_agent/prompts/experiment_writer.py +265 -0
- evalrx/eval_agent/prompts/explore_step.py +37 -0
- evalrx/eval_agent/prompts/fix_agent.py +257 -0
- evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
- evalrx/eval_agent/prompts/nl_runner.py +38 -0
- evalrx/eval_agent/prompts/probe_agent.py +25 -0
- evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
- evalrx/eval_agent/prompts/probe_generator.py +35 -0
- evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
- evalrx/eval_agent/report.py +58 -0
- evalrx/eval_agent/run_context.py +354 -0
- evalrx/eval_agent/run_log.schema.json +1215 -0
- evalrx/eval_agent/run_logger_v2.py +1764 -0
- evalrx/eval_agent/run_metadata.py +208 -0
- evalrx/eval_agent/stages/__init__.py +56 -0
- evalrx/eval_agent/stages/case_discovery.py +293 -0
- evalrx/eval_agent/stages/diagnosis.py +1017 -0
- evalrx/eval_agent/stages/experiment_writer.py +1634 -0
- evalrx/eval_agent/stages/fix_agent.py +3916 -0
- evalrx/eval_agent/stages/fix_internals.py +499 -0
- evalrx/eval_agent/stages/fix_pipeline.py +725 -0
- evalrx/eval_agent/stages/fix_tiers.py +187 -0
- evalrx/eval_agent/stages/fix_tools.py +1034 -0
- evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
- evalrx/eval_agent/stages/probe.py +439 -0
- evalrx/eval_agent/stages/probe_agent.py +1079 -0
- evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
- evalrx/eval_agent/stages/probe_generator.py +326 -0
- evalrx/eval_agent/stages/probe_search_agent.py +106 -0
- evalrx/eval_agent/stages/protocol.py +112 -0
- evalrx/eval_agent/stages/repair_catalog.py +273 -0
- evalrx/eval_agent/stages/surgery.py +524 -0
- evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
- evalrx/eval_agent/store.py +231 -0
- evalrx/logging_utils.py +112 -0
- evalrx/models/__init__.py +161 -0
- evalrx/models/_discover.py +101 -0
- evalrx/models/agent.py +380 -0
- evalrx/models/backends/__init__.py +58 -0
- evalrx/models/backends/api.py +169 -0
- evalrx/models/backends/base.py +57 -0
- evalrx/models/backends/gemini_compat.py +579 -0
- evalrx/models/backends/hf_local.py +2074 -0
- evalrx/models/backends/openai_compat.py +301 -0
- evalrx/models/backends/vllm_offline.py +116 -0
- evalrx/models/base.py +24 -0
- evalrx/models/blackbox/__init__.py +4 -0
- evalrx/models/blackbox/agent.py +31 -0
- evalrx/models/blackbox/base.py +29 -0
- evalrx/models/blackbox/gemini.py +279 -0
- evalrx/models/blackbox/llm_api.py +17 -0
- evalrx/models/blackbox/vlm_api.py +17 -0
- evalrx/models/compose.py +66 -0
- evalrx/models/inference.py +88 -0
- evalrx/models/paper_methods/__init__.py +8 -0
- evalrx/models/paper_methods/aad.py +53 -0
- evalrx/models/paper_methods/ifcd.py +204 -0
- evalrx/models/paper_methods/pai.py +164 -0
- evalrx/models/paper_methods/tcd.py +202 -0
- evalrx/models/paper_methods/vcd.py +45 -0
- evalrx/models/paper_methods/vicrop.py +137 -0
- evalrx/models/toolcodec.py +143 -0
- evalrx/models/tools/__init__.py +20 -0
- evalrx/models/tools/perception.py +300 -0
- evalrx/models/tools/visual.py +174 -0
- evalrx/models/whitebox/__init__.py +26 -0
- evalrx/models/whitebox/agent.py +31 -0
- evalrx/models/whitebox/base.py +24 -0
- evalrx/models/whitebox/qwen.py +61 -0
- evalrx/models/whitebox/qwen2_5_omni.py +29 -0
- evalrx/models/whitebox/qwen2_audio.py +25 -0
- evalrx/models/whitebox/qwen_omni.py +53 -0
- evalrx/models/whitebox/qwen_vl.py +62 -0
- evalrx/observability/__init__.py +21 -0
- evalrx/observability/envelope.py +122 -0
- evalrx/observability/outbox.py +111 -0
- evalrx/observability/tracer.py +882 -0
- evalrx/reporting/__init__.py +28 -0
- evalrx/reporting/case_study.py +947 -0
- evalrx/reporting/compiler.py +587 -0
- evalrx/reporting/dynamic.py +1882 -0
- evalrx/reporting/html_report.py +2225 -0
- evalrx/reporting/langfuse_exporter.py +38 -0
- evalrx/reporting/langfuse_source.py +155 -0
- evalrx/reporting/model.py +151 -0
- evalrx/reporting/run_events.py +184 -0
- evalrx/reporting/server.py +557 -0
- evalrx/reporting/stages.py +58 -0
- evalrx/reporting/static_export.py +142 -0
- evalrx/reporting/web_dist/index.html +146 -0
- evalrx/specs.py +727 -0
- evalrx/stats/__init__.py +47 -0
- evalrx/stats/api.py +192 -0
- evalrx/stats/bootstrap.py +86 -0
- evalrx/stats/ebh.py +27 -0
- evalrx/stats/evalue.py +98 -0
- evalrx/stats/friedman.py +138 -0
- evalrx/stats/mcnemar.py +40 -0
- evalrx/stats/multiplicity.py +159 -0
- evalrx/stats/subset_sampling.py +55 -0
- evalrx/term_links.py +43 -0
- evalrx/viz/__init__.py +7 -0
- evalrx/viz/labels.py +77 -0
- evalrx/viz/prompts.py +39 -0
- evalrx/viz/renderer.py +590 -0
- evalrx/viz/schema.py +36 -0
- evalrx/viz/style.py +134 -0
- evalrx-0.1.2.dist-info/METADATA +532 -0
- evalrx-0.1.2.dist-info/RECORD +339 -0
- evalrx-0.1.2.dist-info/WHEEL +5 -0
- evalrx-0.1.2.dist-info/entry_points.txt +3 -0
- evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
- 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})"
|
evalrx/core/tokentype.py
ADDED
|
@@ -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
|
+
]
|
evalrx/datasets/base.py
ADDED
|
@@ -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"]
|