Galaxy agent evals
Real-LLM evaluation harness for the agents in lib/galaxy/agents/. Built on
pydantic-evals. Runs on demand, not in
CI -- real LLMs cost money, are slow, and are flaky.
What it does
Runs curated datasets against Galaxy agents, scores each model, and emits a markdown comparison table. The point is to weigh models against each other ("is gpt-oss-120b on Jetstream good enough as a free default? does Llama-4-Maverick win on error analysis?") and to iterate on prompts with real measurement. Not run in CI.
Current datasets:
- routing: (query, expected handoff target) pairs against
QueryRouterAgent. Scored byHandoffMatch(deterministic). - error_analysis: prose failure descriptions against
ErrorAnalysisAgent. Scored two ways:MustMention(deterministic keyword check) andLLMJudge(fuzzy "is the advice on-topic and actionable for this failure mode"). - tool_recommendation: "what tool should I use for X?" queries against
ToolRecommendationAgent. Scored byMustMentionAny(any of a set of canonical tool names appears) plus optionalLLMJudgewith per-case rubrics. Runs without a live toolbox, so it measures the model's prior knowledge of Galaxy tools rather than grounded search behavior. - router_tool_use: inventory/availability queries that should trigger
router fast-path tool calls (
search_tools,search_workflows,list_histories,get_server_info,get_user_info). Scored byToolCallMatch-- did the model invoke at least one of the expected tools? Includes a regression case for the failure observed in galaxyproject/galaxy#21661 (comment 4367167981) where the router answered "what tools are installed?" with a generic essay instead of callingsearch_tools. - capabilities: "what can you do?" groundedness against
QueryRouterAgent. Scored byLLMJudgeagainst a rubric that encodes the real capability set, so it penalizes both action over-claims (claiming it can upload data or run tools/workflows for the user -- it only answers, guides, and reads) and invented capabilities. Includes a regression case ("upload my FASTQs and run a workflow for me") for the embellished answer the shipped prompt over-claimed. - staining_quantification: end-to-end bioimaging use case --
brightfield RGB inputs, color deconvolution, per-ROI quantification,
Omero export. Scored by
LLMJudgeagainst per-case rubrics for response substance. The routing decision for the same prompts is scored separately by the matching cases in theroutingdataset, so a full flight check runs both. Cases needing a live Galaxy session (history sanity check, save-to-page) are off by default; pass--include-galaxy-requiredto include them.
Layout
test/evals/
datasets/ # One module per dataset
routing.py
error_analysis.py
evaluators.py # HandoffMatch, MustMention
judge.py # Builds an OpenAIChatModel for use as LLM judge
specs.py # DatasetSpec registry: dataset -> task + evaluators
tasks.py # Wraps agents as pydantic-evals task callables
run_evals.py # CLI
results/ # Local-only run artifacts (gitignored). Keep
# baselines you care about somewhere outside the
# repo (the JSON sidecars are noisy). Filenames
# encode the repo SHA so any baseline pairs back
# to a commit.
Two runners
The harness ships with two runners that share dataset definitions, evaluators, and report format. Use whichever fits the question you're asking.
Fast loop -- standalone CLI (default)
python -m evals.run_evals builds GalaxyAgentDependencies from a
MagicMock'd trans, so the agents run without a live Galaxy. Fast
iteration, no Galaxy startup, ideal for prompt work and cross-model
comparison. Cases marked requires_galaxy=True are filtered out by
default.
Real flight check -- pytest live runner
test/integration/test_live_evals.py runs the same datasets inside a
Galaxy integration-test fixture with a real trans. Seeds a demo
history via test/evals/seed_staining_quantification_history.py, runs
the requires_galaxy=True cases against it, writes a report to
test/evals/results/ in the same shape as the CLI. Slower (Galaxy
startup), but the only path that actually exercises history-dependent
cases.
Which to use
- Iterating on a prompt? CLI.
- Choosing between models? CLI.
- End-to-end flight check before a demo rehearsal? Pytest live runner.
- Cases involving "my history", "my analysis", or the history agent doing real tool calls? Pytest live runner.
Running the CLI
Copy test/evals/models.yaml.sample to test/evals/models.yaml
(gitignored), list each model you want to evaluate with its proxy URL
and key, then run from the test/ directory so the evals.* modules
import without an editable install:
. .venv/bin/activate
cd test
python -m evals.run_evals --model-config evals/models.yaml
That runs every model in the YAML against every dataset. Output goes to
stdout and to test/evals/results/<date>-<datasets>-<sha>.md.
You can still pass --models gpt-oss-120b,Llama-4-Maverick-17B-128E-Instruct
to restrict to a subset.
Running the pytest live runner
export GALAXY_TEST_ENABLE_LIVE_LLM=1
export GALAXY_TEST_LIVE_EVALS=1
export GALAXY_TEST_AI_API_KEY=...
export GALAXY_TEST_AI_API_BASE_URL=http://localhost:4000/v1/
export GALAXY_TEST_AI_MODEL=gpt-oss-120b
# Optional: override which datasets/models/judge to run
# export EVALS_MODEL_CONFIG=/path/to/models.yaml
# export EVALS_DATASETS=staining_quantification
# export EVALS_MODELS=gpt-oss-120b
# export EVALS_JUDGE_MODEL=gpt-oss-120b
pytest test/integration/test_live_evals.py -v
The live runner always passes include_galaxy_required=True, so the
history-needing staining-quantification cases (history_sanity_check,
summarize_to_page, report_takeaway, social_media_post) actually
get exercised. Default scope is staining_quantification only;
override with EVALS_DATASETS.
The default judge is Llama-4-Maverick-17B-128E-Instruct rather than
gpt-oss-120b because gpt-oss-120b tends to grade itself too
charitably; Maverick scores hand-checked-correct responses more
accurately. Override with EVALS_JUDGE_MODEL if Maverick isn't
reachable. (The standalone CLI keeps gpt-oss-120b as its default
judge for baseline continuity.)
Diffing against a previous run
Save baselines outside the repo (the JSON sidecars are noisy). Filenames encode the repo SHA they were generated from, so any baseline file pairs back to a specific commit.
cd test
python -m evals.run_evals \
--baseline /path/to/baselines/2026-05-08-121624-bioinformatics_workflows+orchestrator_planning+tool_recommendation+router_tool_use-db9b1cb0799.md
--baseline looks for the JSON sidecar next to the markdown (each run writes
both) and renders a "Changes vs baseline" section listing per-case
regressions and improvements per (dataset, model). Useful when iterating
on a prompt -- run before, change the prompt, run with --baseline pointing
at the before file, see what moved.
Useful flags
--datasets routing,error_analysis-- pick which datasets to run (default is all registered).--repeat 3-- run each case N times to expose stochasticity. Per-case detail shows e.g.2/3 [+1 ERR]when one of three runs errored.--judge-model gpt-oss-120b-- model for fuzzy LLM-as-judge scoring (default gpt-oss-120b). Must also be declared in--model-config.--only oom_137,exit_127-- restrict to specific case names.--include-galaxy-required-- include cases that need a live Galaxy session (thehistory_analyzerones). Off by default.--max-concurrency 8-- raise the per-model concurrency limit (default 4).--model-config <yaml>-- override default ofevals/models.yaml.--baseline <md-or-json>-- diff results against a previous run.--no-write-- skip writing the report file.
Adding cases
Edit the relevant datasets/<name>.py. Each case is the input the agent
sees plus whatever metadata the dataset's evaluators need (expected handoff
target for routing, must_mention keywords + failure_mode for error_analysis).
Adding datasets
- Drop a new module under
datasets/exporting a<name>_dataset(...)function that returns a pydantic-evalsDataset. - Add a
make_<name>_tasktotasks.py. - Register a
build_<name>inspecs.py'sSPECSdict.
The CLI auto-includes anything registered in SPECS.
How this relates to other agent test infrastructure
test/unit/app/test_agents.py::TestAgentUnitMocked-- deterministic mocked unit tests, run in CI. Cover plumbing, not LLM behaviour.test/unit/app/test_static_agent_backend.py-- tests for theStaticAgentRegistry, which swaps real agents for canned-YAML responses. Complementary, not a substitute: static fixture is for orchestrator plumbing tests; evals are for measuring the LLM itself.