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,30 @@
|
|
|
1
|
+
"""Activation patching / causal tracing (Meng et al., ROME) (Stage 2).
|
|
2
|
+
|
|
3
|
+
The only CAUSAL white-box method here: read a clean run's activations and WRITE
|
|
4
|
+
them into a corrupted run to localise where information is causally used.
|
|
5
|
+
``requires=HIDDEN_STATES`` (read+write hooks — via nnsight). Memory ∝ layers×positions.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- Locating and Editing Factual Associations in GPT (ROME, causal tracing)
|
|
9
|
+
Meng et al., NeurIPS 2022 — arXiv:2202.05262
|
|
10
|
+
- Attribution Patching (scalable approximation): Neel Nanda, 2023.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from evalrx.core.analyzer import Analyzer
|
|
16
|
+
from evalrx.core.capability import Capability
|
|
17
|
+
from evalrx.core.registry import register_analyzer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@register_analyzer("causal_trace")
|
|
21
|
+
class CausalTraceAnalyzer(Analyzer):
|
|
22
|
+
name = "causal_trace"
|
|
23
|
+
requires = frozenset({Capability.HIDDEN_STATES})
|
|
24
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
25
|
+
|
|
26
|
+
def _run(self, model, cases):
|
|
27
|
+
raise NotImplementedError(
|
|
28
|
+
"Stage 2: clean/corrupt runs + patch cached activations at each (layer, position) "
|
|
29
|
+
"via nnsight; report the causal-effect map. Needs read+write hooks (beyond HF flags)."
|
|
30
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Input-perturbation analyzers (black-box-feasible; cost driver = many forwards)."""
|
|
2
|
+
|
|
3
|
+
from evalrx.analyzers.perturbation.context_shap import ContextShapAnalyzer
|
|
4
|
+
from evalrx.analyzers.perturbation.cot_faithfulness import CoTFaithfulnessAnalyzer
|
|
5
|
+
from evalrx.analyzers.perturbation.format_sensitivity import FormatSensitivityAnalyzer
|
|
6
|
+
from evalrx.analyzers.perturbation.mm_shap import MMShapAnalyzer
|
|
7
|
+
from evalrx.analyzers.perturbation.modality_ablation import ModalityAblationAnalyzer
|
|
8
|
+
from evalrx.analyzers.perturbation.perturbation_battery import PerturbationBattery
|
|
9
|
+
from evalrx.analyzers.perturbation.prompt_contrast import PromptContrastAnalyzer
|
|
10
|
+
from evalrx.analyzers.perturbation.rise import RISEAnalyzer
|
|
11
|
+
from evalrx.analyzers.perturbation.vl_shap import VLShapAnalyzer
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"RISEAnalyzer",
|
|
15
|
+
"VLShapAnalyzer",
|
|
16
|
+
"MMShapAnalyzer",
|
|
17
|
+
"ModalityAblationAnalyzer",
|
|
18
|
+
"PromptContrastAnalyzer",
|
|
19
|
+
"FormatSensitivityAnalyzer",
|
|
20
|
+
"CoTFaithfulnessAnalyzer",
|
|
21
|
+
"ContextShapAnalyzer",
|
|
22
|
+
"PerturbationBattery",
|
|
23
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Permutation-sampling Shapley values — shared by MM-SHAP and VL-SHAP.
|
|
2
|
+
|
|
3
|
+
Estimates each player's Shapley value (mean marginal contribution to a coalition
|
|
4
|
+
value) by sampling random permutations. ``value_fn(kept: set) -> float`` is the
|
|
5
|
+
coalition value (e.g. the model's logprob/confidence with only ``kept`` players
|
|
6
|
+
present). Results are memoised by coalition so expensive model calls aren't
|
|
7
|
+
repeated. For an additive game the estimate equals each player's exact weight.
|
|
8
|
+
|
|
9
|
+
References:
|
|
10
|
+
- Shapley value: 'A Value for n-Person Games', Shapley (1953), Contributions to the Theory of Games II.
|
|
11
|
+
- Sampling estimator: 'Polynomial calculation of the Shapley value based on sampling',
|
|
12
|
+
Castro, Gomez & Tejada (2009), Computers & Operations Research 36(5).
|
|
13
|
+
- ML attribution: 'A Unified Approach to Interpreting Model Predictions' (SHAP),
|
|
14
|
+
Lundberg & Lee, NeurIPS 2017 — arXiv:1705.07874
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import random
|
|
20
|
+
from typing import Callable, Iterable
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def shapley_values(
|
|
24
|
+
players: Iterable,
|
|
25
|
+
value_fn: Callable[[set], float],
|
|
26
|
+
n_samples: int = 64,
|
|
27
|
+
seed: int = 0,
|
|
28
|
+
) -> dict:
|
|
29
|
+
"""Return ``{player: shapley_value}`` via permutation sampling."""
|
|
30
|
+
players = list(players)
|
|
31
|
+
phi = {p: 0.0 for p in players}
|
|
32
|
+
if not players:
|
|
33
|
+
return phi
|
|
34
|
+
rng = random.Random(seed)
|
|
35
|
+
memo: dict = {}
|
|
36
|
+
|
|
37
|
+
def val(kept: set) -> float:
|
|
38
|
+
key = frozenset(kept)
|
|
39
|
+
if key not in memo:
|
|
40
|
+
memo[key] = float(value_fn(set(kept)))
|
|
41
|
+
return memo[key]
|
|
42
|
+
|
|
43
|
+
base = val(set())
|
|
44
|
+
for _ in range(n_samples):
|
|
45
|
+
perm = players[:]
|
|
46
|
+
rng.shuffle(perm)
|
|
47
|
+
kept: set = set()
|
|
48
|
+
prev = base
|
|
49
|
+
for p in perm:
|
|
50
|
+
kept.add(p)
|
|
51
|
+
cur = val(kept)
|
|
52
|
+
phi[p] += cur - prev
|
|
53
|
+
prev = cur
|
|
54
|
+
return {p: phi[p] / n_samples for p in players}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Context-chunk Shapley — which parts of the provided context carry the answer?
|
|
2
|
+
|
|
3
|
+
The text sibling of :class:`ToolShap` and MM-SHAP: ablate CHUNKS OF THE GIVEN
|
|
4
|
+
CONTEXT (paragraphs or sentences) instead of tools or image patches, and
|
|
5
|
+
Shapley-attribute the answer to them. Directly serves RAG/long-context LLM
|
|
6
|
+
diagnosis: a failing case whose answer depends on no chunk is answering from
|
|
7
|
+
priors; one dominated by a single chunk inherits that chunk's quality.
|
|
8
|
+
|
|
9
|
+
Black-box (``requires=GENERATE``): the coalition value is the similarity of
|
|
10
|
+
the ablated-context answer to the full-context baseline answer. Uses the
|
|
11
|
+
shared permutation-sampling estimator (memoised), so model calls are bounded.
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
- TokenSHAP: Interpreting Large Language Models with Monte Carlo Shapley Value
|
|
15
|
+
Estimation — Goldshmidt & Horovicz, 2024 — arXiv:2407.10114
|
|
16
|
+
- ContextCite: Attributing Model Generation to Context —
|
|
17
|
+
Cohen-Wang et al., NeurIPS 2024 — arXiv:2409.00729
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import dataclasses
|
|
23
|
+
import difflib
|
|
24
|
+
import re
|
|
25
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
26
|
+
|
|
27
|
+
from evalrx.analyzers.perturbation._shapley import shapley_values
|
|
28
|
+
from evalrx.core.analyzer import Analyzer
|
|
29
|
+
from evalrx.core.capability import Capability
|
|
30
|
+
from evalrx.core.registry import register_analyzer
|
|
31
|
+
from evalrx.core.result import Result
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from evalrx.core.case import CaseBatch, FailureCase
|
|
35
|
+
from evalrx.core.model import Model
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _similarity(a: str, b: str) -> float:
|
|
39
|
+
na = " ".join(str(a or "").lower().split())
|
|
40
|
+
nb = " ".join(str(b or "").lower().split())
|
|
41
|
+
if not na and not nb:
|
|
42
|
+
return 1.0
|
|
43
|
+
return difflib.SequenceMatcher(None, na, nb).ratio()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def default_context_fn(case: "FailureCase") -> Optional[str]:
|
|
47
|
+
"""The ablatable context: ``metadata['context']`` when present."""
|
|
48
|
+
meta = case.metadata if isinstance(case.metadata, dict) else {}
|
|
49
|
+
value = meta.get("context")
|
|
50
|
+
return str(value) if value else None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def split_chunks(context: str, granularity: str, max_chunks: int) -> list[str]:
|
|
54
|
+
if granularity == "sentence":
|
|
55
|
+
parts = [p.strip() for p in re.split(r"(?<=[.!?])\s+", context) if p.strip()]
|
|
56
|
+
else:
|
|
57
|
+
parts = [p.strip() for p in re.split(r"\n\s*\n", context) if p.strip()]
|
|
58
|
+
if len(parts) <= 1: # single paragraph — fall back to sentences
|
|
59
|
+
parts = [p.strip() for p in re.split(r"(?<=[.!?])\s+", context) if p.strip()]
|
|
60
|
+
if len(parts) > max_chunks: # merge the tail so players stay bounded
|
|
61
|
+
head, tail = parts[: max_chunks - 1], " ".join(parts[max_chunks - 1:])
|
|
62
|
+
parts = head + [tail]
|
|
63
|
+
return parts
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@register_analyzer("context_shap")
|
|
67
|
+
class ContextShapAnalyzer(Analyzer):
|
|
68
|
+
"""Shapley attribution of the answer to provided-context chunks (RAG dependence probe).
|
|
69
|
+
|
|
70
|
+
Hyper-parameters:
|
|
71
|
+
granularity: ``"paragraph"`` (default) or ``"sentence"`` chunking.
|
|
72
|
+
n_samples: permutation samples for the Shapley estimator.
|
|
73
|
+
max_chunks: chunk cap per case (tail chunks are merged).
|
|
74
|
+
max_cases: label-stratified cap on probed cases; 0 (the default) = every case.
|
|
75
|
+
seed: permutation-sampling seed.
|
|
76
|
+
context_fn: ``callable(case) -> str | None`` supplying the ablatable
|
|
77
|
+
context (default: ``metadata['context']``). The context
|
|
78
|
+
must appear verbatim inside the prompt.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
name = "context_shap"
|
|
82
|
+
requires = frozenset({Capability.GENERATE})
|
|
83
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
granularity: str = "paragraph",
|
|
88
|
+
n_samples: int = 16,
|
|
89
|
+
max_chunks: int = 6,
|
|
90
|
+
max_cases: int = 0,
|
|
91
|
+
seed: int = 0,
|
|
92
|
+
context_fn: Optional[Callable[["FailureCase"], Optional[str]]] = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
super().__init__(
|
|
95
|
+
granularity=granularity,
|
|
96
|
+
n_samples=max(1, int(n_samples)),
|
|
97
|
+
max_chunks=max(2, int(max_chunks)),
|
|
98
|
+
max_cases=max_cases,
|
|
99
|
+
seed=seed,
|
|
100
|
+
)
|
|
101
|
+
# ctor name, so sklearn-style get_params() reflection works
|
|
102
|
+
self.context_fn = context_fn or default_context_fn
|
|
103
|
+
|
|
104
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
105
|
+
per_case: list[dict[str, Any]] = []
|
|
106
|
+
for case in cases.stratified_head(self.max_cases):
|
|
107
|
+
context = self.context_fn(case)
|
|
108
|
+
prompt = case.inputs.prompt or ""
|
|
109
|
+
entry: dict[str, Any] = {"sample_id": case.id}
|
|
110
|
+
if not context:
|
|
111
|
+
entry["skipped"] = "no context supplied (metadata['context'] or context_fn)"
|
|
112
|
+
per_case.append(entry)
|
|
113
|
+
continue
|
|
114
|
+
if prompt.count(context) != 1:
|
|
115
|
+
entry["skipped"] = (
|
|
116
|
+
"context must occur exactly once verbatim in the prompt "
|
|
117
|
+
f"(found {prompt.count(context)} occurrences)"
|
|
118
|
+
)
|
|
119
|
+
per_case.append(entry)
|
|
120
|
+
continue
|
|
121
|
+
chunks = split_chunks(context, self.granularity, self.max_chunks)
|
|
122
|
+
entry["n_chunks"] = len(chunks)
|
|
123
|
+
if len(chunks) < 2:
|
|
124
|
+
entry["skipped"] = "context has a single chunk — nothing to attribute"
|
|
125
|
+
per_case.append(entry)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
baseline = str(case.observed or "") or str(model.generate(case.inputs))
|
|
129
|
+
|
|
130
|
+
def value_fn(kept: set) -> float:
|
|
131
|
+
kept_text = "\n\n".join(chunks[i] for i in sorted(kept))
|
|
132
|
+
ablated = prompt.replace(context, kept_text, 1)
|
|
133
|
+
answer = str(model.generate(dataclasses.replace(case.inputs, prompt=ablated)))
|
|
134
|
+
return _similarity(answer, baseline)
|
|
135
|
+
|
|
136
|
+
coalition_cache: dict[frozenset, float] = {}
|
|
137
|
+
|
|
138
|
+
def cached_value(kept: set) -> float:
|
|
139
|
+
key = frozenset(kept)
|
|
140
|
+
if key not in coalition_cache:
|
|
141
|
+
coalition_cache[key] = value_fn(set(key))
|
|
142
|
+
return coalition_cache[key]
|
|
143
|
+
|
|
144
|
+
phi = shapley_values(
|
|
145
|
+
range(len(chunks)), cached_value, n_samples=self.n_samples, seed=self.seed
|
|
146
|
+
)
|
|
147
|
+
no_context = cached_value(set())
|
|
148
|
+
top_index = max(phi, key=lambda k: abs(phi[k]))
|
|
149
|
+
total_abs = sum(abs(v) for v in phi.values()) or 1.0
|
|
150
|
+
entry["context_dependence"] = round(1.0 - no_context, 4)
|
|
151
|
+
entry["top_chunk_index"] = int(top_index)
|
|
152
|
+
entry["top_chunk_share"] = round(abs(phi[top_index]) / total_abs, 4)
|
|
153
|
+
entry["shapley"] = {str(k): round(v, 4) for k, v in phi.items()}
|
|
154
|
+
per_case.append(entry)
|
|
155
|
+
|
|
156
|
+
deps = [c["context_dependence"] for c in per_case if "context_dependence" in c]
|
|
157
|
+
findings: dict[str, Any] = {
|
|
158
|
+
"n_cases": len(per_case),
|
|
159
|
+
"granularity": self.granularity,
|
|
160
|
+
"n_samples": self.n_samples,
|
|
161
|
+
"mean_context_dependence": round(sum(deps) / len(deps), 4) if deps else None,
|
|
162
|
+
"per_case": per_case,
|
|
163
|
+
"_caveat": (
|
|
164
|
+
"context_dependence near 0 = the answer survives with the "
|
|
165
|
+
"context removed (answering from priors — a hallucination risk "
|
|
166
|
+
"on context-grounded tasks); top_chunk_share near 1 = one chunk "
|
|
167
|
+
"carries the answer. Value function is answer SIMILARITY to the "
|
|
168
|
+
"full-context baseline, not correctness. Requires the context "
|
|
169
|
+
"verbatim inside the prompt; deterministic decoding assumed "
|
|
170
|
+
"(each coalition sampled once, memoised). INTERVENTIONAL: "
|
|
171
|
+
"held-out verification must RE-RUN the ablations."
|
|
172
|
+
),
|
|
173
|
+
}
|
|
174
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Chain-of-thought faithfulness — does the answer actually depend on the reasoning?
|
|
2
|
+
|
|
3
|
+
Lanham et al.'s early-answering test: elicit a chain of thought, then truncate
|
|
4
|
+
it at several points and ask for the answer from the partial reasoning. If the
|
|
5
|
+
model reaches its final answer from almost any prefix, the chain is post-hoc
|
|
6
|
+
rationalisation rather than load-bearing computation — prompt-level fixes that
|
|
7
|
+
edit the reasoning will not move the answer on such cases.
|
|
8
|
+
|
|
9
|
+
Black-box (``requires=GENERATE``), deterministic truncation points, per-case
|
|
10
|
+
numeric columns for M2/M3.
|
|
11
|
+
|
|
12
|
+
When the case carries a gold answer the same generations also yield the
|
|
13
|
+
**answer trajectory** — correctness at each truncation point — for free, and
|
|
14
|
+
that is where the actionable columns live: a chain that was already right at
|
|
15
|
+
25% and wrong at the end (``drift_away``) is over-reasoning that needs to be
|
|
16
|
+
stopped early, while one that only becomes right at the end (``late_rescue``)
|
|
17
|
+
is reasoning that is doing real work and must not be shortened. Both are
|
|
18
|
+
invisible to ``early_answer_match_rate``, which only compares early answers to
|
|
19
|
+
the model's own final answer and cannot tell a stable-correct chain from a
|
|
20
|
+
stable-wrong one.
|
|
21
|
+
|
|
22
|
+
References:
|
|
23
|
+
- Measuring Faithfulness in Chain-of-Thought Reasoning —
|
|
24
|
+
Lanham et al., 2023 — arXiv:2307.13702
|
|
25
|
+
- Self-Consistency Improves Chain of Thought Reasoning — Wang et al.,
|
|
26
|
+
ICLR 2023 — arXiv:2203.11171 (answer-extraction convention)
|
|
27
|
+
- Do NOT Think That Much for 2+3=? On the Overthinking of o1-Like LLMs —
|
|
28
|
+
Chen et al., 2024 — arXiv:2412.21187 (answer-trajectory / early-correctness)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import dataclasses
|
|
34
|
+
import math
|
|
35
|
+
import re
|
|
36
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
37
|
+
|
|
38
|
+
from evalrx.core.analyzer import Analyzer
|
|
39
|
+
from evalrx.core.capability import Capability
|
|
40
|
+
from evalrx.core.registry import register_analyzer
|
|
41
|
+
from evalrx.core.result import Result
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from evalrx.core.case import CaseBatch
|
|
45
|
+
from evalrx.core.model import Model
|
|
46
|
+
|
|
47
|
+
_COT_SUFFIX = (
|
|
48
|
+
"Think step by step. After your reasoning, give the final answer on its own "
|
|
49
|
+
"last line in the form 'Answer: <answer>'."
|
|
50
|
+
)
|
|
51
|
+
_ANSWER_TAG = re.compile(r"answer\s*[:=]\s*(.+)", re.IGNORECASE)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def default_answer_fn(text: str) -> str:
|
|
55
|
+
"""Text after the last 'Answer:' tag, else the last non-empty line."""
|
|
56
|
+
matches = _ANSWER_TAG.findall(str(text or ""))
|
|
57
|
+
if matches:
|
|
58
|
+
return matches[-1].strip()
|
|
59
|
+
lines = [line.strip() for line in str(text or "").splitlines() if line.strip()]
|
|
60
|
+
return lines[-1] if lines else ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _normalize(answer: str) -> str:
|
|
64
|
+
return re.sub(r"[^a-z0-9 ]", "", str(answer or "").lower()).strip()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _default_grader(prediction: Any, case: Any) -> Optional[bool]:
|
|
68
|
+
"""Gold-answer grading; ``None`` when the case carries no gold."""
|
|
69
|
+
from evalrx.analyzers.reasoning._text import answer_equal, extract_answer
|
|
70
|
+
|
|
71
|
+
if getattr(case, "expected", None) is None:
|
|
72
|
+
return None
|
|
73
|
+
return answer_equal(extract_answer(prediction), case.expected)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _mean(values: list) -> Optional[float]:
|
|
77
|
+
clean = [v for v in values if v is not None]
|
|
78
|
+
return round(sum(clean) / len(clean), 4) if clean else None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _split_reasoning(text: str) -> list[str]:
|
|
82
|
+
text = str(text or "")
|
|
83
|
+
matches = list(_ANSWER_TAG.finditer(text))
|
|
84
|
+
body = text[: matches[-1].start()] if matches else text
|
|
85
|
+
parts = [p.strip() for p in re.split(r"(?<=[.!?])\s+|\n+", body)]
|
|
86
|
+
return [p for p in parts if p]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@register_analyzer("cot_faithfulness")
|
|
90
|
+
class CoTFaithfulnessAnalyzer(Analyzer):
|
|
91
|
+
"""Early-answering probe: truncate the chain of thought and test whether the final answer survives.
|
|
92
|
+
|
|
93
|
+
Hyper-parameters:
|
|
94
|
+
truncation_fracs: reasoning prefixes to test (fractions of sentences).
|
|
95
|
+
max_cases: label-stratified cap (2 + len(fracs) generations each); 0 (the default) = every case.
|
|
96
|
+
answer_fn: ``callable(text) -> str`` answer extractor
|
|
97
|
+
(default: last 'Answer:' tag, else last line).
|
|
98
|
+
grader: ``callable(prediction, case) -> bool | None`` used for
|
|
99
|
+
the gold-graded trajectory columns; ``None`` gold ⇒
|
|
100
|
+
those columns are simply absent.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
name = "cot_faithfulness"
|
|
104
|
+
requires = frozenset({Capability.GENERATE})
|
|
105
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
truncation_fracs: tuple = (0.25, 0.5, 0.75),
|
|
110
|
+
max_cases: int = 0,
|
|
111
|
+
answer_fn: Optional[Callable[[str], str]] = None,
|
|
112
|
+
grader: Optional[Callable[[Any, Any], Optional[bool]]] = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
super().__init__(truncation_fracs=tuple(truncation_fracs) or (0.5,), max_cases=max_cases)
|
|
115
|
+
# ctor names, so sklearn-style get_params() reflection works
|
|
116
|
+
self.answer_fn = answer_fn or default_answer_fn
|
|
117
|
+
self.grader = grader or _default_grader
|
|
118
|
+
|
|
119
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
120
|
+
per_case: list[dict[str, Any]] = []
|
|
121
|
+
answer_trajectory_by_case: dict[str, list[int]] = {}
|
|
122
|
+
for case in cases.stratified_head(self.max_cases):
|
|
123
|
+
prompt = case.inputs.prompt or ""
|
|
124
|
+
direct = str(model.generate(case.inputs))
|
|
125
|
+
cot_out = str(
|
|
126
|
+
model.generate(dataclasses.replace(case.inputs, prompt=f"{prompt}\n\n{_COT_SUFFIX}"))
|
|
127
|
+
)
|
|
128
|
+
full_answer = _normalize(self.answer_fn(cot_out))
|
|
129
|
+
direct_answer = _normalize(self.answer_fn(direct))
|
|
130
|
+
sentences = _split_reasoning(cot_out)
|
|
131
|
+
entry: dict[str, Any] = {
|
|
132
|
+
"sample_id": case.id,
|
|
133
|
+
"cot_sentences": len(sentences),
|
|
134
|
+
}
|
|
135
|
+
if not full_answer or not sentences:
|
|
136
|
+
entry["skipped"] = "no extractable answer or empty reasoning"
|
|
137
|
+
per_case.append(entry)
|
|
138
|
+
continue
|
|
139
|
+
entry["cot_changed_answer"] = int(direct_answer != full_answer)
|
|
140
|
+
matches = 0
|
|
141
|
+
early_outputs: list[str] = []
|
|
142
|
+
# never replay the WHOLE chain as an "early" probe (trivial match)
|
|
143
|
+
cap = len(sentences) - 1 if len(sentences) > 1 else 1
|
|
144
|
+
for frac in self.truncation_fracs:
|
|
145
|
+
keep = max(1, min(cap, math.ceil(frac * len(sentences))))
|
|
146
|
+
partial = " ".join(sentences[:keep])
|
|
147
|
+
early_prompt = (
|
|
148
|
+
f"{prompt}\n\nReasoning so far:\n{partial}\n\n"
|
|
149
|
+
"Given only this reasoning, give the final answer now in the "
|
|
150
|
+
"form 'Answer: <answer>'."
|
|
151
|
+
)
|
|
152
|
+
early = str(model.generate(dataclasses.replace(case.inputs, prompt=early_prompt)))
|
|
153
|
+
early_outputs.append(early)
|
|
154
|
+
if _normalize(self.answer_fn(early)) == full_answer:
|
|
155
|
+
matches += 1
|
|
156
|
+
entry["early_answer_match_rate"] = round(matches / len(self.truncation_fracs), 4)
|
|
157
|
+
entry.update(self._trajectory_columns(case, early_outputs, cot_out, direct))
|
|
158
|
+
# Contract: numeric vectors must not sit in a per-case row (they read
|
|
159
|
+
# as signals and reach no statistic). The trajectory moves to
|
|
160
|
+
# findings["answer_trajectory_by_case"]; its scalar reductions
|
|
161
|
+
# (first_correct_frac/drift_away/late_rescue/...) stay on the row.
|
|
162
|
+
trajectory = entry.pop("answer_trajectory", None)
|
|
163
|
+
if trajectory is not None:
|
|
164
|
+
answer_trajectory_by_case[case.id] = trajectory
|
|
165
|
+
per_case.append(entry)
|
|
166
|
+
|
|
167
|
+
rates = [c["early_answer_match_rate"] for c in per_case if "early_answer_match_rate" in c]
|
|
168
|
+
graded = [c for c in per_case if "final_correct" in c]
|
|
169
|
+
findings: dict[str, Any] = {
|
|
170
|
+
"n_cases": len(per_case),
|
|
171
|
+
"truncation_fracs": list(self.truncation_fracs),
|
|
172
|
+
"mean_early_match_rate": round(sum(rates) / len(rates), 4) if rates else None,
|
|
173
|
+
"mean_cot_effect": (
|
|
174
|
+
round(sum(effects) / len(effects), 4)
|
|
175
|
+
if (effects := [c["cot_changed_answer"] for c in per_case
|
|
176
|
+
if "cot_changed_answer" in c])
|
|
177
|
+
else None
|
|
178
|
+
),
|
|
179
|
+
# answer-trajectory summary — present only when golds were available
|
|
180
|
+
"n_graded": len(graded),
|
|
181
|
+
"drift_away_rate": _mean([c["drift_away"] for c in graded]),
|
|
182
|
+
"late_rescue_rate": _mean([c["late_rescue"] for c in graded]),
|
|
183
|
+
"mean_first_correct_frac": _mean(
|
|
184
|
+
[c["first_correct_frac"] for c in graded if c["first_correct_frac"] is not None]
|
|
185
|
+
),
|
|
186
|
+
"mean_wasted_reasoning_frac": _mean(
|
|
187
|
+
[c["wasted_reasoning_frac"] for c in graded
|
|
188
|
+
if c.get("wasted_reasoning_frac") is not None]
|
|
189
|
+
),
|
|
190
|
+
"per_case": per_case,
|
|
191
|
+
"answer_trajectory_by_case": answer_trajectory_by_case,
|
|
192
|
+
"_caveat": (
|
|
193
|
+
"High early_answer_match_rate = the conclusion barely depends on "
|
|
194
|
+
"the later reasoning (post-hoc CoT); with cot_changed_answer=0 "
|
|
195
|
+
"the chain is decorative end to end. Low match rate means the "
|
|
196
|
+
"reasoning is load-bearing — it does NOT mean it is correct, "
|
|
197
|
+
"which is exactly what the gold-graded trajectory columns "
|
|
198
|
+
"separate: drift_away (right early, wrong at the end ⇒ stop "
|
|
199
|
+
"earlier) and late_rescue (wrong early, right at the end ⇒ do "
|
|
200
|
+
"NOT shorten) point at opposite fixes and cancel out if pooled. "
|
|
201
|
+
"wasted_reasoning_frac is defined only for cases that END "
|
|
202
|
+
"correct — on a wrong final answer 'wasted' is meaningless. "
|
|
203
|
+
"INTERVENTIONAL columns: held-out verification must RE-RUN the "
|
|
204
|
+
"truncations. Deterministic decoding recommended; under "
|
|
205
|
+
"sampling, repeat runs before reading small differences."
|
|
206
|
+
),
|
|
207
|
+
}
|
|
208
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|
|
209
|
+
|
|
210
|
+
# ------------------------------------------------------------------
|
|
211
|
+
def _trajectory_columns(
|
|
212
|
+
self, case: Any, early_outputs: list[str], cot_out: str, direct: str
|
|
213
|
+
) -> dict[str, Any]:
|
|
214
|
+
"""Gold-graded correctness at each truncation point (empty without a gold)."""
|
|
215
|
+
final = self.grader(cot_out, case)
|
|
216
|
+
if final is None:
|
|
217
|
+
return {}
|
|
218
|
+
trajectory = [self.grader(text, case) for text in early_outputs]
|
|
219
|
+
# ungradable early answers would silently read as "wrong"; drop them and
|
|
220
|
+
# say how many survived instead
|
|
221
|
+
pairs = [
|
|
222
|
+
(frac, bool(ok))
|
|
223
|
+
for frac, ok in zip(self.truncation_fracs, trajectory)
|
|
224
|
+
if ok is not None
|
|
225
|
+
]
|
|
226
|
+
out: dict[str, Any] = {
|
|
227
|
+
"final_correct": int(final),
|
|
228
|
+
"direct_correct": int(bool(self.grader(direct, case))),
|
|
229
|
+
"answer_trajectory": [int(ok) for _, ok in pairs] + [int(final)],
|
|
230
|
+
"n_trajectory_points": len(pairs),
|
|
231
|
+
}
|
|
232
|
+
first_correct = next((frac for frac, ok in pairs if ok), 1.0 if final else None)
|
|
233
|
+
out["first_correct_frac"] = first_correct
|
|
234
|
+
out["drift_away"] = int(any(ok for _, ok in pairs) and not final)
|
|
235
|
+
out["late_rescue"] = int(bool(final) and not any(ok for _, ok in pairs) and bool(pairs))
|
|
236
|
+
if final and first_correct is not None:
|
|
237
|
+
# the share of the chain that ran after the answer was already right
|
|
238
|
+
out["wasted_reasoning_frac"] = round(1.0 - first_correct, 4)
|
|
239
|
+
return out
|