Skip to content

fix(agent): retrofit the autonomous agent for the new Biolink restrictions - #75

Open
SkyeAv wants to merge 1 commit into
mainfrom
fix/agent-biolink-retrofit
Open

fix(agent): retrofit the autonomous agent for the new Biolink restrictions#75
SkyeAv wants to merge 1 commit into
mainfrom
fix/agent-biolink-retrofit

Conversation

@SkyeAv

@SkyeAv SkyeAv commented Aug 11, 2026

Copy link
Copy Markdown
Owner

The gap

96ebc23 ("fix(biolink): make KGX output validate against the Biolink Model") rebuilt the emit path — resolve_association_class, prune_to_class, inline_supporting_study, enum-ranged qualifiers, UNSATISFIABLE_EDGE_FIELDS, node demotion, validate-kgx. Its entire diff against the agent surface (agent.py, docs/agent.md, tests/test_agent_lazy.py) was the unrelated dspy[optimize] packaging move. Not one Biolink concept crossed over.

6f8456d did retrofit the prompt for effect_size/effect_type, so the pattern was expected — which makes this an omission rather than a policy.

Measured against the installed biolink-model 4.4.3:

Gap Evidence
No biolink-validity signal at all. build_and_audit wrote its NDJSON, counted lines, returned. biolink.validate_kgx had three callers — the CLI, one test, the docs — never the agent. agent.py:1230-1242
Nothing in the objective rewarded validity. quality_score = 0.5·coverage + 0.2·qc + 0.2·F1 + 0.1·schema. GEPA optimized the prompt against that. A run could converge on, persist, and register a config whose output validated at 0%. agent.py:2952, 3251
The prompt recommended gene_associated_with_condition — the exact predicate 96ebc23 measured failing on 723,595 edges. Post-fix it no longer errors; resolve_association_class silently walks up to bare biolink:Association. optimized_instructions.yaml:20-21,41,80
The QC assay flagged the correct predicate as wrong. GENERIC_PREDICATES marked associated_with a "generic fallback" — it is one of only three predicates GeneToDiseaseAssociation permits. qc/qc_report.py:18
The prompt told the LLM to emit supporting_study_size, which is in UNSATISFIABLE_EDGE_FIELDS and gets rerouted into a StudyResult.description string. No Annotation-level guard, no feedback. agent.py:1939,1988
map_coverage resolved qualifiers the build skips. lib.py:955 filters on if x.resolved; the agent didn't. Enum-ranged qualifiers want the token increased, never resolve, and drag overall down — enough to flip a good config to SKIPPED. agent.py:864, 1418
Both final-answer gates swallowed the coded error text, returning bare False for exactly the errors 96ebc23 wrote to be actionable. agent.py:688, 735
Zero documentation. Empty ## Unreleased; the 8.2.0 notes never mentioned the Biolink fix; docs/agent.md:6 claimed "NCATS Translator-compliant KGX" with nothing behind it. CHANGELOG.md:5-10

What this changes

Signal

build_and_audit now constructs every emitted record as its own Biolink class — the same check validate-kgx runs — and reports four fields. Non-fatal exactly like coverage: the KG already built, so a failure is a score to improve, never a build error.

Field Meaning
biolink_valid_pct Pass rate excluding known-pending fields. The scored number.
biolink_valid_pct_strict Unexempted rate, so the pending gap stays visible
biolink_problems Top "field: error-type" failures with counts
demoted_edge_pct Fraction of edges that fell back to bare biolink:Association

demoted_edge_pct is the predicate signal. A predicate its class forbids is never an error — it silently discards the class and every qualifier and evidence slot that class declared. gene_associated_with_condition on a gene~disease table builds cleanly, maps perfectly, and produces biolink:Association edges. Nothing else surfaces it.

quality_score reweighted to coverage 0.40 / biolink 0.25 / F1 0.15 / QC 0.10 / schema 0.10 (still a hard gate). Most of the new weight came out of w_qc, which scored a structurally constant 1.0 if qc else None. GEPA's feedback carries the problem histogram and demotion fraction; the judge gained a biolink_validity dimension, and its predicate/category heuristic is now capped by measured demotion rather than config shape.

New --biolink-threshold gates MAPPED. Default 0.0 (report-only) keeps today's terminal behavior; biolink_valid_pct / demoted_edge_pct are recorded on every state.json record either way.

