Quickstart
This page shows the common ways to run EvalRX.
Bring Your Own Model
If you already have a loaded Hugging Face causal LM, wrap it — no registry key
needed. The wrapped model is the same object evalrx.load(...) returns, so
every capability-compatible analyzer works on it.
import evalrx
from transformers import AutoModelForCausalLM, AutoTokenizer
from evalrx.analyzers.lens.logit_lens import LogitLensAnalyzer
model = AutoModelForCausalLM.from_pretrained("my-org/my-llama")
tokenizer = AutoTokenizer.from_pretrained("my-org/my-llama")
wrapped = evalrx.wrap(model, tokenizer)
result = LogitLensAnalyzer().run(wrapped, "The capital of France is")
print(result.summary())
Capabilities (attention, hidden states, logits, …) are inferred from the live
model. Attention capture needs eager attention; wrap switches the model to it
when it can, otherwise load with attn_implementation="eager". White-box capture
currently supports text decoder-only models (VLM capture is Stage 2).
One-Liner Model Load
import evalrx
from evalrx.analyzers.attention.summary import AttentionAnalyzer
model = evalrx.load("qwen2.5-7b-instruct")
result = AttentionAnalyzer(layer=-1, top_k=5).run(
model,
"The Eiffel Tower is in",
)
print(result.summary())
print(result.findings)
By default, evalrx.load uses the hf_local backend unless the spec is
API-only.
Config-Driven Run
from evalrx import load_config, run
config = load_config("configs/qwen_attention.yaml")
result = run(config, "The Eiffel Tower is in")
Explicit Backend Selection
Use compose when you want to control the runtime and negotiate capabilities
before model weights load.
from evalrx import Capability
from evalrx.models import compose
model = compose(
"qwen2.5-7b-instruct",
"hf_local",
want={Capability.ATTENTION},
)
If the selected backend cannot provide the requested capability, EvalRX
raises a CapabilityError before constructing the model.
Discovery
import evalrx
print(evalrx.list_specs())
print(evalrx.registry.analyzers.list())
print(evalrx.registry.analyzers.names_compatible_with(model))
This is the same discovery surface intended for an automated evaluation agent.
Agent — Tool-Calling Loop, Any Backend
Agent(wraps=handle) is backend-agnostic: it needs only GENERATE +
TOOL_CALLS (checked up front), never internals — so the same loop drives
an API model and a local model. The single thing that varies is the
ToolCallCodec (auto-selected): OpenAI-native structured calls for the API,
Hermes-style <tool_call>{…}</tool_call> text parsing for local templates.
Tool execution goes through a pluggable ToolExecutor (swap in your
APIToolHandler).
from evalrx import Agent, Tool, compose, RuntimeConfig
from evalrx.models.backends import call_vision_api_chat_fn
search = Tool(name="search", description="web search",
parameters={"type": "object", "properties": {"q": {"type": "string"}}},
fn=my_search)
# API agent (reuse your engine): native tool-calls, OpenAI codec
api = compose("qwen3-8b", "api", RuntimeConfig(chat_fn=call_vision_api_chat_fn(call_vision_api)))
traj = Agent(api, tools=[search]).run("who won the 2022 world cup?") # -> Trajectory
# Local agent: SAME Agent; tools rendered via the model's chat template, Qwen codec
local = compose("qwen3-8b", "hf_local") # TOOL_CALLS granted only if spec.tool_calling
traj = Agent(local, tools=[search]).run("...") # -> Trajectory (steps: USER→ACTOR→TOOL→…)
TOOL_CALLS is a conditional capability for local models: the backend
provides the channel, but it's granted only when the model's chat template
renders tools (spec.tool_calling, verified against the template at load).
So compose(non_tool_model, "hf_local", want={TOOL_CALLS}) fails up front.
Multimodal agents. The same loop runs vision cases end-to-end: give the
case an image and it enters the first user message; a tool that returns a
ToolResult with images gets them re-injected as a new message, so the
model can look at what its tool produced. evalrx.models.tools ships
subject-side visual tools (deterministic, model-agnostic schemas):
from evalrx import Capability, compose, RuntimeConfig
from evalrx.core.case import FailureCase, Inputs
from evalrx.models.agent import Agent
from evalrx.models.tools import zoom_in_tool
vlm = compose("qwen3-vl-2b-instruct", "hf_local",
runtime=RuntimeConfig(device="cuda:0"),
want={Capability.GENERATE, Capability.TOOL_CALLS})
agent = Agent(vlm, tools=[zoom_in_tool(image, save_dir="outputs/images")],
system="Zoom into the relevant region before answering.")
traj = agent.run(FailureCase(inputs=Inputs(prompt="What does the sign say?", image=image)))
json.dump(traj.to_dict(), open("trajectory.json", "w")) # serializable end-to-end
Runnable version: examples/agent_demos/visual_zoom_agent/run.py.
Scale path. For batch probing, serve the open checkpoint with vLLM and use the built-in OpenAI-compatible client — no dependency injection needed:
vllm serve Qwen/Qwen3-VL-2B-Instruct --port 8901 \
--enable-auto-tool-choice --tool-call-parser hermes
from evalrx.models.agent import run_batch
from evalrx.models.backends import openai_runtime
vlm = compose("qwen3-vl-2b-instruct", "api",
runtime=openai_runtime(base_url="http://localhost:8901/v1"))
trajs = run_batch(vlm, cases, tools_factory=lambda c: [zoom_in_tool(c.inputs.image)],
system=SYSTEM, concurrency=8) # threads; error-stub per failed case
Every turn records latency and token usage on the step's span; the
trajectory flattener (evalrx.analysis.trajectory_records) turns finished
runs into records.json rows that evalrx explore consumes directly.
Closed models: the same openai_runtime against an OpenAI endpoint, or
GeminiModel (native function calling) for Gemini.
Standalone M2 Explore
If you already have result logs and want M2 to analyze them without writing analysis code, run a single-shot exploration:
evalrx explore /path/to/results \
--backend antigravity \
-q "Which failure patterns distinguish wrong answers from correct ones?" \
--out evalrx_explore_output \
--serve-report # optional local browser server
The run writes the generated code, stdout/stderr, a structured exploratory
report (exploratory_report.json), and rendered charts under
evalrx_explore_output/figures/ + tables/.
Add --holdout-frac 0.4 --holdout-confirm for a held-out design: the split
is carved off BEFORE exploration (outcome-stratified, deterministic), the
explorer is told to freeze threshold-explicit recipes, and after M3 the
held-out rows re-test every recipe verbatim (e-BH, split_label="held_out")
while an LLM judge grades each hypothesis — confirm_report.json lands next
to the report and fills the Held-out Verdicts section.
Agent-trajectory records. Rows produced by
evalrx.analysis.trajectory_records flow through explore unchanged; use
trajectory_records.AGENT_QUESTION_TEMPLATE as the question (it declares the
column-family semantics — causal shap_outcome_*, stability success_rate,
judge-assigned failure_mode, cost columns — and steers hypotheses toward
the black-box fix surface). One held-out caveat is unique to agents: columns
from the interventional probes (reliability_probe, tool_shap) must be
re-run on the held-out cases (k repetitions / subset ablations there) —
reusing exploration-set values would test nothing. Budget for it: held-out
confirmation of these columns costs the same per case as exploration did.
Open the saved output as a local static report:
See Exploratory Analysis (M2/M3) for the full standalone explore + hypothesis-generation workflow.
Case study on a paper-method bench run
Point the same command at an examples/ bench run — a directory holding
outputs/<name>.json beside the data/ manifest the runner read — and the
dashboard opens a case book instead of the explore layout:
Three tabs: Run Overview (splits, hypothesis, where the baseline fails),
Case Study and Repair Methods. The Case Study tab plays each case's actual
stimulus — an <audio> player for MMAU clips, the image for the VLM slices —
next to the question and its options, so a human can answer the item before
anything is revealed (blind mode is on by default, and the outcome filter is
disabled while it is, so the filter cannot leak the answer). Unblinding shows
the correct answer, the model's baseline answer and, per repair candidate, what
that candidate did to this case: repaired / broke / no change. Answers you
lock in are scored against the model on the same cases and stay in the browser
session — nothing is written back to the report.
The Repair Methods tab is the other half of a case study: for every candidate in the sweep it shows the mechanism (TCD's blurred-waveform contrast, an L1 prompt template, an L2 self-refine scaffold …), its exact configuration or prompt text, its paired McNemar / e-value verdict, and a button per flipped case that jumps straight to that case in the Case Study tab.
Run a Codebase, Then Explore
If you don't have result logs yet — only an existing evaluation/inference
codebase — run_codebase bridges the gap: a CLI coding agent runs the
codebase for you inside an isolated copy (your original directory is never
modified), harvests the per-case results it produces, and hands them
straight to the same M2/M3 explore pipeline above.
evalrx run-codebase ./my_eval_repo \
--backend claude_code \
-q "Where does the model fail and why?" \
--out evalrx_run_codebase_output \
--dashboard # optional
Or from Python:
import evalrx
result = evalrx.run_codebase("./my_eval_repo", out="run_output")
print(result.ran_ok, len(result.records))
print(result.explore.report.observations)
Output contract: the run agent is instructed to write records.json (a
JSON array or JSON-Lines file, one row per evaluation case) in the codebase's
working copy, each row carrying a label (e.g. PASS/FAIL) plus the
case's input/prediction/target fields. If the codebase already writes
per-case outputs, the agent converts them into this format rather than
re-running everything. One repair turn is attempted if nothing usable was
produced (max_attempts, default 2) before giving up with ran_ok=False
and an error explaining why.
Whatever environment the codebase needs — API keys, GPU, installed
dependencies — is the user's responsibility; the agent runs with your local
environment and does not provision infrastructure. Pass analyze=False /
--no-explore to only run + harvest without the M2/M3 step.
Convenience Shim
Models expose call_<analysis> methods dynamically through the analyzer
registry:
This is convenience syntax for:
from evalrx.analyzers.attention.summary import AttentionAnalyzer
result = AttentionAnalyzer(layer=-1, top_k=5).run(model, data)
Prefer direct analyzer construction in reusable code because it makes parameters and dependencies explicit.
Hallucination Analysis (POPE + CHAIR)
Both analyzers are black-box (GENERATE-only) and work with any vision-capable
model, including API endpoints.
POPE — yes/no object-presence probes; reports accuracy, precision, recall, F1.
Each case needs metadata["pope_label"] = "yes" or "no".
Paper: Li et al., EMNLP 2023 — https://arxiv.org/abs/2305.10355
Code: https://github.com/AoiDragon/POPE
from evalrx.analyzers.hallucination.pope import POPEAnalyzer
from evalrx.core.case import CaseBatch, FailureCase, Inputs
cases = CaseBatch([
FailureCase(inputs=Inputs(prompt="Is there a cat? Answer yes or no.", image=img),
metadata={"pope_label": "yes"}),
FailureCase(inputs=Inputs(prompt="Is there a plane? Answer yes or no.", image=img),
metadata={"pope_label": "no"}),
])
result = POPEAnalyzer().run(model, cases)
# result.findings → {"accuracy": 0.83, "f1": 0.86, "yes_rate": 0.50, ...}
print(result.summary())
CHAIR — caption hallucination rate vs a fixed object vocabulary.
Each case needs metadata["gt_objects"] = list of gold object strings.
Paper: Rohrbach et al., EMNLP 2018 — https://arxiv.org/abs/1809.02156
from evalrx.analyzers.hallucination.chair import CHAIRAnalyzer
COCO_VOCAB = ["cat", "dog", "car", "chair", ...] # 80 COCO categories
cases = CaseBatch([
FailureCase(inputs=Inputs(prompt="Describe the image.", image=img),
metadata={"gt_objects": ["cat", "chair"]}),
])
result = CHAIRAnalyzer(object_vocab=COCO_VOCAB).run(model, cases)
# result.findings → {"chair_i": 0.25, "chair_s": 0.50, "n": 2}
# chair_i: mean per-caption hallucination rate; chair_s: fraction of captions with ≥1 hallucination
Full runnable example: examples/analyzer_demos/hallucination/ (launch with docker compose up).
Shapley Attribution — MM-SHAP + VL-SHAP
Both analyzers require LOGPROBS capability and use Monte-Carlo Shapley sampling.
MM-SHAP — decomposes model reliance between text tokens and the image.
mm_score near 1.0 ⇒ image-driven; near 0.0 ⇒ text-driven. Measures reliance,
not correctness.
Paper: Parcalabescu & Frank, ACL 2022 — https://arxiv.org/abs/2212.08158
Code: https://github.com/coastalcph/mm-shap
from evalrx.analyzers.perturbation.mm_shap import MMShapAnalyzer
from evalrx.core.case import CaseBatch, FailureCase, Inputs
case = FailureCase(inputs=Inputs(prompt="What color is the car?", image=img))
result = MMShapAnalyzer(n_samples=64, top_k=5).run(model, CaseBatch([case]))
# result.findings → {
# "mm_score": 0.62, # 0=text-reliant, 1=image-reliant
# "text_contribution": 0.38,
# "image_contribution": 0.62,
# "top_text_tokens": [{"token": "color", "shapley": 0.12}, ...]
# }
VL-SHAP — spatial Shapley attribution over a grid of image regions. Ranks which regions most influenced the model's output logprob.
Based on: Lundberg & Lee, NeurIPS 2017 — https://arxiv.org/abs/1705.07874
Applied via MM-SHAP framework (Parcalabescu & Frank, ACL 2022)
from evalrx.analyzers.perturbation.vl_shap import VLShapAnalyzer
result = VLShapAnalyzer(n_regions=16, n_samples=64, top_k=3).run(model, CaseBatch([case]))
# result.findings → {
# "grid_side": 4, # 4×4 grid
# "top_regions": [{"region": 5, "shapley": 0.31}, ...],
# "total_abs_attribution": 1.84
# }
Full runnable example: examples/analyzer_demos/mm_shap/ (launch with docker compose up).
Logprob Entropy (black-box uncertainty)
LogprobEntropyAnalyzer computes sequence perplexity and per-token predictive
entropy from output-token logprobs — works on any API model that returns
top_logprobs (e.g. OpenAI). No white-box access needed.
Predictive entropy: Gal & Ghahramani, ICML 2016 — https://arxiv.org/abs/1506.02142
LLM self-knowledge: Kadavath et al. 2022 — https://arxiv.org/abs/2207.05221
from evalrx.analyzers.uncertainty.logprob_entropy import LogprobEntropyAnalyzer
from evalrx.core.case import CaseBatch, FailureCase, Inputs
case = FailureCase(inputs=Inputs(prompt="The capital of France is"))
result = LogprobEntropyAnalyzer().run(model, CaseBatch([case]))
# result.findings → {
# "n_tokens": 3,
# "perplexity": 1.12, # low ⇒ model is confident
# "mean_logprob": -0.11,
# "mean_top_entropy": 0.08, # high ⇒ broad uncertainty at that step
# "min_token_logprob": -0.31
# }
Wire an OpenAI endpoint as the logprobs_fn:
from evalrx.models.backends.api import parse_openai_logprobs
from evalrx.models.backends.base import RuntimeConfig
def logprobs_fn(prompt, *, model="gpt-4o-mini", max_new_tokens=40, top_k=5, **_):
resp = client.chat.completions.create(
model=model, messages=[{"role": "user", "content": prompt}],
max_tokens=max_new_tokens, logprobs=True, top_logprobs=top_k,
)
return parse_openai_logprobs(resp.choices[0].logprobs.content)
rt = RuntimeConfig(generate_fn=..., logprobs_fn=logprobs_fn)
Full runnable example: examples/analyzer_demos/logprob_entropy/ (launch with docker compose up).
Statistical Comparison — stats.compare
compare is the single entry point for pairwise A/B comparison. It never
returns a bare p-value: it always gives effect size + clustered bootstrap CI +
anytime-valid e-value + corrected reject decision.
McNemar: McNemar (1947) https://doi.org/10.1007/BF02295996
E-values: Grünwald et al. (2022) https://arxiv.org/abs/1906.07801
e-BH FDR: Wang & Ramdas (2022) https://arxiv.org/abs/2009.02824
from evalrx.stats import compare
# success_a / success_b: list of bool (one per example, same order)
r = compare(success_a, success_b, paired=True, alpha=0.05,
min_effect=0.03, cluster_by=task_ids)
print(r.summary())
# [mcnemar + e-value] effect=+0.12 (B>A) CI=[+0.04, +0.20] e=18.4 → REJECT H0
print(r.effect) # float: mean(B) - mean(A)
print(r.ci) # (lo, hi) 95% clustered bootstrap CI
print(r.e_value) # float: anytime-valid evidence (reject when e ≥ 1/alpha)
print(r.reject) # bool: corrected decision
print(r.underpowered) # bool: True when CI width > 2 × min_effect
For 3+ strategies use compare_multiple (Friedman omnibus + Nemenyi post-hoc):
Friedman + Nemenyi: Demšar (2006) https://jmlr.org/papers/v7/demsarar06a.html
from evalrx.stats import compare_multiple
mr = compare_multiple({"A": success_a, "B": success_b, "C": success_c}, alpha=0.05)
print(mr.reject_global) # bool: at least one strategy differs
print(mr.avg_ranks) # {"A": 2.1, "B": 1.7, "C": 2.2}
print(mr.significant_pairs) # [("A", "B"), ...] — pairs that pass Nemenyi CD
VLDiagnoseLoop — Automated Failure Attribution (Current)
Two diagnosis loops are available. VLDiagnoseLoop is the current
architecture for VL and LLM tasks (AutoDiagnoseLoop, below, is kept for
backward compatibility):
M1 ProbeAgent protocol-guided analyzer selection + execute
M2 StatsAnalysisAgent stats tools + e-BH FDR correction + LLM evidence chain
M3 DiagnosisAgent "AI scientist" hypothesis generation
M4 HypothesisTester stats test + protocol consistency check
↑___________________________________|
stop when M4 finds a verified, protocol-consistent hypothesis
M5 (SurgeryAgent) runs after the loop via loop.run_m5() to propose
(Plan A) or execute (Plan B) a targeted fix for the best verified hypothesis.
See Architecture
for the stage contracts, M1's two-tier analyzer selection, and what M2/M4 ask.
from evalrx import compose
from evalrx.core.capability import Capability
from evalrx.eval_agent import VLDiagnoseLoop, AgyModel, RunLoggerV2
from evalrx.eval_agent.stages.protocol import ExperimentProtocol
from evalrx.eval_agent.stages.probe_agent import ProbeAgent
from evalrx.analysis.stats_agent import StatsAnalysisAgent
from evalrx.eval_agent.stages.diagnosis import DiagnosisAgent
protocol = ExperimentProtocol(
description="The VLM gives wrong left/right positions in spatial questions.",
task_domain="spatial reasoning",
success_criteria="Positions must match what is visible in the image.",
)
model = compose("qwen2.5-vl-7b-instruct", "hf_local",
want={Capability.GENERATE, Capability.ATTENTION})
judge = AgyModel() # or any Model with Capability.GENERATE
loop = VLDiagnoseLoop(
model=model,
probe_agent=ProbeAgent(max_analyzers=3),
stats_agent=StatsAnalysisAgent(judge=judge),
diagnosis_agent=DiagnosisAgent(judge=judge),
max_cycles=3,
protocol=protocol,
run_logger=RunLoggerV2(),
)
report = loop.run(failure_cases)
print(report.resolved) # True when M4 finds a supported, consistent hypothesis
print(report.final_hypotheses) # list[Hypothesis] — status SUPPORTED/REFUTED/INCONCLUSIVE
fix = loop.run_m5(report, failure_cases) # post-loop fix proposal
RunLoggerV2() above writes just its own run.json + M<n>/log.json files at
the given (or auto-generated) path — no other producer's output directory.
For a full managed output directory — figures, per-trial fix/experiment
sandboxes, contract export — construct a RunContext and pass
run_logger=ctx.logger instead. See
RunContext.
ExperimentProtocol is the human prior that anchors the loop. M1 uses it
to select analyzers relevant to the task; M4 uses it to reject hypotheses that
drift from what the user was investigating:
from evalrx.eval_agent.stages.protocol import ExperimentProtocol
protocol = ExperimentProtocol(
description="free text — what the experiment tests and what failure looks like",
task_domain="spatial reasoning", # short label
success_criteria="what counts as a pass",
failure_patterns="observations already noticed (optional)",
target_modalities=frozenset({"text", "image"}),
)
AgenticDiagnoseLoop — Judge-Decided M1-M4 (Alternative to the Fixed Cycle)
VLDiagnoseLoop above always runs M1→M2→M3→M4 in the same order every cycle.
AgenticDiagnoseLoop wraps the identical stages, confirm-split, and post-loop
run_m5/run_fix — but a CLI judge decides which tool to call next each
turn (probe, run stats, explore the raw data, cluster failure modes, search
for new failure cases via a Macro/Micro MCTS probe search, propose
hypotheses, test one, fix, or stop), instead of a fixed sequence. The host — not the judge —
enforces call limits, tool preconditions, and the stopping discipline: the
judge cannot declare success (stop(resolved=true)) until a hypothesis has
actually been tested and is statistically supported and protocol-consistent;
an early attempt is rejected and fed back to the judge.
from evalrx.eval_agent import AgenticDiagnoseLoop, ClaudeModel
loop = AgenticDiagnoseLoop(
model=model,
protocol=protocol,
judge=ClaudeModel(), # the decision judge — any CLI-backed Model
max_actions=12, # hard cap on judge decision turns
run_logger=RunLoggerV2(),
)
report = loop.run(failure_cases) # same VLDiagnoseReport shape as VLDiagnoseLoop
report.stopped_by is one of agent_stop / max_actions / budget /
time_budget / invalid_actions (three consecutive unparseable judge
responses, even after a repair prompt). Two event types — agent_decision
(the chosen tool + rationale) and agent_tool (the dispatch layer's
accept/reject outcome) — sit inside run.json, alongside the reused
probe/analysis/diagnosis/surgery events from the wrapped stages
(log_agent_decision/log_agent_tool).
Input Modes — Submitting a Diagnosis Run
There are two ways to trigger the diagnosis agent.
Mode 1 — Container Submission (Reproduce a Known Failure)
Write a run.py that defines your failure cases as FailureCase objects,
then package it into a Docker container. The agent runs the M1→M5 loop
inside the container and writes findings to outputs/.
- Define failure cases and run the loop in
run.py— see theVLDiagnoseLoopexample above; build aCaseBatchofFailureCaseobjects instead offailure_casesand callloop.run(cases). - Add a Dockerfile + docker-compose.yml mirroring one of the concrete
example directories under
examples/analyzer_demos/,examples/m2_m3/, orexamples/m1_m3//examples/m1_m5/. - Submit the container:
Outputs (logs, analyzer artifacts, hypotheses) are written to outputs/ in
the container, mounted to your local directory via the compose volume. See
examples/m1_m3/qwen_loop_agy/ and
examples/m1_m3/qwen_video_temporal/ for complete working examples.
Mode 2 — Natural-Language Description (Agent Writes the Container)
Describe the failure in plain English. The scaffold generator produces a
ready-to-run Docker experiment with a run.py, Dockerfile, and
docker-compose.yml. A CLI coding agent (Claude Code, Gemini CLI, …) can
write a fully customised run.py; otherwise a template is used as a
starting point.
from evalrx.eval_agent import scaffold_from_description
out = scaffold_from_description(
description="My VLM frequently confuses left and right when answering "
"spatial relationship questions about images.",
model_key="qwen2.5-vl-7b-instruct",
output_dir="./my_experiment",
provider="claude_code", # optional: generates a bespoke run.py; omit for a template
cli_model="sonnet",
)
# cd my_experiment && docker compose up
Or via CLI:
python -m evalrx.eval_agent.nl_runner \
--description "My VLM confuses left and right in spatial questions" \
--model qwen2.5-vl-7b-instruct \
--out ./my_experiment \
--provider claude_code --cli-model sonnet # omit --provider for template mode
cd my_experiment && docker compose up
The generated scaffold contains run.py (diagnosis script with
ExperimentProtocol pre-filled from your description), Dockerfile,
docker-compose.yml (mounts HF cache, GPU, outputs, and agy binary), and
.gitignore. In template mode, edit the CASES list in run.py to add your
own failure examples before running docker compose up.
AutoDiagnoseLoop — Legacy M1→M5 Pipeline
AutoDiagnoseLoop closes the analysis→diagnosis→intervention cycle automatically.
It needs a judge model (any instruction-following model with GENERATE) and
the model under evaluation.
from evalrx import Capability
from evalrx.eval_agent import AutoDiagnoseLoop, DiagnosisAgent
from evalrx.models import compose
from evalrx.models.backends.base import RuntimeConfig
# 1. The model under evaluation (local VLM with white-box access)
model = compose("qwen3-vl-8b-instruct", "hf_local", want={Capability.ATTENTION})
# 2. A capable judge (API model — needs only GENERATE)
judge = compose("qwen3-8b", "api", RuntimeConfig(generate_fn=my_generate_fn))
# 3. Build the loop
loop = AutoDiagnoseLoop(
model=model,
diagnosis_agent=DiagnosisAgent(judge=judge),
max_cycles=3, # max M1→M5 iterations
max_analyzers=4, # analyzers per cycle (ranked by diagnostic priority)
)
# 4. Run on your failure cases
report = loop.run(failure_cases)
print(report.resolved) # True if an intervention fixed it
for h in report.final_hypotheses:
print(h.statement, "→", h.status) # SUPPORTED / REFUTED / INCONCLUSIVE
print(report.final_results.keys()) # analyzers run in the last cycle
To persist the run (event log, figures, report files, fix/experiment trial
folders), wrap the loop in a RunContext — see "Log and persist a diagnosis
run" in Extending.
Analysis-only mode
Omit diagnosis_agent to run M1+M2 only — useful when you want the probe's
ranked analysis without automated hypothesis generation:
loop = AutoDiagnoseLoop(model=model, max_analyzers=5)
report = loop.run(cases)
for name, result in report.final_results.items():
print(name, result.summary())
Controlling analyzer selection
StrategyProbe ranks analyzers by the modality slots the run is actually about
— the model's declared modalities intersected with the slots the batch fills:
from evalrx.eval_agent import StrategyProbe, ModelKind
probe = StrategyProbe()
probe.routed_slots(model, cases) # {"text", "audio"} for an ALM run
probe.detect_kind(model) # ModelKind.VLM / ALM / AVLM / AGENT / LLM
probe.select(model, max_analyzers=4, data=cases)
Pass data= whenever you have it. Without it, ranking falls back to the model's
declaration, and an omni model on an audio benchmark is ranked as a VLM because
it declares image.
Custom intervention (SurgeryAgent)
Override the default label-correlation verification with your own logic:
from evalrx.eval_agent import SurgeryAgent, InterventionResult, HypothesisStatus
def my_verify(hypothesis, model, results, data):
# domain-specific logic — return True when fixed
fixed = run_my_intervention(model, data)
return InterventionResult(
hypothesis=hypothesis,
status=HypothesisStatus.SUPPORTED if fixed else HypothesisStatus.INCONCLUSIVE,
fixed=fixed,
evidence={"custom": True},
)
loop = AutoDiagnoseLoop(
model=model,
diagnosis_agent=DiagnosisAgent(judge=judge),
surgery_agent=SurgeryAgent(verify_fn=my_verify),
)
Or trigger a param sweep — re-run analyzers with modified settings to compare before/after findings:
loop = AutoDiagnoseLoop(
model=model,
diagnosis_agent=DiagnosisAgent(judge=judge),
surgery_agent=SurgeryAgent(analyzer_params={"attention": {"top_k": 20}}),
)
Eval Agent — Pre-Registered A/B Loop
EvalOrchestrator enforces selective-inference safety: mine on explore,
pre-register a falsifiable hypothesis (hash + timestamp) before unblinding,
test once on validate, lock confirm for the final report.
from evalrx.eval_agent import EvalOrchestrator, PreregisteredHypothesis, DataSplit
hyp = PreregisteredHypothesis(
predicate="cluttered scenes",
statement="chain-of-thought prompt improves accuracy on multi-step questions",
direction="B>A",
min_effect=0.03,
alpha=0.05,
split="validate",
)
orch = EvalOrchestrator(split=DataSplit(explore_frac=0.5, validate_frac=0.3))
report = orch.run(cases, hyp, strategy_a, strategy_b)
# report keys: prereg_hash, decision, effect, ci, e_value, split, hypothesis
print(report["prereg_hash"]) # proves hypothesis was fixed before unblinding
print(report["decision"]) # "REJECT H0" or "inconclusive"
print(report["effect"]) # float
CounterfactualReplay ranks agent trajectory steps by causal influence (flip-rate):
Causal framework: Pearl (2000) Causality
Applied to NLP: Vig et al. 2020 — https://arxiv.org/abs/2004.12265
from evalrx.analyzers.agent.counterfactual import CounterfactualReplay
def rerun_fn(trajectory, step_idx, seed):
# wrap your live agent + verifier here; return True/False (success)
...
analyzer = CounterfactualReplay(rerun_fn=rerun_fn, n_replays=5)
result = analyzer._run(model, CaseBatch([case_with_trajectory]))
# result.findings["per_case"][0]["steps"] →
# [{"step": 1, "action": "extract_answer", "flip_rate": 0.80}, ...]
# High flip_rate ⇒ that step was causally influential.
Full runnable example: examples/preregistered_ab_demo/eval_agent/ (no API key needed, docker compose up).