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
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
"""Case-level study view for paper-method bench reports (audio and image).
|
|
2
|
+
|
|
3
|
+
A bench runner under ``examples/`` — ``examples/agent_loop/qwen2_audio_tcd_mmau/
|
|
4
|
+
run.py`` (TCD x MMAU, audio) or ``examples/vlm_paper_benchmark/run_hf_autofix.py``
|
|
5
|
+
(VCD/PAI/ViCrop x POPE/V*Bench, images) — writes ONE report JSON shaped::
|
|
6
|
+
|
|
7
|
+
{paper, model, splits, hypothesis,
|
|
8
|
+
baseline: {diagnosis, selection: {cases: [{id, output, correct}]}, confirmation},
|
|
9
|
+
auto_fix: {selection: FixOutcome.to_dict(), confirmation}}
|
|
10
|
+
|
|
11
|
+
That report records per-case *outcomes* (which case ids a repair candidate
|
|
12
|
+
flipped) but never the stimulus: the question, its options and the media file
|
|
13
|
+
live in the benchmark manifest the runner read, beside the report. Nothing in
|
|
14
|
+
the product joined the two, so a repaired case was only ever a bare uuid — you
|
|
15
|
+
could not listen to the clip, read the question, or answer it yourself.
|
|
16
|
+
|
|
17
|
+
This module is that join, and it is deliberately Streamlit-free (same split as
|
|
18
|
+
``workbench.py``): it resolves the manifest, normalizes rows from either
|
|
19
|
+
benchmark's field names into one :class:`CaseView`, and labels every case with
|
|
20
|
+
what each repair candidate did to it. The static HTML report renders the result.
|
|
21
|
+
|
|
22
|
+
Manifest resolution prefers an explicit ``report["dataset"]["manifest"]``
|
|
23
|
+
pointer (written by newer runners); older reports fall back to a search of the
|
|
24
|
+
conventional sibling locations, scored by how many of the report's own case ids
|
|
25
|
+
each candidate manifest actually covers — a directory holding five benchmark
|
|
26
|
+
manifests (vlm_paper_benchmark/data/) then resolves to the right one instead of
|
|
27
|
+
the alphabetically-first one.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import re
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any, Iterable
|
|
37
|
+
|
|
38
|
+
#: Manifest field aliases, most specific first. MMAU rows carry
|
|
39
|
+
#: ``instruction``/``choices``/``audio_path``; the VLM slices carry
|
|
40
|
+
#: ``question``/``options``/``image``. Neither runner should have to rename its
|
|
41
|
+
#: data to get a case view.
|
|
42
|
+
QUESTION_FIELDS = ("question", "instruction", "prompt", "query")
|
|
43
|
+
CHOICES_FIELDS = ("choices", "options", "candidates")
|
|
44
|
+
EXPECTED_FIELDS = ("expected", "answer", "label", "gt")
|
|
45
|
+
AUDIO_FIELDS = ("audio_path", "audio", "wav_path", "audio_file")
|
|
46
|
+
IMAGE_FIELDS = ("image", "image_path", "images", "image_file")
|
|
47
|
+
|
|
48
|
+
AUDIO_SUFFIXES = {".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac", ".opus"}
|
|
49
|
+
IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff"}
|
|
50
|
+
|
|
51
|
+
#: Split names in report["baseline"], in reading order. ``diagnosis`` is
|
|
52
|
+
#: summary-only in both runners (no per-case list) and simply yields nothing.
|
|
53
|
+
SPLITS = ("diagnosis", "selection", "confirmation")
|
|
54
|
+
|
|
55
|
+
#: What each repair did to one case, relative to the unmodified baseline.
|
|
56
|
+
FLIP_REPAIRED = "repaired"
|
|
57
|
+
FLIP_BROKE = "broke"
|
|
58
|
+
FLIP_UNCHANGED = "unchanged"
|
|
59
|
+
FLIP_UNTESTED = "untested"
|
|
60
|
+
|
|
61
|
+
# Only ever matches an explicit option marker — "(A) foo", "A. foo", "B) foo" —
|
|
62
|
+
# never a bare answer that happens to start with a letter ("A minor chord").
|
|
63
|
+
_CHOICE_MARKER = re.compile(r"^\s*(?:\(([A-Za-z])\)|([A-Za-z])[.):])\s+(.*)$", re.S)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Report detection
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_bench_report(obj: Any) -> bool:
|
|
72
|
+
"""True for a paper-method bench report (see the module docstring)."""
|
|
73
|
+
if not isinstance(obj, dict):
|
|
74
|
+
return False
|
|
75
|
+
baseline, auto_fix = obj.get("baseline"), obj.get("auto_fix")
|
|
76
|
+
if not isinstance(baseline, dict) or not isinstance(auto_fix, dict):
|
|
77
|
+
return False
|
|
78
|
+
# A baseline split carrying per-case rows is what makes a case study
|
|
79
|
+
# possible at all; a report without one is a summary, not a case book.
|
|
80
|
+
return any(
|
|
81
|
+
isinstance(baseline.get(name), dict) and isinstance(baseline[name].get("cases"), list)
|
|
82
|
+
for name in SPLITS
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def find_bench_reports(root: str | Path) -> list[Path]:
|
|
87
|
+
"""Bench reports under *root*: its own ``*.json`` and ``outputs/*.json``."""
|
|
88
|
+
base = Path(root)
|
|
89
|
+
seen: set[Path] = set()
|
|
90
|
+
found: list[Path] = []
|
|
91
|
+
for pattern in ("*.json", "outputs/*.json", "*/outputs/*.json"):
|
|
92
|
+
for path in sorted(base.glob(pattern)):
|
|
93
|
+
resolved = path.resolve()
|
|
94
|
+
if resolved in seen:
|
|
95
|
+
continue
|
|
96
|
+
seen.add(resolved)
|
|
97
|
+
if is_bench_report(_read_json(path)):
|
|
98
|
+
found.append(path)
|
|
99
|
+
return found
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _read_json(path: Path) -> Any:
|
|
103
|
+
try:
|
|
104
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
105
|
+
except (OSError, ValueError):
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Case model
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class MediaRef:
|
|
116
|
+
"""The stimulus file for one case, resolved against the manifest's dir."""
|
|
117
|
+
|
|
118
|
+
kind: str = "none" # "audio" | "image" | "none"
|
|
119
|
+
path: str = "" # absolute path, "" when unresolved
|
|
120
|
+
declared: str = "" # what the manifest said, for the "missing file" message
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def exists(self) -> bool:
|
|
124
|
+
return bool(self.path) and Path(self.path).is_file()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class CaseView:
|
|
129
|
+
"""One benchmark case: the stimulus, the question, and every arm's answer."""
|
|
130
|
+
|
|
131
|
+
id: str
|
|
132
|
+
split: str
|
|
133
|
+
question: str = ""
|
|
134
|
+
choices: "list[tuple[str, str]]" = field(default_factory=list) # [(letter, text)]
|
|
135
|
+
expected: str = ""
|
|
136
|
+
media: MediaRef = field(default_factory=MediaRef)
|
|
137
|
+
metadata: "dict[str, Any]" = field(default_factory=dict)
|
|
138
|
+
duration_sec: "float | None" = None
|
|
139
|
+
baseline_output: str = ""
|
|
140
|
+
baseline_correct: "bool | None" = None
|
|
141
|
+
#: candidate name -> FLIP_* (what that repair did to THIS case)
|
|
142
|
+
flips: "dict[str, str]" = field(default_factory=dict)
|
|
143
|
+
#: True when the manifest had no row for this id (outcome-only case).
|
|
144
|
+
unresolved: bool = False
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def expected_text(self) -> str:
|
|
148
|
+
for letter, text in self.choices:
|
|
149
|
+
if letter == self.expected.strip().upper():
|
|
150
|
+
return text
|
|
151
|
+
return ""
|
|
152
|
+
|
|
153
|
+
def flipped_by(self) -> "list[str]":
|
|
154
|
+
return [n for n, v in self.flips.items() if v == FLIP_REPAIRED]
|
|
155
|
+
|
|
156
|
+
def broken_by(self) -> "list[str]":
|
|
157
|
+
return [n for n, v in self.flips.items() if v == FLIP_BROKE]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class CaseStudy:
|
|
162
|
+
"""Everything the case-study UI renders for one bench report."""
|
|
163
|
+
|
|
164
|
+
report_path: Path
|
|
165
|
+
report: "dict[str, Any]"
|
|
166
|
+
cases: "list[CaseView]"
|
|
167
|
+
candidates: "list[dict[str, Any]]"
|
|
168
|
+
manifest_path: "Path | None" = None
|
|
169
|
+
manifest_coverage: float = 0.0
|
|
170
|
+
#: Reader-facing warnings (missing manifest, unresolved media, ...).
|
|
171
|
+
notes: "list[str]" = field(default_factory=list)
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def name(self) -> str:
|
|
175
|
+
return self.report_path.stem
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def media_kind(self) -> str:
|
|
179
|
+
for case in self.cases:
|
|
180
|
+
if case.media.kind != "none":
|
|
181
|
+
return case.media.kind
|
|
182
|
+
return "none"
|
|
183
|
+
|
|
184
|
+
def case_by_id(self, case_id: str) -> "CaseView | None":
|
|
185
|
+
return next((c for c in self.cases if c.id == case_id), None)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# Manifest resolution
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _first(row: "dict[str, Any]", fields: Iterable[str]) -> Any:
|
|
194
|
+
for name in fields:
|
|
195
|
+
if row.get(name) not in (None, "", [], {}):
|
|
196
|
+
return row[name]
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def read_manifest(path: Path) -> "dict[str, dict[str, Any]]":
|
|
201
|
+
"""Read a ``.jsonl`` (or ``.json`` list) benchmark manifest, keyed by id."""
|
|
202
|
+
rows: "dict[str, dict[str, Any]]" = {}
|
|
203
|
+
try:
|
|
204
|
+
text = path.read_text(encoding="utf-8")
|
|
205
|
+
except OSError:
|
|
206
|
+
return rows
|
|
207
|
+
if path.suffix == ".json":
|
|
208
|
+
data = _read_json(path)
|
|
209
|
+
records = data if isinstance(data, list) else (data or {}).get("records") or []
|
|
210
|
+
else:
|
|
211
|
+
records = []
|
|
212
|
+
for line in text.splitlines():
|
|
213
|
+
line = line.strip()
|
|
214
|
+
if not line:
|
|
215
|
+
continue
|
|
216
|
+
try:
|
|
217
|
+
records.append(json.loads(line))
|
|
218
|
+
except ValueError:
|
|
219
|
+
continue
|
|
220
|
+
for record in records:
|
|
221
|
+
if isinstance(record, dict) and record.get("id") is not None:
|
|
222
|
+
rows[str(record["id"])] = record
|
|
223
|
+
return rows
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _manifest_search_paths(report_path: Path) -> "list[Path]":
|
|
227
|
+
"""Conventional manifest locations relative to a report file.
|
|
228
|
+
|
|
229
|
+
Both runners write ``<example>/outputs/<name>.json`` beside
|
|
230
|
+
``<example>/data/<manifest>.jsonl``, so the example root is the report's
|
|
231
|
+
grandparent; the flatter layouts are included for hand-assembled dirs.
|
|
232
|
+
"""
|
|
233
|
+
parents = [report_path.parent, report_path.parent.parent, report_path.parent.parent.parent]
|
|
234
|
+
out: "list[Path]" = []
|
|
235
|
+
seen: set[Path] = set()
|
|
236
|
+
for parent in parents:
|
|
237
|
+
for sub in (parent / "data", parent):
|
|
238
|
+
if not sub.is_dir() or sub in seen:
|
|
239
|
+
continue
|
|
240
|
+
seen.add(sub)
|
|
241
|
+
out.extend(sorted(sub.glob("*.jsonl")))
|
|
242
|
+
out.extend(sorted(sub.glob("*.json")))
|
|
243
|
+
return [p for p in out if p != report_path]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def resolve_manifest(
|
|
247
|
+
report: "dict[str, Any]", report_path: Path, case_ids: "set[str]"
|
|
248
|
+
) -> "tuple[Path | None, dict[str, dict[str, Any]], float]":
|
|
249
|
+
"""Find the manifest whose ids best cover *case_ids*.
|
|
250
|
+
|
|
251
|
+
An explicit ``report["dataset"]["manifest"]`` pointer wins outright (it is
|
|
252
|
+
the runner stating what it read); otherwise every conventional sibling is
|
|
253
|
+
scored by id coverage, which is what disambiguates a data dir holding
|
|
254
|
+
several benchmarks' manifests.
|
|
255
|
+
"""
|
|
256
|
+
declared = ((report.get("dataset") or {}).get("manifest") or "").strip()
|
|
257
|
+
if declared:
|
|
258
|
+
path = Path(declared)
|
|
259
|
+
if not path.is_absolute():
|
|
260
|
+
for base in (report_path.parent, report_path.parent.parent):
|
|
261
|
+
if (base / declared).is_file():
|
|
262
|
+
path = base / declared
|
|
263
|
+
break
|
|
264
|
+
if path.is_file():
|
|
265
|
+
rows = read_manifest(path)
|
|
266
|
+
covered = len(case_ids & set(rows)) / max(1, len(case_ids))
|
|
267
|
+
return path, rows, covered
|
|
268
|
+
|
|
269
|
+
best: "tuple[Path | None, dict[str, dict[str, Any]], float]" = (None, {}, 0.0)
|
|
270
|
+
for path in _manifest_search_paths(report_path):
|
|
271
|
+
rows = read_manifest(path)
|
|
272
|
+
if not rows:
|
|
273
|
+
continue
|
|
274
|
+
covered = len(case_ids & set(rows)) / max(1, len(case_ids))
|
|
275
|
+
if covered > best[2]:
|
|
276
|
+
best = (path, rows, covered)
|
|
277
|
+
if covered == 1.0:
|
|
278
|
+
break
|
|
279
|
+
return best
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _media_ref(row: "dict[str, Any]", media_root: Path) -> MediaRef:
|
|
283
|
+
"""Resolve a manifest row's stimulus file. Field name decides the kind
|
|
284
|
+
first (``audio_path`` is audio even if the suffix is unusual); the file
|
|
285
|
+
suffix decides for a generic field name."""
|
|
286
|
+
raw = _first(row, AUDIO_FIELDS)
|
|
287
|
+
kind = "audio" if raw is not None else ""
|
|
288
|
+
if raw is None:
|
|
289
|
+
raw = _first(row, IMAGE_FIELDS)
|
|
290
|
+
kind = "image" if raw is not None else ""
|
|
291
|
+
if raw is None:
|
|
292
|
+
return MediaRef()
|
|
293
|
+
if isinstance(raw, (list, tuple)): # multi-image rows: the first is the view
|
|
294
|
+
raw = raw[0] if raw else None
|
|
295
|
+
if not isinstance(raw, str) or not raw:
|
|
296
|
+
return MediaRef()
|
|
297
|
+
|
|
298
|
+
suffix = Path(raw).suffix.lower()
|
|
299
|
+
if suffix in AUDIO_SUFFIXES:
|
|
300
|
+
kind = "audio"
|
|
301
|
+
elif suffix in IMAGE_SUFFIXES:
|
|
302
|
+
kind = "image"
|
|
303
|
+
path = Path(raw)
|
|
304
|
+
if not path.is_absolute():
|
|
305
|
+
path = media_root / raw
|
|
306
|
+
return MediaRef(kind=kind or "none", path=str(path), declared=raw)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def split_choice(raw: Any, index: int) -> "tuple[str, str]":
|
|
310
|
+
"""``"(A) siren"`` -> ``("A", "siren")``; an unmarked option gets its
|
|
311
|
+
positional letter, so ``["red", "blue"]`` still reads as A/B."""
|
|
312
|
+
text = str(raw or "").strip()
|
|
313
|
+
match = _CHOICE_MARKER.match(text)
|
|
314
|
+
if match:
|
|
315
|
+
letter = (match.group(1) or match.group(2) or "").upper()
|
|
316
|
+
return letter, match.group(3).strip()
|
|
317
|
+
return chr(ord("A") + index), text
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# Build
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _flip_map(attempted: "list[dict[str, Any]]", tested_ids: "set[str]") -> "dict[str, dict[str, str]]":
|
|
326
|
+
"""case_id -> {candidate name: FLIP_*} for every case in the sweep."""
|
|
327
|
+
per_case: "dict[str, dict[str, str]]" = {cid: {} for cid in tested_ids}
|
|
328
|
+
for attempt in attempted:
|
|
329
|
+
name = str(attempt.get("name") or "")
|
|
330
|
+
if not name:
|
|
331
|
+
continue
|
|
332
|
+
repaired = {str(c) for c in attempt.get("fixed_cases") or []}
|
|
333
|
+
broke = {str(c) for c in attempt.get("broken_cases") or []}
|
|
334
|
+
for cid in tested_ids:
|
|
335
|
+
per_case.setdefault(cid, {})[name] = (
|
|
336
|
+
FLIP_REPAIRED if cid in repaired
|
|
337
|
+
else FLIP_BROKE if cid in broke
|
|
338
|
+
else FLIP_UNCHANGED
|
|
339
|
+
)
|
|
340
|
+
return per_case
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def build_case_study(report_path: str | Path) -> "CaseStudy | None":
|
|
344
|
+
"""Join a bench report with its benchmark manifest into a case book."""
|
|
345
|
+
path = Path(report_path)
|
|
346
|
+
report = _read_json(path)
|
|
347
|
+
if not is_bench_report(report):
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
baseline = report.get("baseline") or {}
|
|
351
|
+
outcomes: "list[tuple[str, dict[str, Any]]]" = []
|
|
352
|
+
for split in SPLITS:
|
|
353
|
+
block = baseline.get(split)
|
|
354
|
+
if isinstance(block, dict):
|
|
355
|
+
for case in block.get("cases") or []:
|
|
356
|
+
if isinstance(case, dict) and case.get("id") is not None:
|
|
357
|
+
outcomes.append((split, case))
|
|
358
|
+
|
|
359
|
+
case_ids = {str(c["id"]) for _, c in outcomes}
|
|
360
|
+
manifest_path, rows, coverage = resolve_manifest(report, path, case_ids)
|
|
361
|
+
media_root = manifest_path.parent if manifest_path else path.parent
|
|
362
|
+
|
|
363
|
+
selection = (report.get("auto_fix") or {}).get("selection") or {}
|
|
364
|
+
attempted = [a for a in selection.get("attempted") or [] if isinstance(a, dict)]
|
|
365
|
+
# Candidates are validated on the SELECTION split only — a confirmation-split
|
|
366
|
+
# case was never in the sweep and must not be shown as "unchanged by" a
|
|
367
|
+
# repair that never ran on it.
|
|
368
|
+
tested_ids = {str(c["id"]) for split, c in outcomes if split == "selection"}
|
|
369
|
+
flips = _flip_map(attempted, tested_ids)
|
|
370
|
+
|
|
371
|
+
cases: "list[CaseView]" = []
|
|
372
|
+
for split, outcome in outcomes:
|
|
373
|
+
cid = str(outcome["id"])
|
|
374
|
+
row = rows.get(cid) or {}
|
|
375
|
+
raw_choices = _first(row, CHOICES_FIELDS) or []
|
|
376
|
+
choices = [split_choice(c, i) for i, c in enumerate(raw_choices)]
|
|
377
|
+
duration = row.get("duration_sec")
|
|
378
|
+
cases.append(CaseView(
|
|
379
|
+
id=cid,
|
|
380
|
+
split=split,
|
|
381
|
+
question=str(_first(row, QUESTION_FIELDS) or ""),
|
|
382
|
+
choices=choices,
|
|
383
|
+
expected=str(_first(row, EXPECTED_FIELDS) or "").strip(),
|
|
384
|
+
media=_media_ref(row, media_root),
|
|
385
|
+
metadata={**(row.get("metadata") or {}),
|
|
386
|
+
**({"task": row["task"]} if row.get("task") else {})},
|
|
387
|
+
duration_sec=float(duration) if isinstance(duration, (int, float)) else None,
|
|
388
|
+
baseline_output=str(outcome.get("output", "")),
|
|
389
|
+
baseline_correct=(
|
|
390
|
+
bool(outcome["correct"]) if outcome.get("correct") is not None else None
|
|
391
|
+
),
|
|
392
|
+
flips=dict(flips.get(cid, {})) if split == "selection" else {},
|
|
393
|
+
unresolved=not row,
|
|
394
|
+
))
|
|
395
|
+
|
|
396
|
+
notes: "list[str]" = []
|
|
397
|
+
if manifest_path is None:
|
|
398
|
+
notes.append(
|
|
399
|
+
"No benchmark manifest was found next to this report, so questions, "
|
|
400
|
+
"options and media cannot be shown — only per-case outcomes. Expected "
|
|
401
|
+
"a `.jsonl` manifest under the example's `data/` directory."
|
|
402
|
+
)
|
|
403
|
+
elif coverage < 1.0:
|
|
404
|
+
missing = sum(1 for c in cases if c.unresolved)
|
|
405
|
+
notes.append(
|
|
406
|
+
f"{manifest_path.name} covers {coverage:.0%} of this report's cases; "
|
|
407
|
+
f"{missing} case(s) have outcomes but no question/media row."
|
|
408
|
+
)
|
|
409
|
+
missing_media = [c for c in cases if c.media.kind != "none" and not c.media.exists]
|
|
410
|
+
if missing_media:
|
|
411
|
+
notes.append(
|
|
412
|
+
f"{len(missing_media)} case(s) reference a media file that is not on "
|
|
413
|
+
f"disk (e.g. `{missing_media[0].media.declared}`) — re-run the "
|
|
414
|
+
"example's download step to restore playback."
|
|
415
|
+
)
|
|
416
|
+
if attempted:
|
|
417
|
+
notes.append(
|
|
418
|
+
"Repair candidates record which cases they flipped, not what they "
|
|
419
|
+
"answered: FixAgent stores per-case pass/fail, so a repaired case "
|
|
420
|
+
"shows the flip, not the candidate's own output text."
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
return CaseStudy(
|
|
424
|
+
report_path=path,
|
|
425
|
+
report=report,
|
|
426
|
+
cases=cases,
|
|
427
|
+
candidates=[describe_candidate(a) for a in attempted],
|
|
428
|
+
manifest_path=manifest_path,
|
|
429
|
+
manifest_coverage=coverage,
|
|
430
|
+
notes=notes,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# ---------------------------------------------------------------------------
|
|
435
|
+
# Repair-method description
|
|
436
|
+
# ---------------------------------------------------------------------------
|
|
437
|
+
|
|
438
|
+
#: One line per candidate ``kind`` saying what the intervention actually does.
|
|
439
|
+
#: Sourced from this repo's own implementations (``models/paper_methods/*.py``,
|
|
440
|
+
#: ``eval_agent/stages/fix_agent.py``) so the UI never invents a mechanism.
|
|
441
|
+
METHOD_NOTES: "dict[str, str]" = {
|
|
442
|
+
"tcd": (
|
|
443
|
+
"Temporal Contrastive Decoding (Li et al. 2026, arXiv:2604.15383): re-encodes "
|
|
444
|
+
"a Hann-window-blurred copy of the waveform once before decoding, then adds a "
|
|
445
|
+
"positive-rectified contrast between the clean and blurred logits over a small "
|
|
446
|
+
"candidate set. Blur window and update scale are per-example adaptive."
|
|
447
|
+
),
|
|
448
|
+
"vcd": (
|
|
449
|
+
"Visual Contrastive Decoding: contrasts the clean-image logits against a "
|
|
450
|
+
"diffusion-noised copy of the same image to subtract language-prior mass."
|
|
451
|
+
),
|
|
452
|
+
"icd": (
|
|
453
|
+
"Instruction Contrastive Decoding: contrasts against a negatively-instructed "
|
|
454
|
+
"forward pass instead of a corrupted input."
|
|
455
|
+
),
|
|
456
|
+
"ifcd": (
|
|
457
|
+
"Internal Fact-Contrastive Decoding: contrasts against a representation-edited "
|
|
458
|
+
"forward pass produced by a trained editor."
|
|
459
|
+
),
|
|
460
|
+
"pai": (
|
|
461
|
+
"Pay Attention to Image: scales image-attention logits during decoding "
|
|
462
|
+
"(an internals WRITE, not a read-only decode contrast)."
|
|
463
|
+
),
|
|
464
|
+
"vicrop": (
|
|
465
|
+
"ViCrop (MLLMs Know Where to Look, ICLR 2025): reads task-vs-general attention "
|
|
466
|
+
"to pick a crop, then answers from the original image plus that crop."
|
|
467
|
+
),
|
|
468
|
+
"vicrop_consensus": (
|
|
469
|
+
"ViCrop with a label-free consensus guard: keeps the crop answer only when it "
|
|
470
|
+
"agrees with the baseline answer."
|
|
471
|
+
),
|
|
472
|
+
"opera": (
|
|
473
|
+
"OPERA's binary specialization: penalises next-token candidates that neglect "
|
|
474
|
+
"the image, scoped to one-token yes/no answers."
|
|
475
|
+
),
|
|
476
|
+
"template": (
|
|
477
|
+
"Prompt template (L1): wraps the unchanged task prompt in extra instruction "
|
|
478
|
+
"text. Nothing about the model, the input media or the decoder changes."
|
|
479
|
+
),
|
|
480
|
+
"spec": (
|
|
481
|
+
"Input/scaffold spec (L2): a declarative pipeline — media ops, prompt template, "
|
|
482
|
+
"sampling settings and a multi-call strategy — run around the same model."
|
|
483
|
+
),
|
|
484
|
+
"code": "Coded pipeline (L2): agent-written Python run in a sandbox around the model.",
|
|
485
|
+
"primitive": "Pre-audited internals primitive, parameterised by the judge.",
|
|
486
|
+
"finetune_spec": "LoRA repair (L4): parameter-space fine-tuning on a repair pool.",
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#: Multi-call strategies a PipelineSpec can name in ``payload["strategy"]``.
|
|
490
|
+
#: These are the ONLY values ``fix_tools._SCAFFOLD_STRATEGIES`` admits besides
|
|
491
|
+
#: ``direct`` — a spec with no strategy runs ``n_samples`` plain calls and
|
|
492
|
+
#: majority-votes them, which is what `self_consistency_5` actually is.
|
|
493
|
+
STRATEGY_NOTES: "dict[str, str]" = {
|
|
494
|
+
"self_refine": "answer once, critique that answer, then revise it (deterministic, single path).",
|
|
495
|
+
"least_to_most": "break the task into subproblems, then answer it using that decomposition.",
|
|
496
|
+
"chain_of_verification": "answer once, list independent checks for that answer, then re-answer under them.",
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
#: The ordered call chain each strategy issues, as ``(step label, instruction
|
|
500
|
+
#: prepended for that call)``; an empty instruction means the call sends the
|
|
501
|
+
#: task prompt unchanged.
|
|
502
|
+
#:
|
|
503
|
+
#: This MIRRORS ``fix_tools.STRATEGY_CALLS``, which is what actually formats
|
|
504
|
+
#: the calls. It is copied rather than imported because ``evalrx.analysis``
|
|
505
|
+
#: must stay usable standalone and may not import ``evalrx.eval_agent`` at
|
|
506
|
+
#: runtime (locked by test_agent_architecture). Drift is impossible anyway:
|
|
507
|
+
#: ``test_strategy_flow_matches_what_the_pipeline_sends`` asserts the two are
|
|
508
|
+
#: equal, so re-wording a prompt on either side fails the suite.
|
|
509
|
+
STRATEGY_CALLS: "dict[str, tuple[tuple[str, str], ...]]" = {
|
|
510
|
+
"self_refine": (
|
|
511
|
+
("answer", ""),
|
|
512
|
+
("critique that answer",
|
|
513
|
+
"Check the attempted answer for factual, reasoning, arithmetic, and "
|
|
514
|
+
"instruction-following errors. Give concise correction advice only."),
|
|
515
|
+
("revise it",
|
|
516
|
+
"Produce a corrected final answer to the original task using the "
|
|
517
|
+
"feedback. Do not discuss the revision process."),
|
|
518
|
+
),
|
|
519
|
+
"least_to_most": (
|
|
520
|
+
("break into subproblems",
|
|
521
|
+
"Break the following task into the smallest useful subproblems. "
|
|
522
|
+
"Do not answer the task yet."),
|
|
523
|
+
("solve using the decomposition",
|
|
524
|
+
"Solve the original task using the decomposition. Give only the final "
|
|
525
|
+
"answer required by the task."),
|
|
526
|
+
),
|
|
527
|
+
"chain_of_verification": (
|
|
528
|
+
("answer", ""),
|
|
529
|
+
("list verification checks",
|
|
530
|
+
"List short, independent checks needed to verify this attempted answer. "
|
|
531
|
+
"Do not answer the original task yet."),
|
|
532
|
+
("answer after the checks",
|
|
533
|
+
"Answer the original task after applying the independent verification "
|
|
534
|
+
"checks. Give only the final answer required by the task."),
|
|
535
|
+
),
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def strategy_steps(strategy: str, *, n_samples: int = 1) -> "list[tuple[str, str]]":
|
|
540
|
+
"""The call chain to draw for one spec, as ``(label, prompt sent)``.
|
|
541
|
+
|
|
542
|
+
A named strategy has a fixed chain. Everything else is ``direct``: it makes
|
|
543
|
+
``n_samples`` independent calls and majority-votes them, so it is only
|
|
544
|
+
worth drawing when there is more than one."""
|
|
545
|
+
calls = STRATEGY_CALLS.get(strategy)
|
|
546
|
+
if calls:
|
|
547
|
+
return [(label, prompt) for label, prompt in calls]
|
|
548
|
+
if n_samples > 1:
|
|
549
|
+
return [
|
|
550
|
+
(f"sample {n_samples} answers independently", ""),
|
|
551
|
+
("take the majority answer", ""),
|
|
552
|
+
]
|
|
553
|
+
return []
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def describe_candidate(attempt: "dict[str, Any]") -> "dict[str, Any]":
|
|
557
|
+
"""Normalize one ``attempted`` entry into what the Repair-methods UI shows."""
|
|
558
|
+
payload = attempt.get("payload") if isinstance(attempt.get("payload"), dict) else {}
|
|
559
|
+
kind = str(attempt.get("kind") or "spec")
|
|
560
|
+
strategy = str(payload.get("strategy") or "")
|
|
561
|
+
# The tier label on the card already says "L2", and the verdict legend says
|
|
562
|
+
# what a tier is — so the generic kind blurb costs a paragraph without
|
|
563
|
+
# describing THIS candidate. When a strategy is known it says the specific
|
|
564
|
+
# thing instead; the kind blurb is only the fallback.
|
|
565
|
+
note = STRATEGY_NOTES.get(strategy) or METHOD_NOTES.get(kind, "")
|
|
566
|
+
|
|
567
|
+
prompt_template = str(payload.get("prompt_template") or "")
|
|
568
|
+
knobs = {
|
|
569
|
+
k: v for k, v in payload.items()
|
|
570
|
+
if k not in {"prompt_template", "name", "strategy"} and v not in (None, "", [], {})
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
"name": str(attempt.get("name") or ""),
|
|
574
|
+
"tier": str(attempt.get("tier") or ""),
|
|
575
|
+
"kind": kind,
|
|
576
|
+
"source": str(attempt.get("source") or ""),
|
|
577
|
+
"note": note,
|
|
578
|
+
"strategy": strategy,
|
|
579
|
+
"strategy_steps": strategy_steps(
|
|
580
|
+
strategy, n_samples=int(payload.get("n_samples") or 1)
|
|
581
|
+
),
|
|
582
|
+
"prompt_template": prompt_template,
|
|
583
|
+
"knobs": knobs,
|
|
584
|
+
"defaults_only": not payload,
|
|
585
|
+
"n_fixed": attempt.get("n_fixed"),
|
|
586
|
+
"n_broken": attempt.get("n_broken"),
|
|
587
|
+
"n_pairs": attempt.get("n_pairs"),
|
|
588
|
+
"effect": attempt.get("effect"),
|
|
589
|
+
"e_value": attempt.get("e_value"),
|
|
590
|
+
"verdict": str(attempt.get("verdict") or ""),
|
|
591
|
+
"summary": str(attempt.get("summary") or ""),
|
|
592
|
+
"fixed_cases": [str(c) for c in attempt.get("fixed_cases") or []],
|
|
593
|
+
"broken_cases": [str(c) for c in attempt.get("broken_cases") or []],
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
# ---------------------------------------------------------------------------
|
|
598
|
+
# Facets / aggregation
|
|
599
|
+
# ---------------------------------------------------------------------------
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def facet_keys(cases: "list[CaseView]", *, max_values: int = 20) -> "list[str]":
|
|
603
|
+
"""Metadata keys worth offering as filters: present, categorical, not unique."""
|
|
604
|
+
values: "dict[str, set[str]]" = {}
|
|
605
|
+
for case in cases:
|
|
606
|
+
for key, value in (case.metadata or {}).items():
|
|
607
|
+
if isinstance(value, (dict, list)):
|
|
608
|
+
continue
|
|
609
|
+
values.setdefault(key, set()).add(str(value))
|
|
610
|
+
return sorted(
|
|
611
|
+
k for k, vals in values.items()
|
|
612
|
+
if 2 <= len(vals) <= max_values and len(vals) < max(2, len(cases))
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def facet_values(cases: "list[CaseView]", key: str) -> "list[str]":
|
|
617
|
+
return sorted({str(c.metadata.get(key)) for c in cases if c.metadata.get(key) is not None})
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def accuracy_by_facet(cases: "list[CaseView]", key: str) -> "list[dict[str, Any]]":
|
|
621
|
+
"""Baseline accuracy grouped by one metadata key, most-cases first."""
|
|
622
|
+
buckets: "dict[str, list[bool]]" = {}
|
|
623
|
+
for case in cases:
|
|
624
|
+
if case.baseline_correct is None or case.metadata.get(key) is None:
|
|
625
|
+
continue
|
|
626
|
+
buckets.setdefault(str(case.metadata[key]), []).append(case.baseline_correct)
|
|
627
|
+
rows = [
|
|
628
|
+
{"value": value, "n": len(hits), "correct": sum(hits),
|
|
629
|
+
"accuracy": sum(hits) / len(hits)}
|
|
630
|
+
for value, hits in buckets.items()
|
|
631
|
+
]
|
|
632
|
+
rows.sort(key=lambda r: (-r["n"], r["value"]))
|
|
633
|
+
return rows
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def case_label(case: CaseView, *, blind: bool = False) -> str:
|
|
637
|
+
"""Compact picker label: outcome badge, split, then the question stem."""
|
|
638
|
+
if blind:
|
|
639
|
+
badge = "•"
|
|
640
|
+
elif case.baseline_correct is None:
|
|
641
|
+
badge = "?"
|
|
642
|
+
else:
|
|
643
|
+
badge = "✓" if case.baseline_correct else "✗"
|
|
644
|
+
if not blind and case.flipped_by():
|
|
645
|
+
badge = "🛠"
|
|
646
|
+
elif not blind and case.broken_by():
|
|
647
|
+
badge = "💥"
|
|
648
|
+
stem = (case.question or case.id).strip().replace("\n", " ")
|
|
649
|
+
if len(stem) > 68:
|
|
650
|
+
stem = stem[:65] + "…"
|
|
651
|
+
return f"{badge} [{case.split[:4]}] {stem}"
|