effect_size/effect_type are exempted via KNOWN_PENDING_EDGE_FIELDS, derived as TABLASERT_EDGE_EXTRAS - <fields any association declares> — so it empties itself when biolink-model#1774 ships, with no code change. Same version-proofing discipline as UNSATISFIABLE_EDGE_FIELDS, and asserted as such.

Guidance

The agent's only vocabulary channel was the ~30 KB Section.model_json_schema() dump — 247 predicates and 159 categories with nothing tying the two together. The prompt now carries a compact predicate↔class table rendered at import from the installed model, via the new lib.predicate_options / biolink.legal_predicates (the missing authoring-time helpers — there was no way to ask which predicates a subject/object pair may carry without composing three private functions):

- Gene ~ Disease -> GeneToDiseaseAssociation: affects, associated_with, contributes_to
- SequenceVariant ~ Gene -> VariantToGeneAssociation: condition_associated_with_gene, …
- any predicate is safe for: Gene~Gene, Gene~Pathway, ChemicalEntity~Disease, …

Plus the two silent-relocation rules and the qualifier token rule — qualifiers had zero mentions in the prompt, so the three config-time validators 96ebc23 added were unreachable from agent-authored configs. optimized_instructions.yaml and qc_report.py corrected.

Consistency

  • map_coverage and _statement_nodes skip enum-ranged qualifiers as the build does. Qualifier.resolved already existed and simply wasn't consulted.
  • The improve loop requires no regression on either axis and a gain on one — a coverage win can no longer be bought with invalid KGX. Still monotonic.
  • derive_config returns the coded error instead of forwarding an invalid config, since the boolean gate can't carry it. New section_error() / table_config_error() expose the reason; the gates keep their bool contract and never-raises guarantee.
  • Annotation emits a BiolinkRelocationWarning naming where a relocated value went — a warning, not an error: nothing is lost, and every existing config (including the tutorial's supporting_study_size) keeps building.

Also fixed

A false pass in validate_kgx: a missing or misspelled path yielded total=0, valid=0, kept ok true, and exited 0 reporting "KGX output is Biolink-compliant." Left alone, the new agent gate would have inherited a silent 100%.

Verification

  • pytest: 765 passed, 35 skipped (was 728/34 — 22 new tests)
  • cargo test: 69 + 10 passed
  • ruff check / ruff format / pyright: clean

The end-to-end proof is test_demoted_edge_pct_catches_a_predicate_its_class_forbids — the same gene~disease table, built twice:

Predicate ok demoted_edge_pct
associated_with True 0.0 — keeps GeneToDiseaseAssociation
gene_associated_with_condition True 1.0 — demoted to bare Association

Both build successfully. That is the point: the forbidden predicate is never an error, which is why the metric had to exist.

Notes for review

  • --biolink-threshold defaults to 0.0, so nothing gates today. Worth looking at a real batch's biolink_valid_pct before picking a threshold.
  • Not done, and deliberately: the optional full-corpus replay against .tablassert/million-kg/ to quantify the pending-exemption rate against the strict rate.
  • Out of scope but worth flagging while nearby: derive_mode (agent.py:3204) is a library-only parameter with no CLI flag, no test, and no docs — and DERIVED sits in the resume-terminal set, so such a record is never picked up by a later full run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PNmGrcymfW5ZS1W34nD1H9

…tions

96ebc23 ("fix(biolink): make KGX output validate against the Biolink Model") rebuilt
the emit path, but its entire diff against the agent surface (`agent.py`,
`docs/agent.md`, `test_agent_lazy.py`) was the unrelated `dspy` -> `[optimize]`
packaging move. Not one Biolink concept crossed over. 6f8456d DID retrofit the prompt
for `effect_size`/`effect_type`, so the pattern was expected -- this is an omission,
not a policy.

Measured against the installed biolink-model 4.4.3, the agent:

- had no biolink-validity signal at all. `build_and_audit` wrote its NDJSON, counted
  the lines, and returned; `biolink.validate_kgx` had three callers, none of them the
  agent. Nothing in `quality_score` or GEPA's objective rewarded validity, so a run
  could converge on, persist, and register a config whose output validated at 0%.
- recommended `gene_associated_with_condition` in `optimized_instructions.yaml` -- the
  exact predicate 96ebc23 measured failing on 723,595 edges. Post-fix it no longer
  errors; `resolve_association_class` silently walks up to bare `biolink:Association`.
- was told to emit `supporting_study_size`, which is in `UNSATISFIABLE_EDGE_FIELDS` and
  gets rerouted into a `StudyResult.description` string with no feedback.
- resolved enum-ranged qualifiers through the fullmap that `lib.py:955` deliberately
  skips, depressing coverage for columns the build never resolves.
- swallowed the coded error text at both final-answer gates, so the LLM got a bare
  False for exactly the errors 96ebc23 wrote to be actionable.

SIGNAL

`build_and_audit` now constructs every emitted record as its own Biolink class (the
same check `validate-kgx` runs) and reports `biolink_valid_pct`,
`biolink_valid_pct_strict`, `biolink_problems`, and `demoted_edge_pct` -- the last
being the only surfaced evidence that a predicate cost its edge the association class.
Non-fatal exactly like coverage: the KG already built.

`quality_score` reweighted to coverage 0.40 / biolink 0.25 / F1 0.15 / QC 0.10 /
schema 0.10. Most of the new weight came from `w_qc`, which scored a structurally
constant `1.0 if qc else None`. GEPA's feedback carries the problem histogram and
demotion fraction; the judge gained a `biolink_validity` dimension, and its
predicate/category heuristic is now capped by measured demotion rather than config
shape. New `--biolink-threshold` gates MAPPED; default 0.0 keeps today's behavior
while recording the rate on every record.

`effect_size`/`effect_type` are exempted via `KNOWN_PENDING_EDGE_FIELDS`, derived as
`TABLASERT_EDGE_EXTRAS - <fields any association declares>`, so it empties itself when
biolink-model#1774 ships. Nothing is hardcoded to a model version.

GUIDANCE

The prompt's only vocabulary channel was a ~30 KB enum dump: 247 predicates and 159
categories with nothing tying the two together. It now carries a compact predicate<->
class table rendered at import from the installed model via the new
`lib.predicate_options` / `biolink.legal_predicates`, plus the two silent-relocation
rules and the qualifier token rule (qualifiers had zero prompt mentions).
`optimized_instructions.yaml` and the QC assay are corrected -- the latter's
`GENERIC_PREDICATES` flagged `associated_with`, one of only three predicates
`GeneToDiseaseAssociation` permits, as a "generic fallback".

CONSISTENCY

`map_coverage` and `_statement_nodes` skip enum-ranged qualifiers as the build does;
the improve loop requires no regression on either axis and a gain on one;
`derive_config` returns the coded error instead of forwarding an invalid config, since
the boolean gate cannot carry it. `Annotation` emits a `BiolinkRelocationWarning`
naming where a relocated value went -- a warning, not an error: nothing is lost and
every existing config keeps building.

Also fixes a false pass in `validate_kgx`: a missing path yielded `total=0, valid=0`,
kept `ok` true, and exited 0 reporting compliance. Left alone, the new agent gate
would have inherited a silent 100%.

Verification: pytest 765 passed / 35 skipped (was 728/34; 22 new tests, including an
end-to-end proof that the same table built with `associated_with` scores
demoted_edge_pct 0.0 and with `gene_associated_with_condition` scores 1.0).
cargo test 69 + 10. ruff check / ruff format / pyright clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PNmGrcymfW5ZS1W34nD1H9
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@SkyeAv, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8047f0a1-c2a8-4382-badc-bd25d115bb0e

📥 Commits

Reviewing files that changed from the base of the PR and between 4dd3351 and de437a1.

📒 Files selected for processing (21)
  • CHANGELOG.md
  • docs/agent.md
  • docs/cli.md
  • docs/configuration/table.md
  • examples/agent/optimized_instructions.yaml
  • examples/agent/qc/qc_report.py
  • src/tablassert/agent.py
  • src/tablassert/biolink.py
  • src/tablassert/cli.py
  • src/tablassert/errors.py
  • src/tablassert/lib.py
  • src/tablassert/models.py
  • tests/test_agent_assembly.py
  • tests/test_agent_build.py
  • tests/test_agent_cli.py
  • tests/test_agent_coverage.py
  • tests/test_agent_derive.py
  • tests/test_agent_eval.py
  • tests/test_biolink.py
  • tests/test_lib.py
  • tests/test_models.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@SkyeAv

SkyeAv commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant