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,225 @@
|
|
|
1
|
+
"""Tool-ablation Shapley — which tools does the outcome actually depend on?
|
|
2
|
+
|
|
3
|
+
AgentSHAP (TokenSHAP, arXiv:2407.10114) attributes an agent's response to its
|
|
4
|
+
TOOLS by re-running with tool subsets and Shapley-averaging the marginal
|
|
5
|
+
contributions. This port changes the value function for diagnosis: the
|
|
6
|
+
primary signal is the **outcome** (did the run pass, via an injected grader),
|
|
7
|
+
not answer similarity — "the wording changed" is not "it failed". Answer
|
|
8
|
+
similarity to the all-tools baseline is kept as a secondary value.
|
|
9
|
+
|
|
10
|
+
Sits beside :class:`CounterfactualReplay` in the intervention family: that one
|
|
11
|
+
perturbs a *step inside* the trajectory, this one perturbs the *tool
|
|
12
|
+
configuration* — attribution to steps vs attribution to the agent's setup
|
|
13
|
+
(which is exactly the L2 fix surface: tool subset selection).
|
|
14
|
+
|
|
15
|
+
Cost: with the small tool kits diagnosis uses (N<=4), all 2^N subsets are
|
|
16
|
+
enumerated and the Shapley values are EXACT; beyond that, leave-one-out plus
|
|
17
|
+
sampled subsets give a Monte-Carlo estimate (reported in the caveat).
|
|
18
|
+
|
|
19
|
+
The runner is INJECTED::
|
|
20
|
+
|
|
21
|
+
def run_with_tools(case, tool_names):
|
|
22
|
+
tools = [by_name[n](case) for n in tool_names] # rebind per case
|
|
23
|
+
return Agent(handle, tools, system=SYS).run(case)
|
|
24
|
+
|
|
25
|
+
probe = ToolShap(run_with_tools=run_with_tools,
|
|
26
|
+
tool_names=["image_zoom_in", "image_detect"])
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import difflib
|
|
32
|
+
import itertools
|
|
33
|
+
import math
|
|
34
|
+
import random
|
|
35
|
+
import statistics
|
|
36
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
37
|
+
|
|
38
|
+
from evalrx.analyzers.agent.reliability import default_grader
|
|
39
|
+
from evalrx.core.analyzer import Analyzer
|
|
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, FailureCase, Trajectory
|
|
45
|
+
from evalrx.core.model import Model
|
|
46
|
+
|
|
47
|
+
_FULL_ENUM_MAX_TOOLS = 4 # 2^4 = 16 runs/case — exact Shapley up to here
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _answer_similarity(a: str, b: str) -> float:
|
|
51
|
+
"""Dependency-free answer similarity (difflib ratio on normalized text)."""
|
|
52
|
+
na = " ".join(str(a or "").lower().split())
|
|
53
|
+
nb = " ".join(str(b or "").lower().split())
|
|
54
|
+
if not na and not nb:
|
|
55
|
+
return 1.0
|
|
56
|
+
return difflib.SequenceMatcher(None, na, nb).ratio()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _exact_shapley(values: "dict[frozenset, float]", names: list[str]) -> dict[str, float]:
|
|
60
|
+
"""Exact Shapley over a fully enumerated subset->value table."""
|
|
61
|
+
n = len(names)
|
|
62
|
+
phi: dict[str, float] = {}
|
|
63
|
+
for tool in names:
|
|
64
|
+
others = [t for t in names if t != tool]
|
|
65
|
+
total = 0.0
|
|
66
|
+
for r in range(len(others) + 1):
|
|
67
|
+
weight = math.factorial(r) * math.factorial(n - r - 1) / math.factorial(n)
|
|
68
|
+
for combo in itertools.combinations(others, r):
|
|
69
|
+
s = frozenset(combo)
|
|
70
|
+
total += weight * (values[s | {tool}] - values[s])
|
|
71
|
+
phi[tool] = total
|
|
72
|
+
return phi
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _sampled_shapley(values: "dict[frozenset, float]", names: list[str]) -> dict[str, float]:
|
|
76
|
+
"""Monte-Carlo estimate: mean marginal over the (S, S+tool) pairs we ran."""
|
|
77
|
+
phi: dict[str, float] = {}
|
|
78
|
+
for tool in names:
|
|
79
|
+
margins = [
|
|
80
|
+
values[s | {tool}] - values[s]
|
|
81
|
+
for s in values
|
|
82
|
+
if tool not in s and (s | {tool}) in values
|
|
83
|
+
]
|
|
84
|
+
phi[tool] = statistics.fmean(margins) if margins else 0.0
|
|
85
|
+
return phi
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@register_analyzer("tool_shap")
|
|
89
|
+
class ToolShap(Analyzer):
|
|
90
|
+
"""Shapley attribution of the agent's outcome (and answer) to its tools.
|
|
91
|
+
|
|
92
|
+
Hyper-parameters:
|
|
93
|
+
run_with_tools: ``callable(case, tuple[tool_names]) -> Trajectory`` —
|
|
94
|
+
REQUIRED. Must honor the subset (only those tools
|
|
95
|
+
available) and stay deterministic per subset.
|
|
96
|
+
tool_names: the full tool kit being attributed.
|
|
97
|
+
grader: ``callable(trajectory, case) -> bool | None``
|
|
98
|
+
(default :func:`default_grader`); grades each subset
|
|
99
|
+
run for the outcome value function.
|
|
100
|
+
max_combinations: cap on non-essential subset runs per case; beyond
|
|
101
|
+
full enumeration the estimate is Monte-Carlo.
|
|
102
|
+
max_cases: label-balanced cap on probed cases.
|
|
103
|
+
seed: sampling seed for the Monte-Carlo path.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
name = "tool_shap"
|
|
107
|
+
requires = frozenset() # the injected runner encapsulates the model
|
|
108
|
+
#: Reads agent runs. The model is not what makes this applicable —
|
|
109
|
+
#: the DATA is, which is why `requires` stays empty (these run on
|
|
110
|
+
#: trajectories loaded from disk, with no model at all).
|
|
111
|
+
requires_trajectories = True
|
|
112
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
113
|
+
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
run_with_tools: Callable[["FailureCase", tuple], "Trajectory"],
|
|
117
|
+
tool_names: list[str],
|
|
118
|
+
grader: Optional[Callable[["Trajectory", "FailureCase"], Optional[bool]]] = None,
|
|
119
|
+
max_combinations: Optional[int] = None,
|
|
120
|
+
max_cases: Optional[int] = None,
|
|
121
|
+
seed: int = 0,
|
|
122
|
+
) -> None:
|
|
123
|
+
super().__init__(
|
|
124
|
+
tool_names=list(tool_names),
|
|
125
|
+
max_combinations=max_combinations,
|
|
126
|
+
max_cases=max_cases,
|
|
127
|
+
seed=seed,
|
|
128
|
+
)
|
|
129
|
+
if not tool_names:
|
|
130
|
+
raise ValueError("tool_shap needs at least one tool name")
|
|
131
|
+
# stored under the ctor names so sklearn-style get_params() reflection works
|
|
132
|
+
self.run_with_tools = run_with_tools
|
|
133
|
+
self.grader = grader or default_grader
|
|
134
|
+
|
|
135
|
+
# -- subset schedule ------------------------------------------------
|
|
136
|
+
def _subsets(self) -> "tuple[list[frozenset], bool]":
|
|
137
|
+
"""All subsets to run (full set included) and whether enumeration is exact."""
|
|
138
|
+
names = self.tool_names
|
|
139
|
+
n = len(names)
|
|
140
|
+
full = [frozenset(c) for r in range(n + 1) for c in itertools.combinations(names, r)]
|
|
141
|
+
if n <= _FULL_ENUM_MAX_TOOLS and (
|
|
142
|
+
self.max_combinations is None or len(full) <= self.max_combinations
|
|
143
|
+
):
|
|
144
|
+
return full, True
|
|
145
|
+
essential = [frozenset(names)] # baseline
|
|
146
|
+
essential.append(frozenset()) # no tools at all
|
|
147
|
+
essential += [frozenset(names) - {t} for t in names] # leave-one-out
|
|
148
|
+
rest = [s for s in full if s not in essential]
|
|
149
|
+
budget = max(0, (self.max_combinations or len(full)) - len(essential))
|
|
150
|
+
rng = random.Random(self.seed)
|
|
151
|
+
return essential + rng.sample(rest, min(budget, len(rest))), False
|
|
152
|
+
|
|
153
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
154
|
+
selected = cases.stratified_head(self.max_cases) if self.max_cases else list(cases)
|
|
155
|
+
subsets, exact = self._subsets()
|
|
156
|
+
names = self.tool_names
|
|
157
|
+
per_case: list[dict[str, Any]] = []
|
|
158
|
+
|
|
159
|
+
for case in selected:
|
|
160
|
+
answers: dict[frozenset, str] = {}
|
|
161
|
+
outcome_v: dict[frozenset, float] = {}
|
|
162
|
+
sim_v: dict[frozenset, float] = {}
|
|
163
|
+
runs_audit: list[dict] = []
|
|
164
|
+
gradable = True
|
|
165
|
+
for subset in subsets:
|
|
166
|
+
traj = self.run_with_tools(case, tuple(sorted(subset)))
|
|
167
|
+
answer = str(traj.final_answer or "")
|
|
168
|
+
answers[subset] = answer
|
|
169
|
+
grade = self.grader(traj, case)
|
|
170
|
+
if grade is None:
|
|
171
|
+
gradable = False
|
|
172
|
+
outcome_v[subset] = 1.0 if grade else 0.0
|
|
173
|
+
runs_audit.append(
|
|
174
|
+
{
|
|
175
|
+
"tools": sorted(subset),
|
|
176
|
+
"passed": grade,
|
|
177
|
+
"n_tool_calls": int(traj.metrics.get("n_tool_calls", 0)),
|
|
178
|
+
"terminated": str(traj.metrics.get("terminated", "")),
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
baseline = answers[frozenset(names)]
|
|
182
|
+
for subset, answer in answers.items():
|
|
183
|
+
sim_v[subset] = _answer_similarity(answer, baseline)
|
|
184
|
+
|
|
185
|
+
shap = _exact_shapley if exact else _sampled_shapley
|
|
186
|
+
entry: dict[str, Any] = {
|
|
187
|
+
"sample_id": case.id,
|
|
188
|
+
"n_subset_runs": len(subsets),
|
|
189
|
+
"baseline_pass": (
|
|
190
|
+
int(outcome_v[frozenset(names)]) if gradable else None
|
|
191
|
+
),
|
|
192
|
+
"no_tools_pass": int(outcome_v[frozenset()]) if gradable else None,
|
|
193
|
+
"runs": runs_audit,
|
|
194
|
+
}
|
|
195
|
+
if gradable:
|
|
196
|
+
entry["tools_needed"] = int(
|
|
197
|
+
outcome_v[frozenset(names)] > outcome_v[frozenset()]
|
|
198
|
+
)
|
|
199
|
+
for tool, phi in shap(outcome_v, names).items():
|
|
200
|
+
entry[f"shap_outcome_{tool}"] = round(phi, 4)
|
|
201
|
+
for tool, phi in shap(sim_v, names).items():
|
|
202
|
+
entry[f"shap_answer_{tool}"] = round(phi, 4)
|
|
203
|
+
per_case.append(entry)
|
|
204
|
+
|
|
205
|
+
findings: dict[str, Any] = {
|
|
206
|
+
"n_trajectories": len(per_case),
|
|
207
|
+
"tool_names": names,
|
|
208
|
+
"exact": exact,
|
|
209
|
+
"runs_per_case": len(subsets),
|
|
210
|
+
"per_case": per_case,
|
|
211
|
+
"_caveat": (
|
|
212
|
+
("Exact Shapley (all 2^N subsets enumerated). " if exact else
|
|
213
|
+
"Monte-Carlo Shapley over sampled subsets — treat values as estimates. ")
|
|
214
|
+
+ "shap_outcome_* attributes PASSING to each tool (needs a gradable "
|
|
215
|
+
"expected answer); shap_answer_* attributes the produced answer "
|
|
216
|
+
"(similarity to the all-tools baseline) and is defined even without "
|
|
217
|
+
"labels. Deterministic runners are assumed: each subset is run once. "
|
|
218
|
+
"These are INTERVENTIONAL columns: held-out verification must "
|
|
219
|
+
"RE-RUN the subset ablation on the held-out cases — never reuse "
|
|
220
|
+
"exploration-set values. baseline_pass mechanically tracks the "
|
|
221
|
+
"case label when labels come from the same runner config; treat "
|
|
222
|
+
"it as sanity, not as a candidate signal."
|
|
223
|
+
),
|
|
224
|
+
}
|
|
225
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Trajectory rubric judge — failure-mode code + per-dimension rubric scores.
|
|
2
|
+
|
|
3
|
+
Where :class:`FirstErrorJudge` localises WHERE the failure entered, this judge
|
|
4
|
+
classifies WHAT KIND of failure it was (a compact MAST-inspired taxonomy for
|
|
5
|
+
single-agent tool loops) and scores the run on five 0-2 rubric dimensions.
|
|
6
|
+
The mode is a categorical M2 column; the rubric scores are numeric ones.
|
|
7
|
+
|
|
8
|
+
Taxonomy (single-agent visual tool loops):
|
|
9
|
+
FM-TOOL-SELECT needed tool not called / wrong tool for the need
|
|
10
|
+
FM-TOOL-ARGS malformed or misaimed arguments (wrong region, bad query)
|
|
11
|
+
FM-IGNORE-OBS answer ignores or contradicts tool evidence
|
|
12
|
+
FM-LOOP repeats actions without progress
|
|
13
|
+
FM-PERCEPTION misreads the visual evidence it did gather
|
|
14
|
+
FM-REASONING right evidence, wrong inference
|
|
15
|
+
FM-ANSWER correct reasoning, malformed/incomplete final answer
|
|
16
|
+
FM-NONE no failure visible in the trajectory
|
|
17
|
+
|
|
18
|
+
References:
|
|
19
|
+
- MAST failure-mode taxonomy — Cemri et al., 2025 — arXiv:2503.13657
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
27
|
+
|
|
28
|
+
from evalrx.analyzers.agent.first_error_judge import _render
|
|
29
|
+
from evalrx.core.analyzer import Analyzer
|
|
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
|
|
35
|
+
from evalrx.core.model import Model
|
|
36
|
+
|
|
37
|
+
FAILURE_MODES = (
|
|
38
|
+
"FM-TOOL-SELECT",
|
|
39
|
+
"FM-TOOL-ARGS",
|
|
40
|
+
"FM-IGNORE-OBS",
|
|
41
|
+
"FM-LOOP",
|
|
42
|
+
"FM-PERCEPTION",
|
|
43
|
+
"FM-REASONING",
|
|
44
|
+
"FM-ANSWER",
|
|
45
|
+
"FM-NONE",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
RUBRIC_DIMS = ("grounding", "tool_choice", "tool_args", "evidence_use", "answer_quality")
|
|
49
|
+
|
|
50
|
+
_PROMPT = """You are auditing one AI agent trajectory (a vision-language model calling tools).
|
|
51
|
+
Goal: {goal}
|
|
52
|
+
Expected answer (may be empty): {expected}
|
|
53
|
+
Outcome label (may be unknown): {label}
|
|
54
|
+
|
|
55
|
+
Steps:
|
|
56
|
+
{steps}
|
|
57
|
+
|
|
58
|
+
Classify the PRIMARY failure mode, one of:
|
|
59
|
+
FM-TOOL-SELECT (needed tool not called / wrong tool), FM-TOOL-ARGS (bad or misaimed arguments),
|
|
60
|
+
FM-IGNORE-OBS (answer ignores tool evidence), FM-LOOP (repeats without progress),
|
|
61
|
+
FM-PERCEPTION (misreads visual evidence), FM-REASONING (right evidence, wrong inference),
|
|
62
|
+
FM-ANSWER (malformed/incomplete final answer), FM-NONE (no failure visible).
|
|
63
|
+
|
|
64
|
+
Then score each rubric dimension 0 (clearly bad), 1 (mixed), or 2 (clearly good):
|
|
65
|
+
grounding, tool_choice, tool_args, evidence_use, answer_quality.
|
|
66
|
+
|
|
67
|
+
Reply with ONLY a JSON object:
|
|
68
|
+
{{"failure_mode": "...", "rubric": {{"grounding": 0, "tool_choice": 0, "tool_args": 0, "evidence_use": 0, "answer_quality": 0}}, "first_error_step": <index or -1>, "rationale": "<one sentence>"}}"""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _parse(raw: str) -> Optional[dict]:
|
|
72
|
+
cleaned = re.sub(r"<think>.*?</think>", "", raw or "", flags=re.DOTALL)
|
|
73
|
+
match = re.search(r"\{.*\}", cleaned, flags=re.DOTALL)
|
|
74
|
+
if not match:
|
|
75
|
+
return None
|
|
76
|
+
try:
|
|
77
|
+
return json.loads(match.group())
|
|
78
|
+
except json.JSONDecodeError:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@register_analyzer("trajectory_rubric")
|
|
83
|
+
class TrajectoryRubricJudge(Analyzer):
|
|
84
|
+
"""LLM-judged failure-mode classification + rubric scoring per trajectory.
|
|
85
|
+
|
|
86
|
+
Hyper-parameters:
|
|
87
|
+
judge: any object with ``generate(prompt) -> str`` — REQUIRED.
|
|
88
|
+
max_cases: label-balanced cap on judged cases (one call per case).
|
|
89
|
+
annotate: when True (default), a valid ``first_error_step`` stamps
|
|
90
|
+
``failure_mode`` onto that trajectory step in place.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
name = "trajectory_rubric"
|
|
94
|
+
requires = frozenset() # the judge is injected, not the probed model
|
|
95
|
+
#: Reads agent runs. The model is not what makes this applicable —
|
|
96
|
+
#: the DATA is, which is why `requires` stays empty (these run on
|
|
97
|
+
#: trajectories loaded from disk, with no model at all).
|
|
98
|
+
requires_trajectories = True
|
|
99
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
100
|
+
|
|
101
|
+
def __init__(self, judge: Any, max_cases: Optional[int] = None, annotate: bool = True) -> None:
|
|
102
|
+
super().__init__(max_cases=max_cases, annotate=annotate)
|
|
103
|
+
if judge is None or not hasattr(judge, "generate"):
|
|
104
|
+
raise ValueError("trajectory_rubric needs a judge with .generate(prompt) -> str")
|
|
105
|
+
self.judge = judge # ctor name, so sklearn-style get_params() reflection works
|
|
106
|
+
|
|
107
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
108
|
+
selected = cases.stratified_head(self.max_cases) if self.max_cases else list(cases)
|
|
109
|
+
per_case: list[dict[str, Any]] = []
|
|
110
|
+
mode_counts: dict[str, int] = {}
|
|
111
|
+
|
|
112
|
+
for case in selected:
|
|
113
|
+
traj = case.trajectory
|
|
114
|
+
if traj is None:
|
|
115
|
+
continue
|
|
116
|
+
prompt = _PROMPT.format(
|
|
117
|
+
goal=traj.goal,
|
|
118
|
+
expected=str(case.expected or ""),
|
|
119
|
+
label=case.label.value,
|
|
120
|
+
steps=_render(traj),
|
|
121
|
+
)
|
|
122
|
+
try:
|
|
123
|
+
raw = str(self.judge.generate(prompt))
|
|
124
|
+
except Exception as exc: # judge outage is a finding, not a crash
|
|
125
|
+
per_case.append({"sample_id": traj.sample_id, "judge_ok": 0, "judge_error": repr(exc)})
|
|
126
|
+
continue
|
|
127
|
+
payload = _parse(raw)
|
|
128
|
+
if not payload:
|
|
129
|
+
per_case.append(
|
|
130
|
+
{"sample_id": traj.sample_id, "judge_ok": 0, "judge_raw": raw[:300]}
|
|
131
|
+
)
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
mode = str(payload.get("failure_mode", "")).upper()
|
|
135
|
+
if mode not in FAILURE_MODES:
|
|
136
|
+
mode = "FM-NONE" if case.label.value == "pass" else "FM-REASONING"
|
|
137
|
+
entry: dict[str, Any] = {
|
|
138
|
+
"sample_id": traj.sample_id,
|
|
139
|
+
"judge_ok": 1,
|
|
140
|
+
"failure_mode": mode,
|
|
141
|
+
"rationale": str(payload.get("rationale", ""))[:300],
|
|
142
|
+
}
|
|
143
|
+
rubric = payload.get("rubric") or {}
|
|
144
|
+
for dim in RUBRIC_DIMS:
|
|
145
|
+
val = rubric.get(dim)
|
|
146
|
+
if isinstance(val, (int, float)):
|
|
147
|
+
entry[f"rubric_{dim}"] = max(0, min(2, int(val)))
|
|
148
|
+
step_idx = payload.get("first_error_step")
|
|
149
|
+
if isinstance(step_idx, int) and 0 <= step_idx < len(traj.steps):
|
|
150
|
+
entry["first_error_step"] = step_idx
|
|
151
|
+
if self.annotate:
|
|
152
|
+
traj.steps[step_idx].failure_mode = mode
|
|
153
|
+
mode_counts[mode] = mode_counts.get(mode, 0) + 1
|
|
154
|
+
per_case.append(entry)
|
|
155
|
+
|
|
156
|
+
findings: dict[str, Any] = {
|
|
157
|
+
"n_trajectories": len(per_case),
|
|
158
|
+
"n_judged": sum(c.get("judge_ok", 0) for c in per_case),
|
|
159
|
+
"judge": repr(self.judge),
|
|
160
|
+
"mode_counts": mode_counts,
|
|
161
|
+
"per_case": per_case,
|
|
162
|
+
"_caveat": (
|
|
163
|
+
"Judge-derived labels — treat as hypotheses to verify, not ground "
|
|
164
|
+
"truth. failure_mode is categorical (records-path column); "
|
|
165
|
+
"rubric_* are 0-2 numerics."
|
|
166
|
+
),
|
|
167
|
+
}
|
|
168
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Attention analyzers (require the ATTENTION capability → white-box / HF eager).
|
|
2
|
+
|
|
3
|
+
NOTE: these import torch at module load; on the light (pure-API) install the whole
|
|
4
|
+
subpackage is skipped by ``analyzers/__init__`` (you can't run them without torch).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from evalrx.analyzers.attention.relative_attn import RelativeAttentionAnalyzer
|
|
8
|
+
from evalrx.analyzers.attention.rollout import AttentionRolloutAnalyzer, RolloutResult
|
|
9
|
+
from evalrx.analyzers.attention.sink import AttentionSinkAnalyzer
|
|
10
|
+
from evalrx.analyzers.attention.summary import AttentionAnalyzer, AttentionResult
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AttentionAnalyzer",
|
|
14
|
+
"AttentionResult",
|
|
15
|
+
"AttentionRolloutAnalyzer",
|
|
16
|
+
"RolloutResult",
|
|
17
|
+
"AttentionSinkAnalyzer",
|
|
18
|
+
"RelativeAttentionAnalyzer",
|
|
19
|
+
]
|