Commit 6d05bb0
Env-triad eval: automation on top of Olivia's branch (#69)
* begin eval
* create test cases
* cases were wrong
* update expected value fields
* add multiple models to the cases
* Plumbing for env-triad eval: rename + gitignore + just targets
Mechanical changes to align with sibling-eval conventions
(sampledata, ebs, field-guidance). No behavior change to the
generator or suite content — those are follow-ups per #55.
- Rename `create_test_cases.py` -> `generate_suite.py` to match
other datasets' generator names. Update `OUTPUT_YAML` constant
to `env-triad-suite.yaml` (drop `-test-` infix).
- Remove the committed 48 MB `env-triad-test-suite.yaml` — other
datasets never commit the generated YAML; it's regenerated via
`just generate-*`.
- Gitignore all four `datasets/*/{name}-suite.yaml` paths so a
regenerated suite doesn't accidentally end up committed.
- Add `just` targets mirroring the sibling pattern:
- `generate-env-triad` — run the generator
- `run-env-triad` — run the suite via `llm_matrix`
- `eval-env-triad` — clean + generate + run (matches
`eval-sampledata`, `eval-ebs`)
- `clean-env-triad-outputs` — remove the output dir + sqlite
- Hook `eval-env-triad` into `clean-outputs` and `generate-all`.
Follow-ups tracked in #55:
- Move MIxS schema context from per-case `input` into template
`system` prompt
- Build ideals as `label [CURIE]` instead of bare `ENVO_...`
- Pull from multiple studies for biome differentiation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Correctness pass on env-triad generator: dynamic schema + null safety
Rewrites generate_suite.py. Shrinks from 805 lines to ~215. Behavior
changes:
- **Schema context is now dynamically sourced.** Previously the system
prompt embedded a 700-line hardcoded string that was a frozen snapshot
of the production suggestor's env-triad schema context. Any upstream
MIxS change would silently drift from this generator. Now the system
prompt calls SchemaContextBuilder at generation time and filters slots
to the three env-triad fields per interface. Reproduces the logic of
SchemaContextBuilder.format_env_triad_context (present in newer
suggestor versions but not the pinned v1.1) using v1.1 primitives
(get_interface_schema + format_slot). Once the suggestor dep is bumped
past PR #79's release, this function can collapse to a single call.
- **Ideal construction is null-safe.** Previously
`biosample.get("env_broad_scale").get("term").get("name")` raised
AttributeError on any biosample missing an env-triad field. Now each
term value goes through `_term_to_label_curie` which returns None on
missing/malformed input, and `make_ideal` returns None for a case that
can't produce a scorable ideal. The main loop skips such cases and
reports a count. No more silent crashes mid-generation.
- **--study-id CLI flag.** Was hardcoded to Bioscales; now defaults to
it but accepts any NMDC study ID. Sets up the multi-study scope work
(#55) to come.
Format of ideals (`label [CURIE]`) was already correct on this branch —
that's preserved. Module docstring updated to say "public API" instead
of "MongoDB" (inaccurate; StudySearch hits the NMDC REST API).
Plumbing from the prior commit (rename, just targets, gitignore) remains
unchanged; this commit only touches the generator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Scope pass on env-triad generator: multi-study support
Adds the infrastructure for pulling biosamples from multiple NMDC
studies, setting up the biome-diversity work #55 calls for.
- New config file: datasets/env-triad-prediction/studies.yaml lists
the studies to include. Current list is a starting point (just
Bioscales) with structure in place to add more — an `expected_biomes`
field lets a future reviewer sanity-check that the selection covers
aquatic, host-associated, built-environment, sediment, etc.
- Generator now iterates over a list of study IDs. CLI accepts
--study-id <id> repeated, or reads the curated list from studies.yaml
by default. `--studies-yaml <path>` allows a different config file.
- Each case gets the study ID as an extra tag (alongside the existing
value_prediction tag), so result analysis can filter/group by study
without re-deriving it from the input text.
- Summary reports both the overall and per-study kept/skipped counts.
The actual expansion of studies to get biome diversity is deliberately
not in this commit — picking which NMDC studies to include is a
domain-expert judgment that belongs to Olivia, not me. The infrastructure
to add them is now trivial: extend studies.yaml.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add `just pilot-env-triad` for tiny smoke-test runs
Running the full eval is 416 cases x 5 models (~\$10+). Before
committing to that, it's useful to prove the generate -> run pipeline
works end-to-end on a handful of cases with one cheap model. Previously
that required hand-editing the generated YAML, which is fragile.
- `generate_suite.py` gains two flags:
- `--max-cases N`: cap the number of cases after generation
- `--models NAME[,NAME...]`: override the model list from models.yaml
- `just pilot-env-triad` chains clean + small generate + run. Defaults
to 3 cases x gpt-4o-mini (a fraction of a cent). Override with
positional args: `just pilot-env-triad 10 gpt-4o`.
The generate flags stand on their own — useful outside the pilot
target for any time you want a bounded generator output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Escape braces in env-triad system prompt for llm-matrix str.format
llm-matrix calls template.system.format(**template_params) on the
system prompt at inference time (see llm_matrix.aimodel.AIModel.prompt
line 67). Any literal { / } in the system string — e.g. the JSON
schema example and the regex patterns inside the MIxS schema context
like {7,8} — was treated as a {placeholder} and raised KeyError
during the first live pilot:
Error during eval: '\n "metadata_fields"'
Fix: double literal braces ({{ / }}) so str.format emits the literal
characters instead of trying to look them up as keys.
Pilot run after the fix succeeds end-to-end: 3/3 calls complete,
outputs captured, tokens logged. Scoring misses are a separate
(format-level) issue tracked under #61 — not a pipeline break.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Readable summary + per-field columns for env-triad evals
Two display/reporting changes, both in run_suite.py:
Short labels in the console summary. Previously the per-case, per-
category, and misses sections all dumped the full case_ideal JSON —
a 300+ char blob per row. Adding _try_parse_env_triad() to extract
env_broad_scale / env_local_scale / env_medium values, and
_short_label() to format them as "broad | local | medium", makes the
output readable at any scale. Falls back to the first 80 chars for
non-env-triad ideals — non-env-triad evals (ebs, sampledata) are
unaffected. Also widens the "got" display from 50 to 200 chars.
Per-field columns in results.tsv. Added expected_{broad,local,medium},
got_{broad,local,medium}, and {broad,local,medium}_match booleans.
Lets downstream analysis pivot on per-field accuracy (which field is
hardest? which models nail medium?) without re-parsing JSON. Columns
are all None for evals whose ideal isn't env-triad-shaped.
Pilot run after the changes: output fits on one line per row, per-
category grouping uses short labels, TSV has 9 new columns. Verified
with 3-case gpt-4o-mini pilot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address Copilot review on env-triad PR
Four valid catches:
- **Greedy JSON regex.** The non-greedy `\{.*?\}` theoretically stops
at the first inner `}`; env-triad JSON has nested `{}` per field in
metadata_fields. Backtracking saved us in the pilot, but a greedy
match anchored by the closing fence is safer. Also catches edge
cases with multiple fenced blocks in one response.
- **Exception safety in _try_parse_env_triad.** Docstring said
"never raises" but could raise AttributeError/TypeError if the
parsed JSON is a list, or if metadata_fields items aren't dicts.
Added isinstance guards so the helper is actually exception-safe
and only accesses `.get()` on dicts.
- **`*_match` was always True for all-None rows.** For non-env-triad
evals (or responses that failed to parse), both expected_broad and
got_broad are None, and `None == None` evaluates True — misleading
"all match" signal. Match columns now return None when either side
is None; False only when the sides differ with at least one value.
- **`clean-suites` missed field-guidance suite YAML.** I added
env-triad to the list but the existing field-guidance YAML — which
is also gitignored and regenerated — was never cleaned. Adding it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Document how to use CBORG / AI Studio in eval suites
Two workflow gaps Olivia will hit once she moves beyond the default
model list in `models.yaml`:
- **CBORG in suite evals.** Setting CBORG_API_KEY in the secrets file
only covers verify-auth / probe-tiers (which use the openai SDK
directly). Suite evals route through llm-matrix -> llm plugins, so
they need aliases defined in
`~/.config/io.datasette.llm/extra-openai-models.yaml` plus a separate
`llm keys set cborg`. Now documented with the exact steps + pointer
to `just probe-tiers --list-cborg-models` for the ~200-model catalog.
- **AI Studio expansion.** `llm-gemini` supports the full AI Studio
catalog, not just the entries in `models.yaml`. Added a short
section noting you can drop any `gemini/<name>` into a pilot command.
Also flags #71 as a known limitation: the CBORG key currently has to
live in both the secrets file and the llm keystore. Low-priority
follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add --env flag to generator for dev API fallback
When api.microbiomedata.org returns 502, the dev API
(api-dev.microbiomedata.org) is often still up. The StudySearch class
already supports env="dev" — this just exposes it at the CLI level.
- generate_suite.py: --env {prod,dev} flag, default prod. Prints a
note when dev is used so the source is visible in run logs.
- justfile: threads env param through generate-env-triad and
pilot-env-triad with default "prod". Usage:
just pilot-env-triad 50 gpt-4o-mini dev
just generate-env-triad dev
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Document dev API fallback in justfile, docstring, and auth.md
- justfile: expand pilot-env-triad and generate-env-triad comments
to mention the env param and show an example with dev
- generate_suite.py: add dev-API and multi-model examples to the
Usage:: docstring
- docs/auth.md: add troubleshooting entry for NMDC 502s with the
exact fallback commands
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Make run_suite robust to scorer parse errors; add --scorer-model flag
Problem: llm-matrix's simple_question metric makes a second LLM call
to score each response. Some models (e.g. cborg/claude-sonnet-4-6) return
a verbose explanation like "The score is 0.33 because..." instead of
starting with the number. The regex `r"(\d+(\.\d+)?)"` applied with
`.match()` fails on prose-first responses and raises ValueError("Could not
parse score from ..."), crashing the entire run at the failing call.
Two changes:
1. **Continue on score-parse errors.** Catch ValueError in the per-case
iteration loop and in a fallback except block below it. Record
score=None for the affected results, emit a warning, and continue.
The TSV still gets written with all results collected so far.
2. **--scorer-model flag (+ LLM_SCORER_MODEL env var).** Exposes
LLMRunnerConfig.evaluation_model_name so users can pin the scorer
to a known-reliable model (e.g. gpt-4o-mini) when the default
model behaves badly as a judge. Also printed in the run header so
it appears in logs.
Together these make multi-model runs resilient to scorer flakiness on
any single model — the run completes and marks affected rows as
score=None rather than aborting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Wire --scorer-model through just run target
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix run_suite to continue on scorer parse errors with full logging
Replace the broad try/except block with a while/next() loop that catches
ValueError (scorer parse failure) per iteration. When the scorer returns
prose instead of a leading number:
- The exception text (which contains the scorer's full response) is
printed to stderr immediately
- A placeholder token_data entry is added to keep indexing aligned
- The loop continues from the next case
- score_parse_failures is incremented
For regular results, evaluation_message (the scorer's response) is already
in the TSV via results_to_dataframe, so successful scorer calls are fully
logged without any extra work.
The end-of-run note now accurately describes what happens to failed cases
(the result is lost — not just scored as None — because the exception
interrupted the iterator before the result was yielded), and points users
to --scorer-model / LLM_SCORER_MODEL as a preventative measure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(run_suite): handle NaN case_ideal in _try_parse_env_triad
When a scorer parse error drops a result mid-run, the DataFrame that
feeds the summary has NaN (float) in case_ideal for that row. The old
guard (`if not text`) didn't catch floats — re.search then crashed the
whole run with TypeError.
Tighten the guard to `isinstance(text, str)` and add a regression test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(run_suite): drop NaN rows before summary; harden _short_label
Earlier NaN fix in _try_parse_env_triad only covered one code path.
_short_label also touched case_ideal/response_text without guarding
and crashed mid-summary on lost rows. Two-layer fix:
- Drop rows with NaN case_ideal at the top of _print_summary (these
are results lost to scorer parse errors — nothing in the summary
can be computed from them, they don't belong in ranking or category
pivots).
- Mirror the isinstance guard from _try_parse_env_triad in _short_label
as defense-in-depth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(env-triad): drop simple_question metric — unblocks long runs
Root cause of today's 53-of-1200 early termination: llm-matrix's
runner generator raises ValueError when simple_question's scorer
model returns prose instead of a bare float. Once the generator
has raised, subsequent next() calls return StopIteration — the
continue-on-error logic in run_suite.py can't re-enter the closed
generator. Result: one flaky scorer response kills the whole run.
The env-triad suite doesn't need simple_question anyway. We
override r.score with _env_triad_score (direct per-field match)
in run_suite.py, so every simple_question call was making a
second LLM call per result whose score we discard — strictly
cost and failure surface, no signal.
Other suites (sampledata, ebs, field-guidance) still use
simple_question because they rely on its score.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: olivia.hess <olivia.hess@pnnl.gov>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 9efeb80 commit 6d05bb0
9 files changed
Lines changed: 988 additions & 115 deletions
File tree
- datasets/env-triad-prediction
- docs
- src/nmdc_ai_eval
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
12 | 18 | | |
13 | 19 | | |
14 | 20 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
0 commit comments