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,25 @@
|
|
|
1
|
+
"""Agent failure analyzers — operate on a FailureCase's Trajectory.
|
|
2
|
+
|
|
3
|
+
Build order (cheapest/most reliable first): deterministic heuristics
|
|
4
|
+
(loop_detect, ignored_obs) → LLM-judge (first_error_judge, trajectory_rubric)
|
|
5
|
+
→ intervention probes that RE-RUN the agent (counterfactual = perturb a step,
|
|
6
|
+
reliability_probe = pass@k over k reps, tool_shap = Shapley over tool subsets).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from evalrx.analyzers.agent.counterfactual import CounterfactualReplay
|
|
10
|
+
from evalrx.analyzers.agent.first_error_judge import FirstErrorJudge
|
|
11
|
+
from evalrx.analyzers.agent.ignored_obs import IgnoredObservationDetector
|
|
12
|
+
from evalrx.analyzers.agent.loop_detect import LoopDetector
|
|
13
|
+
from evalrx.analyzers.agent.reliability import ReliabilityProbe
|
|
14
|
+
from evalrx.analyzers.agent.tool_shap import ToolShap
|
|
15
|
+
from evalrx.analyzers.agent.trajectory_rubric import TrajectoryRubricJudge
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"LoopDetector",
|
|
19
|
+
"IgnoredObservationDetector",
|
|
20
|
+
"FirstErrorJudge",
|
|
21
|
+
"TrajectoryRubricJudge",
|
|
22
|
+
"CounterfactualReplay",
|
|
23
|
+
"ReliabilityProbe",
|
|
24
|
+
"ToolShap",
|
|
25
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Counterfactual replay — causal step attribution for agent trajectories.
|
|
2
|
+
|
|
3
|
+
For each decision step, re-run the trajectory forked at that step and see whether
|
|
4
|
+
the outcome flips: a high flip-rate ⇒ the step is causally influential. The actual
|
|
5
|
+
re-run is INJECTED as ``rerun_fn(trajectory, step_idx, seed) -> bool`` (it wraps a
|
|
6
|
+
live Agent + verifier), keeping this analyzer decoupled and testable.
|
|
7
|
+
|
|
8
|
+
Caveat (built into the design): with temperature > 0, run ``n_replays`` per step
|
|
9
|
+
and read the flip-RATE — a single replay conflates "this step was causal" with
|
|
10
|
+
model noise. Prefer temperature=0 replay or a larger ``n_replays``.
|
|
11
|
+
|
|
12
|
+
Causal framework:
|
|
13
|
+
"Causality: Models, Reasoning and Inference"
|
|
14
|
+
Pearl (2000/2009), Cambridge University Press — do-calculus, interventions
|
|
15
|
+
|
|
16
|
+
Applied to neural NLP (causal mediation / interchange intervention):
|
|
17
|
+
"Causal Mediation Analysis for Interpreting Neural NLP: The Case of Gender Bias"
|
|
18
|
+
Vig et al., 2020 — https://arxiv.org/abs/2004.12265
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import TYPE_CHECKING, Callable
|
|
24
|
+
|
|
25
|
+
from evalrx.core.analyzer import Analyzer
|
|
26
|
+
from evalrx.core.capability import Capability
|
|
27
|
+
from evalrx.core.case import Label
|
|
28
|
+
from evalrx.core.registry import register_analyzer
|
|
29
|
+
from evalrx.core.result import Result
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from evalrx.core.case import CaseBatch
|
|
33
|
+
from evalrx.core.model import Model
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@register_analyzer("counterfactual")
|
|
37
|
+
class CounterfactualReplay(Analyzer):
|
|
38
|
+
"""Rank a trajectory's tool-call steps by how often re-running flips the outcome."""
|
|
39
|
+
|
|
40
|
+
name = "counterfactual"
|
|
41
|
+
requires = frozenset({Capability.TOOL_CALLS})
|
|
42
|
+
#: TOOL_CALLS says the model CAN call tools; this says the cases ARE agent
|
|
43
|
+
#: runs. A chat model whose template renders tools declares the former.
|
|
44
|
+
requires_trajectories = True
|
|
45
|
+
applies_to_modalities = frozenset({"text", "image"}) # trajectory analysis is modality-agnostic
|
|
46
|
+
|
|
47
|
+
def __init__(self, rerun_fn: Callable, n_replays: int = 3) -> None:
|
|
48
|
+
super().__init__(rerun_fn=rerun_fn, n_replays=n_replays)
|
|
49
|
+
self.rerun_fn = rerun_fn
|
|
50
|
+
|
|
51
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
52
|
+
per_case = []
|
|
53
|
+
for case in cases:
|
|
54
|
+
traj = case.trajectory
|
|
55
|
+
if traj is None:
|
|
56
|
+
continue
|
|
57
|
+
original_success = traj.outcome == Label.PASS
|
|
58
|
+
candidates = [s for s in traj.steps if s.tool_call]
|
|
59
|
+
step_scores = []
|
|
60
|
+
for s in candidates:
|
|
61
|
+
flips = sum(
|
|
62
|
+
1 for seed in range(self.n_replays)
|
|
63
|
+
if bool(self.rerun_fn(traj, s.idx, seed)) != original_success
|
|
64
|
+
)
|
|
65
|
+
step_scores.append({
|
|
66
|
+
"step": s.idx,
|
|
67
|
+
"action": s.tool_call.get("name"),
|
|
68
|
+
"flip_rate": round(flips / self.n_replays, 4) if self.n_replays else 0.0,
|
|
69
|
+
})
|
|
70
|
+
step_scores.sort(key=lambda d: -d["flip_rate"])
|
|
71
|
+
per_case.append({
|
|
72
|
+
"sample_id": traj.sample_id,
|
|
73
|
+
"original_success": original_success,
|
|
74
|
+
"most_influential_step": step_scores[0] if step_scores else None,
|
|
75
|
+
"steps": step_scores,
|
|
76
|
+
})
|
|
77
|
+
return Result(
|
|
78
|
+
analyzer=self.name, model=repr(model), cases=cases,
|
|
79
|
+
findings={
|
|
80
|
+
"n_trajectories": len(per_case),
|
|
81
|
+
"per_case": per_case,
|
|
82
|
+
"_caveat": "flip-rate ≈ causal influence; use temperature=0 or larger n_replays to separate from noise.",
|
|
83
|
+
},
|
|
84
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""First-error attribution — LLM-as-judge (the Who&When 'all-at-once' protocol).
|
|
2
|
+
|
|
3
|
+
Given a trajectory, ask a JUDGE model which step first introduced the error.
|
|
4
|
+
The judge is a separate handle (judge != system-under-test, to avoid
|
|
5
|
+
self-preference bias). Writes ``is_first_error`` onto the implicated step.
|
|
6
|
+
|
|
7
|
+
Reliability caveats (bake into any reporting): LLM judges show position/length
|
|
8
|
+
bias and self-preference; calibrate against a human-gold subset and report
|
|
9
|
+
step-accuracy/F1. As reported by Zhang et al. (Who&When), even strong judges
|
|
10
|
+
reach only ~14% step-level accuracy — treat outputs as weak signal, not truth.
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
- Which Agent Causes Task Failures and When? Automated Failure Attribution (Who&When)
|
|
14
|
+
Zhang et al., ICML 2025 — arXiv:2505.00212
|
|
15
|
+
- Failure-mode vocabulary — MAST, Cemri et al., 2025 — arXiv:2503.13657
|
|
16
|
+
- LLM-as-judge biases — Judging LLM-as-a-Judge, Zheng et al., NeurIPS 2023 — arXiv:2306.05685
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from typing import TYPE_CHECKING, Optional
|
|
23
|
+
|
|
24
|
+
from evalrx.core.analyzer import Analyzer
|
|
25
|
+
from evalrx.core.registry import register_analyzer
|
|
26
|
+
from evalrx.core.result import Result
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from evalrx.core.case import CaseBatch, Trajectory
|
|
30
|
+
from evalrx.core.model import Model
|
|
31
|
+
|
|
32
|
+
_PROMPT = """You are debugging an AI agent trajectory. The agent failed to achieve the goal.
|
|
33
|
+
Goal: {goal}
|
|
34
|
+
|
|
35
|
+
Steps:
|
|
36
|
+
{steps}
|
|
37
|
+
|
|
38
|
+
Which step index FIRST introduced the error that led to failure?
|
|
39
|
+
Answer with exactly one line: "STEP: <index>" (use -1 if no step is at fault)."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _render(traj: "Trajectory") -> str:
|
|
43
|
+
lines = []
|
|
44
|
+
for s in traj.steps:
|
|
45
|
+
piece = f"[{s.idx}] {s.role.value}"
|
|
46
|
+
if s.tool_call:
|
|
47
|
+
piece += f" calls {s.tool_call.get('name')}({s.tool_call.get('args')})"
|
|
48
|
+
if s.observation is not None:
|
|
49
|
+
piece += f" -> obs: {str(s.observation)[:200]}"
|
|
50
|
+
if s.content:
|
|
51
|
+
piece += f" : {str(s.content)[:200]}"
|
|
52
|
+
lines.append(piece)
|
|
53
|
+
return "\n".join(lines)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@register_analyzer("first_error_judge")
|
|
57
|
+
class FirstErrorJudge(Analyzer):
|
|
58
|
+
"""Attribute the first-erroneous step via an LLM judge (all-at-once protocol)."""
|
|
59
|
+
|
|
60
|
+
name = "first_error_judge"
|
|
61
|
+
requires = frozenset() # the analysed model is irrelevant; the judge does the work
|
|
62
|
+
#: Reads agent runs. The model is not what makes this applicable —
|
|
63
|
+
#: the DATA is, which is why `requires` stays empty (these run on
|
|
64
|
+
#: trajectories loaded from disk, with no model at all).
|
|
65
|
+
requires_trajectories = True
|
|
66
|
+
applies_to_modalities = frozenset({"text", "image"}) # trajectory analysis is modality-agnostic
|
|
67
|
+
|
|
68
|
+
def __init__(self, judge: Optional["Model"] = None) -> None:
|
|
69
|
+
super().__init__(judge=judge)
|
|
70
|
+
|
|
71
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
72
|
+
judge = self.judge if self.judge is not None else model
|
|
73
|
+
if judge is None or not hasattr(judge, "generate"):
|
|
74
|
+
raise ValueError("FirstErrorJudge needs a judge model with generate(); pass judge=...")
|
|
75
|
+
per_case = []
|
|
76
|
+
for case in cases:
|
|
77
|
+
traj = case.trajectory
|
|
78
|
+
if traj is None:
|
|
79
|
+
continue
|
|
80
|
+
raw = judge.generate(_PROMPT.format(goal=traj.goal, steps=_render(traj)))
|
|
81
|
+
m = re.search(r"STEP:\s*(-?\d+)", str(raw))
|
|
82
|
+
idx = int(m.group(1)) if m else -1
|
|
83
|
+
if 0 <= idx < len(traj.steps):
|
|
84
|
+
traj.steps[idx].is_first_error = True
|
|
85
|
+
per_case.append({"sample_id": traj.sample_id, "first_error_step": idx, "judge_raw": str(raw)[:200]})
|
|
86
|
+
return Result(
|
|
87
|
+
analyzer=self.name,
|
|
88
|
+
model=repr(model),
|
|
89
|
+
cases=cases,
|
|
90
|
+
findings={
|
|
91
|
+
"n_trajectories": len(per_case),
|
|
92
|
+
"judge": repr(judge),
|
|
93
|
+
"per_case": per_case,
|
|
94
|
+
"_caveat": "LLM-judge: position/length/self-preference bias; calibrate vs human gold (Who&When).",
|
|
95
|
+
},
|
|
96
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Ignored-observation detection — deterministic trajectory heuristic.
|
|
2
|
+
|
|
3
|
+
Flags the pattern: a tool returns an error/empty signal, yet the next action
|
|
4
|
+
repeats the *same* tool call unchanged (the agent didn't adapt to the
|
|
5
|
+
observation). MAST "ignored tool output" / failure-to-recover mode.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- Failure-mode taxonomy — Why Do Multi-Agent LLM Systems Fail? (MAST)
|
|
9
|
+
Cemri et al., 2025 — arXiv:2503.13657 (ignored-tool-output / fail-to-recover modes)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from evalrx.core.analyzer import Analyzer
|
|
18
|
+
from evalrx.core.case import StepRole
|
|
19
|
+
from evalrx.core.registry import register_analyzer
|
|
20
|
+
from evalrx.core.result import Result
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from evalrx.core.case import CaseBatch
|
|
24
|
+
from evalrx.core.model import Model
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@register_analyzer("ignored_obs")
|
|
28
|
+
class IgnoredObservationDetector(Analyzer):
|
|
29
|
+
"""Detect error observations that the agent then ignored (repeated the same call)."""
|
|
30
|
+
|
|
31
|
+
name = "ignored_obs"
|
|
32
|
+
requires = frozenset()
|
|
33
|
+
#: Reads agent runs. The model is not what makes this applicable —
|
|
34
|
+
#: the DATA is, which is why `requires` stays empty (these run on
|
|
35
|
+
#: trajectories loaded from disk, with no model at all).
|
|
36
|
+
requires_trajectories = True
|
|
37
|
+
applies_to_modalities = frozenset({"text", "image"}) # trajectory analysis is modality-agnostic
|
|
38
|
+
|
|
39
|
+
DEFAULT_MARKERS = ("error", "not found", "failed", "no result", "none", "invalid", "empty", "exception")
|
|
40
|
+
|
|
41
|
+
def __init__(self, markers: tuple[str, ...] | None = None) -> None:
|
|
42
|
+
super().__init__(markers=markers or self.DEFAULT_MARKERS)
|
|
43
|
+
|
|
44
|
+
def _is_error(self, observation) -> bool:
|
|
45
|
+
text = str(observation).lower()
|
|
46
|
+
return any(m in text for m in self.markers)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def _sig(tool_call: dict | None) -> str | None:
|
|
50
|
+
if not tool_call:
|
|
51
|
+
return None
|
|
52
|
+
return json.dumps([tool_call.get("name"), tool_call.get("args", {})], sort_keys=True, default=str)
|
|
53
|
+
|
|
54
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
55
|
+
per_case = []
|
|
56
|
+
for case in cases:
|
|
57
|
+
traj = case.trajectory
|
|
58
|
+
if traj is None:
|
|
59
|
+
continue
|
|
60
|
+
steps = traj.steps
|
|
61
|
+
flags = []
|
|
62
|
+
for i, s in enumerate(steps):
|
|
63
|
+
if s.role is not StepRole.TOOL or s.observation is None or not self._is_error(s.observation):
|
|
64
|
+
continue
|
|
65
|
+
# the actor that produced this observation is the previous step;
|
|
66
|
+
# the next actor that repeats the same call ignored the error.
|
|
67
|
+
prev_sig = self._sig(steps[i - 1].tool_call) if i > 0 else None
|
|
68
|
+
nxt = next((x for x in steps[i + 1:] if x.role is StepRole.ACTOR), None)
|
|
69
|
+
if nxt is not None and prev_sig is not None and self._sig(nxt.tool_call) == prev_sig:
|
|
70
|
+
flags.append({"error_obs_step": s.idx, "repeated_at_step": nxt.idx})
|
|
71
|
+
per_case.append({"sample_id": traj.sample_id, "n_ignored": len(flags), "ignored": flags})
|
|
72
|
+
return Result(
|
|
73
|
+
analyzer=self.name,
|
|
74
|
+
model=repr(model),
|
|
75
|
+
cases=cases,
|
|
76
|
+
findings={
|
|
77
|
+
"n_trajectories": len(per_case),
|
|
78
|
+
"n_with_ignored_obs": sum(1 for c in per_case if c["n_ignored"]),
|
|
79
|
+
"per_case": per_case,
|
|
80
|
+
},
|
|
81
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Loop detection — a deterministic trajectory heuristic (no model, no LLM).
|
|
2
|
+
|
|
3
|
+
Flags repeated ``(tool, normalised args)`` actions in an agent trajectory — the
|
|
4
|
+
MAST "step repetition" failure mode. Cheap, deterministic, and the highest-ROI
|
|
5
|
+
agent signal to compute first.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- Failure-mode taxonomy — Why Do Multi-Agent LLM Systems Fail? (MAST)
|
|
9
|
+
Cemri et al., 2025 — arXiv:2503.13657 (step-repetition mode)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from collections import Counter
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
from evalrx.core.analyzer import Analyzer
|
|
19
|
+
from evalrx.core.registry import register_analyzer
|
|
20
|
+
from evalrx.core.result import Result
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from evalrx.core.case import CaseBatch
|
|
24
|
+
from evalrx.core.model import Model
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _sig(tool_call: dict) -> tuple[str, str]:
|
|
28
|
+
return (tool_call.get("name", ""), json.dumps(tool_call.get("args", {}), sort_keys=True, default=str))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@register_analyzer("loop_detect")
|
|
32
|
+
class LoopDetector(Analyzer):
|
|
33
|
+
"""Detect repeated tool actions in a trajectory.
|
|
34
|
+
|
|
35
|
+
Hyper-parameters:
|
|
36
|
+
min_repeats: how many times an identical action must occur to count as a loop.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
name = "loop_detect"
|
|
40
|
+
requires = frozenset() # pure heuristic over Trajectory — no model needed
|
|
41
|
+
#: Reads agent runs. The model is not what makes this applicable —
|
|
42
|
+
#: the DATA is, which is why `requires` stays empty (these run on
|
|
43
|
+
#: trajectories loaded from disk, with no model at all).
|
|
44
|
+
requires_trajectories = True
|
|
45
|
+
applies_to_modalities = frozenset({"text", "image"}) # trajectory analysis is modality-agnostic
|
|
46
|
+
|
|
47
|
+
def __init__(self, min_repeats: int = 2) -> None:
|
|
48
|
+
super().__init__(min_repeats=min_repeats)
|
|
49
|
+
|
|
50
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
51
|
+
per_case = []
|
|
52
|
+
for case in cases:
|
|
53
|
+
traj = case.trajectory
|
|
54
|
+
if traj is None:
|
|
55
|
+
continue
|
|
56
|
+
sigs = [_sig(s.tool_call) for s in traj.steps if s.tool_call]
|
|
57
|
+
counts = Counter(sigs)
|
|
58
|
+
loops = [
|
|
59
|
+
{"action": name, "args": args, "count": n}
|
|
60
|
+
for (name, args), n in counts.items()
|
|
61
|
+
if n >= self.min_repeats
|
|
62
|
+
]
|
|
63
|
+
consecutive = any(sigs[i] == sigs[i + 1] for i in range(len(sigs) - 1))
|
|
64
|
+
per_case.append({
|
|
65
|
+
"sample_id": traj.sample_id,
|
|
66
|
+
"has_loop": bool(loops),
|
|
67
|
+
"consecutive_repeat": consecutive,
|
|
68
|
+
"loops": loops,
|
|
69
|
+
})
|
|
70
|
+
return Result(
|
|
71
|
+
analyzer=self.name,
|
|
72
|
+
model=repr(model),
|
|
73
|
+
cases=cases,
|
|
74
|
+
findings={
|
|
75
|
+
"n_trajectories": len(per_case),
|
|
76
|
+
"n_with_loops": sum(c["has_loop"] for c in per_case),
|
|
77
|
+
"per_case": per_case,
|
|
78
|
+
},
|
|
79
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Reliability probe — pass@k / pass^k over repeated agent runs.
|
|
2
|
+
|
|
3
|
+
A single run hides the capability/stability split: "reliably fails" and
|
|
4
|
+
"flaky" are different diagnoses with different fixes (prompt/tool surgery vs
|
|
5
|
+
decoding/consistency scaffolds). This probe re-runs each case *k* times
|
|
6
|
+
through an injected runner and reports, per case, the pass@k family plus
|
|
7
|
+
answer- and trajectory-consistency measures.
|
|
8
|
+
|
|
9
|
+
Reference: Pass@k (capability: at least one of k passes) vs Pass^k
|
|
10
|
+
(reliability: all k pass) as popularised by agent-eval harnesses.
|
|
11
|
+
|
|
12
|
+
The runner is INJECTED — the analyzer never builds an agent itself::
|
|
13
|
+
|
|
14
|
+
def runs_fn(case, k):
|
|
15
|
+
# e.g. a temperature>0 endpoint, or per-rep seeds; use run_batch for concurrency
|
|
16
|
+
return [Agent(handle, tools_factory(case), system=SYS).run(case) for _ in range(k)]
|
|
17
|
+
|
|
18
|
+
probe = ReliabilityProbe(runs_fn=runs_fn, k=5)
|
|
19
|
+
result = probe.run(model, cases)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
import statistics
|
|
26
|
+
from collections import Counter
|
|
27
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
28
|
+
|
|
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, FailureCase, Trajectory
|
|
35
|
+
from evalrx.core.model import Model
|
|
36
|
+
|
|
37
|
+
_YESNO = re.compile(r"\b(yes|no)\b")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def default_grader(trajectory: "Trajectory", case: "FailureCase") -> Optional[bool]:
|
|
41
|
+
"""Grade one run against ``case.expected`` (``None`` = ungradable).
|
|
42
|
+
|
|
43
|
+
yes/no expectations match the FIRST yes/no token in the answer; other
|
|
44
|
+
expectations match as a case-insensitive substring. Inject a task-specific
|
|
45
|
+
grader for anything richer.
|
|
46
|
+
"""
|
|
47
|
+
expected = case.expected
|
|
48
|
+
if expected is None or trajectory.final_answer is None:
|
|
49
|
+
return None
|
|
50
|
+
answer = str(trajectory.final_answer).strip().lower()
|
|
51
|
+
exp = str(expected).strip().lower()
|
|
52
|
+
if exp in ("yes", "no"):
|
|
53
|
+
m = _YESNO.search(answer)
|
|
54
|
+
return bool(m and m.group(1) == exp)
|
|
55
|
+
return exp in answer
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _normalized_answer(trajectory: "Trajectory") -> str:
|
|
59
|
+
return " ".join(str(trajectory.final_answer or "").lower().split())[:200]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _tool_sequence(trajectory: "Trajectory") -> tuple:
|
|
63
|
+
return tuple(s.tool_call.get("name", "") for s in trajectory.steps if s.tool_call)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@register_analyzer("reliability_probe")
|
|
67
|
+
class ReliabilityProbe(Analyzer):
|
|
68
|
+
"""Re-run each case k times and measure pass@k / pass^k / consistency.
|
|
69
|
+
|
|
70
|
+
Hyper-parameters:
|
|
71
|
+
runs_fn: ``callable(case, k) -> list[Trajectory]`` — REQUIRED. The
|
|
72
|
+
caller owns sampling variation (temperature/seeds) and
|
|
73
|
+
concurrency; deterministic runners make every rep identical
|
|
74
|
+
and the probe degenerates (it will say so in the caveat).
|
|
75
|
+
k: repetitions per case.
|
|
76
|
+
grader: ``callable(trajectory, case) -> bool | None``; defaults to
|
|
77
|
+
:func:`default_grader`.
|
|
78
|
+
max_cases: label-balanced cap on probed cases (k runs each is costly).
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
name = "reliability_probe"
|
|
82
|
+
requires = frozenset() # the injected runner encapsulates the model
|
|
83
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
runs_fn: Callable[["FailureCase", int], "list[Trajectory]"],
|
|
88
|
+
k: int = 5,
|
|
89
|
+
grader: Optional[Callable[["Trajectory", "FailureCase"], Optional[bool]]] = None,
|
|
90
|
+
max_cases: Optional[int] = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
super().__init__(k=k, max_cases=max_cases)
|
|
93
|
+
# stored under the ctor names so sklearn-style get_params() reflection works
|
|
94
|
+
self.runs_fn = runs_fn
|
|
95
|
+
self.grader = grader or default_grader
|
|
96
|
+
|
|
97
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
98
|
+
selected = cases.stratified_head(self.max_cases) if self.max_cases else list(cases)
|
|
99
|
+
per_case: list[dict[str, Any]] = []
|
|
100
|
+
for case in selected:
|
|
101
|
+
trajectories = list(self.runs_fn(case, self.k) or [])
|
|
102
|
+
if not trajectories:
|
|
103
|
+
continue
|
|
104
|
+
n_runs = len(trajectories)
|
|
105
|
+
grades = [self.grader(t, case) for t in trajectories]
|
|
106
|
+
graded = [g for g in grades if g is not None]
|
|
107
|
+
|
|
108
|
+
answers = Counter(_normalized_answer(t) for t in trajectories)
|
|
109
|
+
agreement = answers.most_common(1)[0][1] / n_runs
|
|
110
|
+
sequences = Counter(_tool_sequence(t) for t in trajectories)
|
|
111
|
+
calls = [
|
|
112
|
+
int(t.metrics.get("n_tool_calls", sum(1 for s in t.steps if s.tool_call)))
|
|
113
|
+
for t in trajectories
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
entry: dict[str, Any] = {
|
|
117
|
+
"sample_id": case.id,
|
|
118
|
+
"n_runs": n_runs,
|
|
119
|
+
"answer_agreement": round(agreement, 4),
|
|
120
|
+
"answer_consistent": 1 if len(answers) == 1 else 0,
|
|
121
|
+
"tool_seq_diversity": round(len(sequences) / n_runs, 4),
|
|
122
|
+
"n_tool_calls_mean": round(statistics.fmean(calls), 3) if calls else 0.0,
|
|
123
|
+
"n_tool_calls_std": round(statistics.pstdev(calls), 3) if len(calls) > 1 else 0.0,
|
|
124
|
+
}
|
|
125
|
+
if graded:
|
|
126
|
+
n_pass = sum(graded)
|
|
127
|
+
entry.update(
|
|
128
|
+
{
|
|
129
|
+
"n_graded": len(graded),
|
|
130
|
+
"n_pass": n_pass,
|
|
131
|
+
"success_rate": round(n_pass / len(graded), 4),
|
|
132
|
+
"pass_at_k": 1 if n_pass > 0 else 0,
|
|
133
|
+
"pass_all_k": 1 if n_pass == len(graded) else 0,
|
|
134
|
+
"flaky": 1 if 0 < n_pass < len(graded) else 0,
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
per_case.append(entry)
|
|
138
|
+
|
|
139
|
+
rates = [c["success_rate"] for c in per_case if "success_rate" in c]
|
|
140
|
+
all_consistent = per_case and all(c["answer_consistent"] == 1 for c in per_case)
|
|
141
|
+
findings: dict[str, Any] = {
|
|
142
|
+
"n_trajectories": len(per_case),
|
|
143
|
+
"k": self.k,
|
|
144
|
+
"n_graded_cases": len(rates),
|
|
145
|
+
"mean_success_rate": round(statistics.fmean(rates), 4) if rates else None,
|
|
146
|
+
"frac_flaky": (
|
|
147
|
+
round(sum(c.get("flaky", 0) for c in per_case) / len(rates), 4) if rates else None
|
|
148
|
+
),
|
|
149
|
+
"per_case": per_case,
|
|
150
|
+
"_caveat": (
|
|
151
|
+
"Ungraded cases (no expected answer) report consistency only. "
|
|
152
|
+
+ (
|
|
153
|
+
"Every case produced k identical answers — the runner looks "
|
|
154
|
+
"deterministic; use temperature>0 or per-rep seeds for a "
|
|
155
|
+
"meaningful reliability read. "
|
|
156
|
+
if all_consistent
|
|
157
|
+
else ""
|
|
158
|
+
)
|
|
159
|
+
+ "pass@k measures capability, pass^k reliability; the gap is the "
|
|
160
|
+
"instability mass. These are INTERVENTIONAL columns: held-out "
|
|
161
|
+
"verification must RE-RUN the k repetitions on the held-out "
|
|
162
|
+
"cases — never reuse exploration-set values."
|
|
163
|
+
),
|
|
164
|
+
}
|
|
165
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|