mirror of
https://github.com/mims-harvard/ToolUniverse.git
synced 2026-09-19 07:31:47 +08:00
fix/duplicate-tool-names
48 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8fc703fa56 |
Fill remaining skill gaps: DHS Program, EBI sequence tools, DeepSpotM, MolGlueDB
Second pass of the pre-release skill sweep, covering tool families the first batch (#592) didn't reach: - dataset-discovery, epidemiological-analysis: add DHS Program (DHSProgram_search_indicators/_get_data) as the international/LMIC equivalent of NHANES for fertility/maternal/child mortality/nutrition/ immunization/HIV survey indicators. - sequence-analysis: add EBI_scan_pfam_domains, EBI_profile_search, EBI_pairwise_align, EBI_translate_sequence, and EBI_predict_membrane_topology alongside the existing Pfam/BLAST/DNA- translation tools they complement (raw-sequence Pfam scan, remote-homology search, pairwise alignment, back-translation/6-frame, topology prediction for unannotated sequences). - spatial-transcriptomics: add DeepSpotM_predict_gene_expression as the virtual-readout option when only an H&E image is available and no real spatial assay exists. - binder-discovery: add MolGlueDB_search_compounds/_get_compound as a known- ligand-mining source specifically for molecular-glue-degrader neosubstrates (targets with no tractable pocket for conventional small-molecule binding). Also re-ran scripts/sync-codex-plugin-skills.sh, which caught up three plugins/tooluniverse/skills/ files (tooluniverse-biomedical-fact-lookup, tooluniverse-phylogenetics, tooluniverse/SKILL.md) that were stale relative to already-merged commits in skills/ and had never been propagated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
0142e85518 |
Sweep skills for gaps left by ~48 new tool families since v1.4.1
Dispatched 6 parallel investigations covering all 41 tool families not already checked in prior rounds (structural/protein, genomic archives, plant/microbiology/ecology, funding/regulatory/ontology, rare-disease, and misc). Fixed every confirmed gap in one batch (16 skills touched): Clean additive fixes (tool exists, right skill exists, just not mentioned): - SKEMPI -> structural-proteomics (protein-protein binding mutations, BindingDB's missing counterpart) - REBASE -> molecular-cloning (Golden Gate hardcodes enzyme names with no lookup path for recognition sites/isoschizomers) - DANDI -> neuroscience (has a dedicated Neurophysiology Reasoning section, zero mention of the field's actual data archive) - BAR + Planteome -> plant-genomics (zero mentions; expression data and ontology annotation are gaps this skill's own description implies it should cover) - BOLD Systems -> ecology-biodiversity (DNA barcode species ID, direct fit for a skill already built around species identification) - Codon usage -> antibody-engineering + sequence-analysis: found a genuinely broken instruction -- antibody-engineering literally says "assess codon optimization for CHO" with no tool to do it, and sequence-analysis's only codon-usage source is a hardcoded human-only table - FDA Purple Book -> drug-regulatory (skill already has a worked example about finding a biosimilar; the tool for that was never named) - Folklore MCP -> variant-interpretation (direct match to the existing ClinVar/gnomAD/OMIM/ClinGen tool list) - Genomic Intelligence MCP -> regulatory-genomics (same tool class as the skill's existing AlphaGenome/Enformer/Borzoi/ChromBPNet/Evo2 comparison table) - EWAS Catalog -> epigenomics (methylation-trait associations, this skill's own domain) - GoaT -> comparative-genomics (genome-assembly-exists check before gene-level lookups on non-model species) - dbGaP/EGA/DDBJ/GSA -> dataset-discovery (frontmatter claimed dbGaP coverage the body never delivered; DDBJ is the strongest case, same INSDC tier as SRA/ENA which the body does list) - NIH RePORTER -> nih-funding-landscape (minor: simpler direct path alongside the existing OpenNIH bulk-corpus tools) Two duplicate-tool-family situations resolved as documentation clarifications, not removals (both families are live and each has some genuinely unique capability, unlike the earlier PR-vs-PR AlphaGenome duplicates which were cleanly supersede-and-close cases): - Orphadata vs. Orphanet (rare-disease-genomics): Orphanet is more complete (10 tools vs 4) and stays the default; noted Orphadata's MeSH/UMLS/MedDRA cross-refs as the one thing Orphanet's own ICD/OMIM/SNOMED mapping doesn't cover - OpenFDADevice vs. OpenFDA_device (product-safety-surveillance): 3 of 6 tools are genuinely new (UDI lookup, classification, PMA) and now listed; the other 3 overlap with an existing family, noted as interchangeable rather than duplicated guidance Also fixed while in here: tooluniverse-nih-funding-landscape and tooluniverse-antigravity-plugin existed only in skills/, never propagated to either plugin distribution since they were added -- this session's earlier plugin-sync runs were incorrectly treating their appearance as sync-script drift and discarding them each time. Both are legitimate (added via #354 and the OpenNIH PR respectively, neither excluded by the sync script's own deliberate exclusion list) and are now properly mirrored to plugin/skills/ and plugins/tooluniverse/skills/. Every tool name referenced (44 total) verified programmatically against the actual registered tool JSON before writing; two MCP env var names (FOLKLORE_MCP_URL, GENOMIC_INTELLIGENCE_MCP_URL) verified against their loader configs. Confirmed correctly out of scope, no action (~18 tool families): mcsa, membrane_topology/pdbtm (too niche for existing skills), bacdive, mediadive, ncats_translator, nci_evs, fhir_terminology, smartapi, scicrunch_rrid, nasa_osdr, mddb, biothings_gateway (flagged as a separate cross-cutting design question), deepspotm (plausible, not clear-cut), GIAB/CMS Open Payments/NIH DSLD/MolGlueDB (genuine gaps but no owning skill exists -- would need a new skill, out of scope for "is an existing skill stale"). |
||
|
|
656ffa22b8 |
Fix stale complex-prediction limitation in protein-structure-prediction skill
The skill flatly stated "complex prediction requires AlphaFold-Multimer (not available via these tools)" -- no longer true since the official Boltz API tools (72 Boltz_* tools, multi-chain + protein-ligand binding) landed. Added a dedicated section rather than folding it into the existing free-tool workflow: Boltz is paid (BOLTZ_API_KEY gated), asynchronous (submit/poll/retrieve), and requires an idempotency key to avoid double-billing -- explicitly instructs estimating cost and confirming with the user before submitting a paid job, and keeps ESMFold/AlphaFold as the default for anything that doesn't actually need a complex. Checked tooluniverse-single-cell for the other major addition since v1.4.1 (Single Cell Portal / UCSC Cell Browser / CellTypist catalog, ~8 dataset-discovery tools) -- correctly out of scope there, since that skill is about analyzing data the user already has (h5ad files), not discovering new public datasets. No fix needed. |
||
|
|
2a4700e55f |
Add Consensus, Noodle, and Exa to the literature-deep-research skill
Found while auditing skills for release readiness: three search tools landed since v1.4.1 (Consensus_search_papers, the opt-in Noodle MCP, the opt-in Exa MCP) with no mention in the skill built specifically around literature search and evidence grading. - Consensus_search_papers: added to the "Broad" domain row (2.2) and to Phase 3 (Evidence Grading) -- its study-design metadata (study type, sample size) is a fast first-pass triage signal, not a substitute for reading the source or the grade itself - Noodle (noodle_*, opt-in via NOODLE_MCP_URL): added to citation expansion (2.1) alongside the existing citation-graph tools, with the same "discovery signal, not evidence of validity" caveat its own tool description carries - Exa (exa_*, opt-in via EXA_MCP_URL): added as a last-resort fallback in 2.5 for when every structured index is empty -- flagged clearly as general web search, not a scientific database, not to be graded T1-T4 SerpBase (the new web_search backend) needed no mention -- it's a transparent swap under an existing tool's hood, invisible to this skill's guidance. Also updated TOOL_NAMES_REFERENCE.md with the same three additions. plugins/tooluniverse/skills/ regenerated for this skill only (unrelated pre-existing drift in other skills reverted, per the established pattern on this branch's predecessors). |
||
|
|
a6afe6e469 |
Cross-reference regulatory-variant-analysis from the rare-disease consequence filter
Found while checking whether the AlphaGenome/Atlas pipeline added to tooluniverse-regulatory-variant-analysis is actually discoverable: the router (tooluniverse/SKILL.md) sends "rare disease" queries to tooluniverse-rare-disease-genomics instead, and that skill's own consequence-hierarchy heuristic said "synonymous/intronic: usually benign unless at splice junction" with no cross-reference to the skill built to check exactly the exception -- a deep intronic variant creating a cryptic splice site far from the canonical boundary, i.e. the real GREGoR/DNM1 case the new pipeline is modeled on. Someone following this skill's own advice verbatim would dismiss precisely the variant type the new capability exists to catch, without ever learning it exists. Added one clause noting the exception and pointing to tooluniverse-regulatory-variant-analysis. |
||
|
|
3993097ed3 |
Cross-reference alternative sequence models when AlphaGenome isn't the fit
The skill's only sequence-prediction option was AlphaGenome -- if it's unavailable (no key) or the case specifically calls for a published, self-hostable model instead, there was no path forward mentioned. ToolUniverse already has that path (Enformer/Borzoi/ChromBPNet/Evo2, covered in tooluniverse-regulatory-genomics); this just points to it from Phase 4.5 and the missing-key fallback bullet, so the pipeline doesn't dead-end when AlphaGenome specifically isn't the right tool. |
||
|
|
341e8b6dd9 |
Fix wrong tool name and add robustness/generality gaps in the regulatory-variant-analysis pipeline update
Found on review: - atlas_scan_interval was referenced without its AlphaGenome_ prefix in two places -- the literal string doesn't match any registered tool, so an agent following the skill verbatim would fail to call it - No guidance for the case where ALPHA_GENOME_API_KEY isn't configured; Phase 0.5 read as a hard dependency when the rest of the skill (and its own pre-existing pipeline) works entirely without any gated key. Added an explicit skip-and-proceed instruction in Phase 0.5 and a matching Fallback Strategies bullet - AlphaGenome_atlas_scan_interval -- built for exactly the "many candidate SNPs at one locus" case this skill already treats as core (gwas_get_variants_for_trait, fine-mapping) -- had no real usage guidance, just two throwaway mentions in fallback/limitations text. Added proper Phase 0.5 coverage and a 4th worked example (triage a GWAS locus's candidate list by AVI_SCORE before running the expensive annotation phases on all of them) Verified every AlphaGenome_* tool name and parameter signature in the file against the actual registered tool schemas -- all others were already correct. |
||
|
|
862de585d2 |
Weave AlphaGenome/Atlas into the regulatory-variant-analysis pipeline
This skill was purely annotation-database-driven (GWAS, eQTL, ENCODE,
RegulomeDB, OpenTargets) with no sequence-based prediction at all --
a real gap, since AlphaGenome answers exactly the question these
databases can't: does this specific sequence change do anything,
independent of whether it's been annotated before.
Adds two pipeline touchpoints rather than one bolted-on section:
- Phase 0.5 (new, runs by default): an instant Atlas AVI_SCORE lookup
before the annotation phases, since it's a free precomputed read
- Phase 4.5 (new, conditional): escalates to the live model
(score_variant / score_ism_variants / predict_interval) specifically
when annotation came back silent or a mechanism is needed for the
report -- mirroring how the GREGoR Consortium actually solved an
unsolved rare-disease case via a deep intronic DNM1 variant that no
annotation database had anything to say about
Also: a 5th evidence question distinguishing model-based (causal,
mechanistic) from annotation-based (correlational) evidence; a new
Phase 5 synthesis tier ("sequence-model-supported, annotation-silent")
so a high AVI_SCORE with no database hits is no longer misclassified
as "no evidence"; a full worked example; and limitations covering
AlphaGenome's model-estimate caveat and the ISM/scan window caps.
plugins/tooluniverse/skills/ regenerated for this skill only via
scripts/sync-codex-plugin-skills.sh (unrelated pre-existing drift in
other skills reverted, as in the prior skills commit on this branch).
|
||
|
|
af05bdb3c3 |
Add AlphaGenome Atlas support; fix 7 bugs found via live-key testing (#579)
* Add AlphaGenome Atlas support and fix bugs found via live-key testing
Adds 7 new AlphaGenome operations exercising the full alphagenome SDK
surface (excluding pure batch convenience wrappers): predict_variant,
predict_sequence, score_interval, score_ism_variants, output_metadata,
atlas_scan_interval, and atlas_list_scorers.
Every operation (new and pre-existing) was verified against a real
ALPHA_GENOME_API_KEY rather than trusting SDK docs, which surfaced 7
real bugs now fixed:
- Wrong default Atlas scorer name ("AVI" -> "AVI_SCORE")
- score_variant/atlas_lookup_variant silently kept only the first gene
of a multi-gene AnnData and mislabeled tracks with meaningless numeric
indices instead of var["name"]
- predict_interval silently dropped 3 of 11 real output modalities
(PROCAP, SPLICE_JUNCTIONS, SPLICE_SITE_USAGE)
- atlas_scan_interval leaked non-JSON-serializable Variant objects
- atlas_list_scorers crashed on real pandas DataFrame truthiness
("x or []" on a DataFrame raises, doesn't fall back)
- top_n with a non-positive/invalid value returned nearly the entire
dataset via Python's negative-slice semantics instead of a summary
- Atlas scorers echoed back in a different case than the actual
response dict keys, causing lookups by the echoed name to fail
Also corrects two doc-only inaccuracies (incomplete output_type/
output_types value lists, wrong predict_sequence length constants).
Added 22 unit tests (0 previously existed for this tool), each mapped
to a real bug found live, using sys.modules stubbing for the alphagenome
SDK dependency (not installed in CI).
* Update regulatory-genomics and variant-interpretation skills for AlphaGenome Atlas
Both skill files (and their plugin/plugins mirrors) recommended only
score_variant/predict_interval, with no mention of the new Atlas lookup
operations added in this PR. Adds:
- AlphaGenome_atlas_lookup_variant/atlas_scan_interval to the tool
tables, with guidance to check Atlas first for known SNVs (precomputed,
cheaper than a live call) before falling back to the live operations
for indels/custom sequences/full per-track detail
- AVI_SCORE noted as a unified impact score (fuses AlphaGenome +
AlphaMissense), distinct from the Δ effect-size outputs of the other
tools, relevant to variant-interpretation's ACMG PS3/PP3/BP4 guidance
- AlphaGenome_score_ism_variants mentioned in regulatory-genomics for
"which base in this element matters" saturation-mutagenesis questions
plugins/tooluniverse/skills/ regenerated via
scripts/sync-codex-plugin-skills.sh, restricted to only the 3 changed
skills (the script also resyncs unrelated pre-existing drift in other
skills, which is out of scope here and was reverted).
|
||
|
|
780ed6b6c2 |
Cross-reference the Codex and Antigravity plugin skills from setup
setup-tooluniverse's "Path B" one-liner install only ever named tooluniverse-claude-code-plugin, even though tooluniverse-codex-plugin and tooluniverse-antigravity-plugin exist and give Codex/Antigravity users the same one-step "marketplace add + install" experience instead of the generic manual TOML config path this skill otherwise sends them through. Adds the parallel codex plugin marketplace add / codex plugin add one-liner alongside Claude Code's, and points Antigravity users at their own skill (agy plugin install, a different flow, so not worth reproducing inline). Applied to all three packaged copies (skills/, plugin/skills/, plugins/tooluniverse/skills/), matching how this skill is distributed. |
||
|
|
6e14a7ee68 |
Fix #559: retire CLUE.io tools (site shut down, no new API keys)
clue.io's own homepage carries an official retirement banner: "Effective January 31, 2026, the clue.io site, including its tools, will be retired due to financial constraints. As a result, subscriptions will no longer be offered." Confirmed live: the site still loads and the API still returns 401 for unauthenticated requests, but new CLUE_API_KEY registration is closed per that notice, so the 5 CLUE tools are unusable for anyone without a pre-existing key. Follows the same archival pattern used for OxO (#215) and other retired upstream APIs: moved clue_tools.json into broken_apis/ with a clue_rest.json entry documenting the failure and the GEO workaround (clue.io's own notice points there for the underlying public data), commented out the default_config.py entry, dropped the 5 generated tool wrappers and their tools/__init__.py + .tool_metadata.json + tool_page_index.json entries, and regenerated api_keys_catalog.json / .env.template via scripts/gen_api_key_catalog.py to drop the now-dead CLUE_API_KEY entry. Also updated the cell-line-profiling skill (all three packaged copies), which referenced CLUE_get_cell_lines as a live tool. clue_tool.py and its _lazy_registry_static.py entry are left in place, matching the OxO precedent -- harmless dead code once nothing loads its config. Verified live: with a dummy CLUE_API_KEY set, unmodified main still loads all 5 CLUE tools; this branch loads 0 and reports the category no longer exists. Reported in #559. |
||
|
|
bb314a29d5 |
Fix Round 41: drug-name lookups answering with a different drug's label (#517)
* Fix drug-name lookups answering with a different drug's label
FDA_get_boxed_warning_info_by_drug_name {"drug_name":"Albuminex"} returned
BEIZRAY/docetaxel's boxed warning -- toxic deaths, neutropenia, hypersensitivity
-- presented as the boxed warning of a plasma-derived human albumin product.
All 77 FDA_*_by_drug_name tools map drug_name onto the OR-group
(openfda.brand_name, openfda.generic_name, spl_product_data_elements). The third
is the raw product/ingredient blob, so the query matched any product merely
CONTAINING albumin; AND-ing _exists_:boxed_warning then deleted both genuine
Albuminex labels -- which correctly have no boxed warning -- and left only the
wrong drug's. Measured at api.fda.gov/drug/label.json:
spl_product_data_elements:"ALBUMINEX" 3
AND _exists_:boxed_warning 1 (BEIZRAY, docetaxel)
openfda.brand_name:"ALBUMINEX" 2
AND _exists_:boxed_warning 0 (the true answer)
AND _exists_:warnings_and_cautions 2
Drop a blob-matched row when the record's own resolved openFDA names exist and
none contains the queried name: that is an excipient match, not a product match.
Rows whose openfda block is empty are KEPT -- they have no resolved identity to
contradict, and that is how genuine unresolved products appear, which preserves
the denosumab 19->25 recall fix. Sampled live, the rule drops nothing for
denosumab, aspirin, metformin, warfarin, docetaxel, Prolia, Humira, Eliquis or
Gamunex-C, and drops exactly BEIZRAY for Albuminex.
When the filter empties a payload it is turned into openFDA's own NOT_FOUND
shape, so the existing section_exists_clause re-probe still engages: Albuminex
now answers with its own 2 labels, boxed_warning null, and the
warnings_and_cautions those labels do carry.
The disclosure note states only what was done. It does not claim the answer
describes the queried drug, because rows with an unresolved openfda block cannot
be checked -- a minocycline lookup whose surviving rows are all zinc gluconate
lozenges would make that claim false exactly where it matters. It instead
reports how many returned rows could not be checked. The caveat is gated on the
guard having actually dropped something, since an unresolved block is the normal
shape for 6 of the 25 real denosumab labels and warning on every clean lookup
would train the reader to skip the note.
* Fix four over-claims in the excipient-guard disclosure
An adversarial review of the previous commit found four assertions the response
does not support. The guard's filtering logic is unchanged -- it loses no recall
across 30+ drug names sampled live -- but what it TOLD the caller was wrong.
meta.total was page-local while the note asserted it had been corrected:
'albumin human' at limit=10 reported 37 when the true post-filter total is 16,
a 2.3x overstatement, and the page-local caveat existed only in a source comment.
The note now says "reduced accordingly, to N" only when the whole result set fits
on one page; otherwise it declares the number an UPPER BOUND and quotes the
upstream hit count and the page size so the reader can audit it.
dedup_note claimed meta.total was "openFDA's upstream hit count before local
processing" in the very responses where the guard had changed it -- two
contradictory statements in one payload. It now names the reduction and the raw
upstream count.
The note named spl_product_data_elements as the sole match route and told the
reader to inspect it. On broadened queries that is false: for minocycline the
term appears in no returned row's blob -- those rows matched
indications_and_usage/description. The note now names the fields actually
searched.
The unchecked-row count was taken before the user limit was applied, so
"N of the returned label(s)" could report 12 when 3 were returned. It is now
counted over the rows actually returned.
Seven tests cover the disclosure itself, which had none -- which is why it
regressed once already. A paged fake transport makes the page-local case
observable; the previous fake made the total correction exact by construction,
so no test could have caught it.
* Fix Round 42: FAERS counts that described a page the response no longer showed
FAERS_count_reactions_by_drug_event applies `reactionmeddraverse`
client-side: openFDA ranks every reaction term on the matching reports, then
the tool keeps only the row whose term equals the one asked for.
`stratified_report_count` -- documented as "what the counts in 'results' sum
to" -- was summed from the ranking page BEFORE that narrowing, so it
described neither the row shown nor the whole facet, and it moved with
`limit` while `results` did not.
Measured live on the anonymous tier. Three round-42 personas hit this
independently, on four drug/reaction pairs:
PROMETHAZINE + somnolence: `results` is the single row SOMNOLENCE 1,438 at
every limit, while stratified_report_count read 1,438 / 2,630 / 6,492 at
limit 1 / 5 / 50.
MODAFINIL + tachycardia, limit 20: the single row is 200 against 322
matching reports -- BELOW the total -- yet coverage_note read "946 --
293.8% of it, i.e. MORE than the number of matching reports", telling a
reader to discount a near-1:1 signal as threefold double-counted.
LISDEXAMFETAMINE + Suicidal ideation, patientagegroup Adolescent: the rows
sum to 8 against 8 matching reports, reported as 35 and "437.5% of it" --
a 35 standing next to an adolescent suicidality signal whose count is 8.
Sum `results` instead of the raw page, so the documented meaning holds by
construction, and give the narrowed regime its own prose: one kept term
cannot double-count against rows that are no longer present, so the
percentage is meaningful there -- a close upper bound on the share of
reports affected, since openFDA counts recorded values rather than reports.
When the narrowing keeps nothing but reports still matched, say so and name
both causes: outside the top-`limit` slice, or matched inside longer
preferred terms (measured: MODAFINIL + "arrest", 176 reports, no preferred
term IS "arrest" -- they are CARDIAC ARREST, RESPIRATORY ARREST).
CONTRACT CHANGE, narrowed path only: stratified_report_count's value moves
to what its own schema says it is. The unnarrowed path is untouched and is
pinned by a regression test that fails if the page sum is reinstated.
Also, additive: FAERS_count_drugs_by_drug_event was the only tool of the 18
in its family with no `medicinalproduct` filter, so it could only ever
return the database-wide top-N drug ranking -- 20,692,690 reports and HUMIRA
at the top whatever the caller wanted, and no way to ask the co-reported
medication question its name implies. With the filter, INTRALIPID returns
INTRALIPID 720, INTRALIPID 10% 119, TPN 82, TEDUGLUTIDE 77, HEPARIN 73
against 961 matching reports, matching openFDA queried directly; a drug that
does not exist returns no rows and a zero total instead of a global ranking.
* Fix Round 42: gene-disease concordance counted sources that never answered
gather_gene_disease_associations queries five databases and reports, on every
association row, `concordance` (how many named the entity) beside
`total_sources_queried: 5`. That denominator counts every source attempted,
including ones that failed and ones that structurally cannot contribute, so
agreement reads as disagreement. Measured for {"gene": "CFH"} with no API
keys configured: DisGeNET and OMIM fail on missing keys and ClinVar carries
no disease name in the gene->disease direction, leaving two sources able to
answer at all -- yet the top row reports "concordance 2, total_sources_
queried 5", and BRCA1's "breast cancer" reports 1 of 5.
Two caps were silent on top of that. `per_source_results` was sliced to ten
rows per source with no flag while `num_associations` counted the full table,
so the same CFH response showed "OpenTargets: 10" against "num_associations:
29" with nothing saying OpenTargets had actually returned 25; `associations`
was sliced to fifty the same way.
All additive -- no existing field changes value or meaning:
- each row gains `total_sources_with_data`, the number of sources that
contributed anything, which is the denominator to divide `concordance`
by; `total_sources_queried` keeps its value and its meaning and now says
in the schema that it is not that denominator
- `per_source_result_counts` gives the untruncated count per source
- `truncated` follows the repo-wide truncation convention for the 50-row
association cap
- `notes` gains a line per disclosure, and only when it applies: no note
when nothing was cut, none when every source contributed
One definition of "contributed data" (`_sources_with_data`) now feeds both
the per-row denominator and the note that explains it, so they cannot drift.
Corroborated across four rounds before this one.
* Fix Round 42: pin the gene-disease disclosures, and exercise the new FAERS filter
Validation of the two commits above found the gene-disease change shipped
without tests: nothing under tests/ mentioned total_sources_with_data,
per_source_result_counts or truncated for this tool, and none of its three
test_examples reaches the 50-row association cap (live: 29 / 41 / 30), so the
truncation branch was unreachable from anything shipped. The FAERS commit had
122 lines of regression tests; this one had none.
Eight tests, all pure functions -- no network, no ToolUniverse construction:
- the honest denominator rides on every row (5 queried, 2 able to answer for
the live CFH case) and concordance never exceeds it
- a source that answers nothing is not counted as disagreeing, and when all
five answer the denominator moves to 5
- each cap names what it cut ("10 of 25" for OpenTargets; 63 against the
50-row cap), and a source sitting exactly at the cap is not listed as cut
- the denominator caveat fires only when the denominator misleads, and a
response with nothing cut and nothing skewed carries no notes at all
- the two fields dropped during review (sources_with_data,
associations_returned) stay dropped, so neither creeps back as a second
way to say what the response already carries
Each was checked against a deliberately reverted build: restoring the
all-sources-queried denominator fails the row test, and silencing
_disclosure_notes fails the three note tests.
Also: FAERS_count_drugs_by_drug_event's only test_example omitted
medicinalproduct, so `tu test` exercised just the database-wide ranking the
new description tells callers not to rely on. Added the filtered example that
the description quotes; both examples pass.
* Fix Round 43: waits that stop early and then report the shortfall as a defect
Thread.join(timeout=...) returns the same thing whether the thread finished
or the wait expired. Four test sites joined with a timeout and then asserted
on the results they had collected, so a slow thread under CI load produced
"Expected 3 results, got 2" -- reporting broken concurrency when the truth
was that we stopped waiting. test_tool_finder_edge_cases::test_concurrent_calls
failed CI on #517 this way and passed on re-run with no code change.
Add tests/helpers/threads.py::join_all: one shared deadline across the set,
then an is_alive() sweep, so a timeout is always reported as a timeout and
names the threads still running. Converted all four sites
(test_tool_finder_edge_cases, test_critical_error_handling,
test_mcp_unit_functionality, test_performance_benchmarks). The budget lives
in one place and is chosen to stay under pytest.ini's per-test timeout = 60,
which the old per-thread budgets could exceed -- 5 x 30s let pytest-timeout
kill the test with its own opaque message first. Sweeping the rest of the
tree found no other bounded wait followed by an assertion; the src/ sites
(smolagent_tool, result_cache_manager, vcf_stats_tool) already check
is_alive() or catch TimeoutExpired.
pytest.ini gains `tests` on pythonpath so `helpers` is importable by
declaration rather than as a byproduct of conftest loading (it otherwise
breaks under --import-mode=importlib), and the four files keep an explicit
sys.path bootstrap so their __main__ blocks still run.
The same shape in tool code: ClinGen_search_actionability fans the Adult and
Pediatric contexts across a thread pool and swallowed a failed context with
`pass`, leaving its list empty. The tool then answered status "success" with
pediatric_count 0 -- indistinguishable from "ClinGen has curated no pediatric
actionability for this gene". Confirmed live: BRCA1 has 6 adult and 1
pediatric curation, but with the Pediatric fetch failing the tool still
answered success/0 with nothing marking the figure as unmeasured. Failed
contexts are now recorded in failed_contexts with a note saying the 0 means
"not retrieved"; when every context fails the tool returns an error rather
than two fabricated zeroes, matching proteins_api_tool's existing policy.
The healthy path is unchanged in keys and values.
* Fix Round 44: an indication inflating a comparator's adverse-event count
FAERS_compare_drugs matched reactions against openFDA's ANALYSED
`patient.reaction.reactionmeddrapt`, whose tokenizer splits on whitespace
and hyphens, so a search for one Preferred Term also matched every compound
term containing it. Measured live 2026-08-12 for "Thrombocytopenia":
heparin 6,805 -> 2,778 on `.exact` (2.45x), bivalirudin 111 -> 27 (4.11x).
The inflation is DIFFERENTIAL, which makes it a wrong answer rather than a
conservative one: `_compare_drugs` divides the two arms' RORs, so a factor
that differs per drug moves the verdict. The contaminating term is
HEPARIN-INDUCED THROMBOCYTOPENIA (3,968 reports) -- bivalirudin's
INDICATION, not its adverse effect -- so the comparator was inflated by
reports where the drug was the response to the event. "heparin and
bivalirudin show similar-strength detected signals" becomes "heparin's is
stronger" (ROR 7.393 vs 4.563) once each arm counts only the term it claims.
Every reaction filter in the module now routes through `_faers_search_query`
so no operation can search a different variant than its siblings. `.exact`
is case-insensitive at openFDA (verified across four capitalisations), so
callers keep working; a string that is not a Preferred Term now returns a
named error listing real terms to retry with, instead of silently counting a
union of terms.
Also in FAERS_compare_drugs: a three-drug list returned status "success", a
confident two-drug verdict and comparison_caveat null, with the third drug
absent from the response. It is now refused, since the analysis is pairwise
by construction.
openFDA label tools:
- A NOT_FOUND blamed `requested_return_fields[0]`, so a boxed-warning search
was explained as "This label section ('openfda.brand_name') is absent" --
naming a field never searched -- and recommended falling back to
`spl_product_data_elements`, the ingredient blob that answers "propylene
glycol" with rabeprazole, glipizide and urea PK. Both corrected; the real
cause, contiguous-phrase matching, is now stated.
- The `_exists_` guard defaults to a tool's RETURN fields, silently dropping
labels that match the search but lack a resolved openfda block
(adverse_reactions:"keratoacanthoma" 4 -> 2, the dropped one muromonab-CD3,
a different drug class). Now disclosed, gated so section tools stay quiet.
- Keyword trimming was skipped for three hard-coded keys and applied to
everything else, including identifiers: `id` and `set_id` came back as ""
because a UUID contains no matching sentence. Replaced with a named
NEVER_KEYWORD_TRIMMED set.
Regression introduced by this change, found and closed before shipping:
moving the three sibling operations to `.exact` turned a loose match into a
raw "404 Client Error ... for url: <full openFDA URL>". All four
reaction-filtered operations now distinguish "not a Preferred Term" from
"no reports for this pair".
That 404 exposed a PRE-EXISTING leak this round also closes, rather than
having caused it: all four RequestException handlers returned `str(e)`, and
because `_with_api_key` appends `&api_key=` to the same URL that requests
quotes back, a configured FDA_API_KEY appeared in the returned error string.
Reproduced with a key set, then closed by stripping at " for url:".
* Fix Round 45: searches answering with a different entity, and probes that under-measure
Three defects, each verified against the upstream API before and after.
proteins_api_search returned an unrelated human protein for a non-human gene
symbol. `{"query": "ben-1"}` returned BANP_HUMAN. ben-1 is the C. elegans
beta-tubulin whose loss confers benzimidazole resistance. Upstream:
gene=ben-1&taxid=9606 is empty, gene=ben-1 returns Q18817_CAEEL, and
protein=ben-1&taxid=9606 returns BANP_HUMAN because the free-text search
matches "BEN domain-containing protein 1". The empty human gene search fell
straight to the free-text field while keeping the human bias, so a different
entity's annotations came back as the requested protein, with no note. The
retry now tries the same field with only the tool's own human default dropped,
accepted when a hit's gene symbol is exactly the query -- which is what keeps
a genuine protein-name query ("insulin" -> "Insulin-like Peptide 1") from
being dragged off. blaKPC, blaCTX-M-15, insulin, BRCA1 and accession routing
are unchanged.
CIViC returned pages with no denominator. Every connection asked GraphQL for
nodes only, so a caller could not tell a complete answer from a first page.
civic_search_evidence_items for melanoma returned 100 records; the true count
is 218. totalCount and pageInfo are now selected across all 10 connection
tools. The count reported matches the tool's own status:ACCEPTED filter (218),
not the wider unfiltered 370. civic_get_variants_by_gene paginates in Python
and rebuilt the block without the count; it now carries totalCount through.
Monarch phenotype tools were nondeterministic, incomplete, and reported an
upstream rejection as success. The multi-phenotype intersection sliced an
unordered set, so three identical calls for HP:0002524 gave three different
differentials and Niemann-Pick type C appeared or vanished by hash seed; it
now intersects in upstream rank order. Each leg took one hard-coded page of
500 and intersected it as the phenotype's whole disease set -- HP:0001250
(Seizure) has 5331 associations, so Seizure + Cataplexy returned one disease
and omitted Niemann-Pick C1/C2, which carry both; legs are now paged, and a
leg still short of its total is disclosed instead of silently returned. The
x3 over-fetch in get_HPO_ID_by_phenotype pushed any limit above 166 past
Monarch's ceiling of 500, and the resulting HTTP 422 body was returned as
status:success quoting an "input" of 501 the caller never supplied; the
over-fetch is clamped and an upstream validation body is now an error.
Each test was confirmed to fail with its fix reverted.
* Fix Round 45: stop echoing an internal over-fetch value back to the caller
The Monarch validation-error path put the raw upstream body into the error
details. That body quotes the value actually sent, and this tool inflates the
caller's limit by 3 before sending it -- so a caller who asked for 167 was
handed back "input": "501", a number they never supplied, presented as their
own argument. Return only the upstream messages.
Found by running the adversarial leak check over the new error path rather
than by any query; no credential is exposed, but nothing the caller did not
supply should come back to them as though they had.
* Fix Round 45: close four defects the round's own changes introduced
A validation pass over the round's earlier commits found four regressions I
had shipped. Each is reproduced, fixed, and covered by a test that fails when
the fix is reverted.
The multi-phenotype intersection returned {diseases, total_matched} without a
'warning' whenever 'limit' truncated it, but the object branch of its own
return_schema listed 'warning' as required and never declared 'total_matched'
-- so the tool violated its contract on its own shipped test_example, in both
directions at once. 'total_matched' is now declared, 'warning' is required
only where it is actually emitted, and the live response validates.
'offset' became a silent no-op on that tool. The new per-leg pager assigns its
own offset into the leg query and overwrote the caller's, so offset=0 and
offset=1 returned byte-identical lists for a parameter still advertised as
"Number of initial entries to skip" and used by the shipped test_examples.
The caller's offset now pages the intersection, which is what it documents.
Two of the ten new CIViC totalCounts were denominators that ignore the query.
browseDiseases/browseTherapies report the whole browse table regardless of the
filter -- 653 therapies for "imatinib", for "zzzzznotatherapy", and for no
filter at all -- so a search matching nothing published 653 under a schema
description telling the caller to divide by it. That is worse than no
denominator, and it was newly introduced. Those two connections now select
pageInfo only, which is filter-aware; the eight filter-aware totals stay.
Clamping the over-fetch turned a loud failure into a quiet short answer:
get_HPO_ID_by_phenotype answered limit=600 with 490 items under
status:"success" and no note, while Monarch_search_gene -- same class, no
result_id_prefix, so no clamp -- errored on the same request. A limit this
tool cannot serve is now an error on both, negative and zero limits are
rejected before reaching the wire, and a page left short by namespace
filtering says so.
Also: civic_get_variant emitted molecularProfiles.totalCount without declaring
it; the widening gate's docstring claimed "gene symbol" while the code also
matches synonyms and locus names; the phenotype intersection kept paging legs
after it was already empty, costing up to 20 upstream requests each.
Two tests were true by construction and have been rewritten to assert the
query on the wire rather than a fixture that already contained the answer.
* Fix Round 46: filters ChEMBL never applied, returned as full-table successes
ChEMBLRESTTool sent every leftover argument to ChEMBL as a query parameter.
ChEMBL ignores parameters it does not recognise and answers with the whole
resource, so an unsupported filter came back as status:success over a table
answering a different question. Verified live against /target.json: no filter,
?species=Schistosoma+mansoni and ?bananaparam=x all return the identical
total_count of 18552 with Homo sapiens rows on top, while the real parameter
?organism=Schistosoma+mansoni returns 12. A search for schistosome drug targets
was answered with 18,552 mostly-human ones and told it was a success.
The check now consults ChEMBL's own published filter list per resource
(/<resource>/schema.json, key "filtering") rather than the tool config, because
the two disagree in both directions and each direction was live. assay_organism
is a real /assay.json filter that ChEMBL_search_assays does not declare -- a
config-only guard rejected 591 genuine Leishmania major assays, which a persona
caught mid-round. organism__icontains looks like ChEMBL filter syntax, but
organism is not an /assay.json field, so ChEMBL drops it exactly like
bananaparam and returns all 1,970,438 assays; trusting the __ suffix would have
let the defect back in through the escape hatch the error message itself
recommends.
The built query is checked rather than the caller's arguments, so rewrites are
covered too. That found five more instances of the same defect shipped in the
tools' own configs, each confirmed by comparing filtered and unfiltered
responses upstream:
/binding_site.json?target_chembl_id__exact=CHEMBL2074 same row as no filter
/protein_classification.json?target_chembl_id__exact=. total_count 905 either way
/activity.json?compound_record_id__exact=1 activity_id 31863, as unfiltered
/document.json?document_id=1 byte-identical to no filter
/compound_structural_alert.json?alert_name__contains=. alert_id 1, as unfiltered
compound_record_id is now sent as record_id, the column /activity.json actually
has (?record_id=1 returns activity_id 57025) -- the tool's own *required*
parameter had been returning the whole activity table. document_id becomes
document_chembl_id and alert_name__contains becomes alert__alert_name__icontains
(394,234 rows of 5,020,133, versus no filtering at all). binding_site and
protein_classification cannot be filtered by target at all, so the parameter
promising it is gone and the descriptions name the tool to use instead.
Round 45's fix in this area was incomplete: ChEMBL_get_drug_mechanisms mapped
drug_chembl_id__exact to the correct parent_molecule_chembl_id but left the
alias in the arguments it rebuilt from, so the request still carried a parameter
ChEMBL discards. The alias is now dropped.
CIViC's variant pager had no page budget. Its only exits were "hasNextPage is
false" and "no endCursor", both the server's to give, so a zero-node page with
hasNextPage true never grew the list and a repeated cursor never advanced --
either one spins forever inside a tool the harness itself calls. The budget is
derived from the caller's own limit, so it cannot truncate a request that is
progressing; BRAF still returns all 114 variants and TP53 all 273.
CKD-EPI called an impossible eGFR normal. creatinine 0.01 mg/dL in an
82-year-old returned "eGFR 240.2 -- CKD stage G1 (normal, >=90)"; 240 is above
any human value including pregnancy hyperfiltration, so that label is wrong
rather than unqualified. A negative creatinine answered with the Python
internals of the resulting complex number, naming neither the parameter nor the
constraint. And creatinine 0.3 at age 82 -- the textbook low-muscle-mass case
where creatinine-based eGFR overestimates GFR, the direction that overdoses
renally-cleared drugs -- came back "normal" with nothing said. The score is
unchanged in every case; values above 150 are now reported as not stageable, and
inputs outside the equation's validated range carry a declared caveats list.
Each test was confirmed to fail with its fix reverted, per assertion rather than
per file: 5 of 10 ChEMBL assertions fail with the guard and the record_id
mapping disabled, and the remaining 5 are controls that must pass either way.
Two pre-existing ChEMBL tests began making live requests because the schema
lookup bypasses the request layer they mock; both are now offline, verified with
a socket guard reporting 0 connection attempts.
* Fix Round 46: close what validation found in the round's own changes
A validation pass over the round-46 commit found three defects. One is the same
class the commit set out to close, left half-closed by it.
ChEMBL_get_drug_mechanisms reads two `__exact` aliases as the drug ID and the
commit dropped only one of them after mapping. `molecule_chembl_id__exact` IS a
real /mechanism filter, so the new guard cannot see it, and it survived as a
second AND filter beside the parent lookup -- silently removing the mechanisms
recorded against other salt forms. Same drug, same intent, both status:success:
CHEMBL20 drug_chembl_id -> 8 mechanisms
CHEMBL20 molecule_chembl_id__exact -> 4 mechanisms
Confirmed upstream: parent_molecule_chembl_id=CHEMBL1382 returns 2 rows, and
adding molecule_chembl_id__exact=CHEMBL1382 returns 0. All five accepted
spellings now agree, verified across CHEMBL20 (8/8/8/8/8), CHEMBL1201354 and
CHEMBL1382 (2/2/2/2/2) rather than on the one drug the report named.
CKD-EPI applied its own new principle to creatinine but not to age. An
impossible creatinine became an error and an impossible eGFR stopped being
called normal, yet age=-5 still returned "eGFR 96.6 -- CKD stage G1 (normal)",
qualified by a caveat about paediatric validity that is a category error for a
negative number. A non-positive age is now rejected the same way.
The creatinine caveats were one-sided. Below 0.2 mg/dL the response said the
value is under the assay's reporting limit; 1000 mg/dL returned "CKD stage G5
(kidney failure)" with nothing said at all, so the same silence remained in the
direction a unit mix-up produces. Values above 20 now say so and name the
umol/L conversion.
Also: `ruff format` on the files this round touched, which pre-commit would
otherwise have rewritten.
Both new assertions were confirmed to fail with their fix reverted. Full unit
suite: 5058 passed, 27 skipped, 0 failed; src/tooluniverse/tools/ and the lazy
registry stayed clean.
* Fix Round 47: failed calls reported as empty answers, and filters that matched the wrong disease
Five fixes, all reproduced against the live upstream APIs before being changed.
Fix-47-1 / Fix-47-5 (openfda_tool, 157 tools): search_openfda handled
NOT_FOUND richly and let every OTHER upstream error fall off the end of the
branch as a bare None, which the CLI renders as {"result": null}. Confirmed
live against api.fda.gov/drug/label.json: one undeclared query parameter is
answered by openFDA with HTTP 400 {"code":"BAD_REQUEST","message":"Invalid
parameter: section"}, and FDA_get_boxed_warning_info_by_drug_name
{"drug_name":"tramadol","section":"boxed"} returned {"result": null} --
indistinguishable from "tramadol has no boxed warning", for a drug whose
label carries one. openFDA reports rate limiting (its anonymous tier is
~40 req/min), server errors and invalid keys the same way. The upstream error
is now returned in the same envelope the NOT_FOUND branch already used, so no
consumer sees a new shape. The three requests.get calls also carried no
timeout and decoded with a bare .json(), so openFDA's HTML error pages
surfaced as "Validation error: Expecting value: line 1 column 1" with
retriable=false; they now share one helper that adds a timeout and reports
transport failures as transport failures.
Fix-47-4 (tool_discovery_tools, compose_tool): the same artifact reached
callers through the dispatch wrappers independently of openFDA -- any tool
returning None became {"result": null} with exit status 0. execute_tool
dispatches through ExecuteToolTool.run; ComposeTool._call_tool is the second
wrapper, used by composition scripts. Both now report a failure as a failure,
through one shared invariant in base_tool.
Fix-47-2 (panelapp_tool): _word_matches bounded only how much was trimmed off
the SHORTER word to reach the shared prefix, and never bounded what was left
dangling off the LONGER one, so any short-ish query became a prefix search
with three characters of slack. Measured against the live 433-panel list,
search="hernia" returned 15 panels, every one a "Hereditary ..." panel with no
hernia content, under a count of 15; search="myopia" returned 3 myopathy
panels. Also matched: sarcoma/sarcoidosis, anaemia/anaesthesia,
neuropathy/{neuronal,neuron,neural}, disease/distal. Bounding both ends
rejects all of them while leaving plurals and the medical adjectival and
-osis/-otic forms untouched. Tokenization now strips punctuation, so a term
followed by a comma or inside a hyphenated compound is still matched.
Fix-47-3 (ncbi_datasets_tool): the species parameter is named `taxon`, but the
tool's own description asks for an "organism" and the sibling species-scoped
tool calls it that. A caller following either had the value dropped and the
lookup fell back to human, then echoed query_taxon: "human" as if that filter
had been applied: {"symbol":"NR2F2","organism":"mouse"} returned human NR2F2
(gene_id 7026, tax_id 9606), and organism:"Vulcan" returned the same human
gene just as confidently. Both spellings are now accepted and declared in the
schema, and an applied default is disclosed as query_taxon_defaulted.
* Fix Round 47: close what validation found in the round's own changes
Three defects found by validating round 47's own changes, plus one piece of
dead code the validation exposed.
1. An overstated claim. _word_matches' docstring said the surviving matches
were "unchanged, verified against the live list". They are not. Measured
over 214 queries against the live 433-panel list, the new bound removes
~198 wrong panel-hits but also loses ~51 real ones, concentrated where
PanelApp's own disease_group vocabulary is a longer derived form:
cardiac/cardiology (15 panels, including Brugada, ARVC and the long/short-QT
panels), immune/immunology (11), muscle/muscular (12),
arrhythmia/arrhythmogenic (6), pigmentation/pigmentary (3),
retinopathy/retinal (1). No threshold separates these from the collisions
the round set out to remove -- cardiac/cardiology needs five dangling
characters and sarcoma/sarcoidosis has six -- so the bound stays and the
cost is now stated instead of denied. The zero-result note names the
remedy, so a caller who searches "cardiac" is told to retry "cardiology"
rather than being left with a bare empty result.
2. A test that passed either way. test_short_words_never_fuzzy_match claimed
to pin the _MIN_WORD_LEN floor while both its inputs were above the floor;
_MIN_WORD_LEN could be changed from 6 to 4 with the whole file still green.
Replaced with pairs that are the right SHAPE to match and are rejected
solely for being too short (renal/renin, colon/colitis), which does fail
when the floor is lowered. The upper end of the suffix bound was also
unpinned; a 3 -> 4 mutation now fails two collision cases.
3. A gained false positive, previously undocumented. Splitting hyphens means
"non-syndromic" yields "syndromic", so search="syndrome" now matches three
panels that are explicitly NON-syndromic. Word-level matching has no notion
of negation and the alternative is losing every hyphenated compound, so
this is recorded as a known cost rather than fixed.
4. Dead code. _MAX_STEM_DROP=3 could not affect any outcome given
_MAX_SUFFIX_LEFT=3: longer - prefix <= N and prefix <= shorter <= longer
together give prefix >= shorter - N, so the trim bound can never reject a
pair the suffix bound accepts. That is why validation could not pin its
upper end, and it means the previous commit's comment crediting it with
holding the -osis/-otic class was wrong -- those pairs pass on the suffix
bound alone. Removed, after checking 298,378 pairs over the live panel
vocabulary and 400,000 synthetic pairs for zero behavioural difference.
The unreachable apostrophe branch of the tokenizer went with it.
* Fix Round 48: restrictions accepted, ignored, and answered as though applied
Four tools returned a confident answer to a question they had not been asked.
FAERS analytics accepted any argument it did not read and returned the
unrestricted result as a success. Measured live 2026-08-13, byte-identical with
and without the filters:
FAERS_calculate_disproportionality {"drug_name": "warfarin",
"adverse_event": "Haemorrhage"} a_drug_and_event = 4313
... plus {"patientagegroup": "6", "concomitant_drug": "aspirin"} = 4313
FAERS_filter_serious_events {"drug_name": "warfarin",
"seriousness_type": "death"} total_serious_events = 17327
... plus {"patientsex": "1", "occurcountry": "US"} = 17327
So a whole-database pharmacovigilance signal was handed to a caller who asked
for an age-stratified or interaction-adjusted one. The spellings are not
invented: this family's own sibling configs teach them -- patientagegroup
appears 60 times in fda_drug_adverse_event_tools.json, whose descriptions read
"all other filters (patientsex, patientagegroup, occurcountry, serious,
seriousnessdeath) are optional". The guard refuses before spending a request
and names the tool that does stratify.
search_clinical_trials reported an upstream refusal as an empty answer.
ClinicalTrials.gov answers {"intervention": "[177Lu]Lu-PSMA-617"} with HTTP 400
"Error parsing query in Other terms: extraneous input '['"; the tool discarded
that and replied "No studies found for the given query parameters", going on to
blame its own phase filter. Dropping the brackets returns 46 studies. The
bracketed form is standard EANM/IUPAC radiopharmaceutical nomenclature and
appears in trial titles this tool itself returns. execute_RESTful_query
collapsed every failure into False and printed the reason to stdout where no
caller could reach it; execute_RESTful_query_detailed returns it instead. The
boolean wrapper is unchanged for its other callers, including the Monarch 422
path that needs a 4xx JSON body handed back parsed.
Clinical calculators scored physiologically impossible inputs. ASCVD with
total_cholesterol=0 answered the raw text "math domain error"; Child-Pugh with
albumin=-1 scored the albumin component in its worst band and returned "Class B
(score 7): significant functional compromise"; HAS-BLED and CURB-65 with
age=-70 returned confident low-risk scores. Fix-R46 closed this for CKD-EPI
from inside that one equation -- the constraint now lives on the shared
_req_number helper, and CKD-EPI's hand-written guards were migrated onto it so
the two cannot drift.
LOINC asked its index for a field name the index does not use. df=SHORT_NAME
returns "" for every code; the index spells it SHORTNAME and returns
"Potassium SerPl-sCnc" for 2823-3. Seven further fields (SYSTEM, SCALE_TYP,
CLASS, STATUS, TIME_ASPCT, METHOD_TYP, COMMON_TEST_RANK) were probed one at a
time and are absent from this index entirely, yet were emitted as empty strings
-- SYSTEM being the field that separates serum potassium from urine potassium.
They are now disclosed, at the shared parse point so all four operations get it.
Two regressions were introduced and closed during the round. Policing arguments
against the declared schema alone refused a valid stratify_by="country" behind
a partial test config, so acceptance is the union of the schema and the module
vocabulary. Moving the CTG search onto the new transport without moving its
own _fetch_total_count probe left that probe reaching the live network under a
test that mocked the search; both now use one seam.
tests/unit: 0 failures. Each new test was confirmed to fail with its fix
reverted -- 7 of 11 FAERS assertions bite, the other 4 are controls that must
pass either way.
* Fix Round 48: close what validation found in the round's own changes
Four defects in the Round 48 changes, found by validating them.
BLOCKER -- execute_RESTful_query stopped handing back a 4xx JSON body. The
round split the transport into a detail-returning function and a boolean
wrapper whose docstring claimed it was "kept exactly as it was". It was not:
the new code consulted status_code after parsing, so a 422 carrying a valid
FastAPI body returned False where it had returned the parsed dict. That killed
the Fix-R16A path in MonarchTool, whose _validation_error_detail turns that
body into "lower your limit, or page with 'offset'" -- instead run() reached
`if "facet_fields" in response` with False and raised
TypeError: argument of type 'bool' is not iterable
across all six shipped Monarch tools. A decodable body is now always returned
whatever the status, and the status code is consulted only when there is no
body to read -- which is the ClinicalTrials.gov case the round was fixing,
whose 400 carries plain text rather than JSON. Both properties were then
confirmed to hold at once. Network-level exceptions propagate again, as they
always had; the round had been swallowing them into a (False, reason) pair.
The two tests that gave false assurance are fixed too. The round's own
test_boolean_wrapper_contract_is_unchanged exercised only 400-with-non-JSON
and 200-dict -- precisely the two cases that had NOT changed -- so it passed
while the contract was broken. It now covers 4xx-with-JSON-body at the real
seam.
FAERS -- the refusal message made a false referral. It told callers "the
FAERS_count_* tools DO filter on concomitant_drug"; concomitant_drug and
reactionmeddraversepa appear in no config under data/ at all. Both are dropped
from the record-filter list and now get the generic refusal. The remaining
eleven names were each checked to appear in a shipped config, since that is
what the sentence claims.
FAERS -- the message also advertised the wrong accepted set. It listed the
union across all six operations, so a disproportionality rejection named
drug1, drugs, stratify_by and seriousness_type as accepted -- arguments that
operation accepts and ignores. The error was advertising the very failure mode
it exists to close. It now names the calling operation's own declared
parameters.
LOINC -- the SHORT_NAME to SHORTNAME rename had been applied to the code but
not to the return_schema, which still declared the field that is never
populated and did not declare fields_unavailable or fields_unavailable_note.
ruff format had drifted on five files; all nine changed files are clean again.
tests/unit: 0 failures.
* Fix Round 49: two LOINC operations that could not succeed but reported success
LOINC_search_forms sent sf=CLASS and then kept only rows whose CLASS contained
"survey"/"panel"/"form". Neither half could ever hold. sf selects which fields
are searched, and the API documents the choices as "text, COMPONENT,
CONSUMER_NAME, RELATEDNAMES2, METHOD_TYP, SHORTNAME, LONG_COMMON_NAME,
SURVEY_QUEST_TEXT, LOINC_NUM" -- CLASS is not among them, so the search matched
nothing:
search?terms=PHQ-9&sf=CLASS -> [0,[],null,[]]
search?terms=PHQ-9 -> 58 hits
and CLASS is separately empty for every code in this index, so the post-filter
would have discarded every row even had rows arrived. All three shipped
test_examples returned count 0 presented as success: the tool's own documented
invocations demonstrated only its failure mode. Restricting with the documented
type parameter instead returns the real instruments -- terms=PHQ-9&type=form
gives the two PHQ-9 panels. A search that now legitimately matches no
instrument says so and points at LOINC_search_tests, so "no instrument has this
name" is distinguishable from a failed query.
LOINC_get_answer_list had the same shape one step further on. It sent
type=answer, which is not an implemented value; unrecognised values are dropped
rather than rejected, so the call degraded into an ordinary keyword search whose
rows were returned as "answer-type codes". Totals were identical for
type=answer and for an invented type=zzzgarbage on every term tried (PHQ-9
58/58, potassium 145/145, hemoglobin 526/526), and the index holds no answer
codes at all (terms=LA6568-5 -> [0,[],null,[]]). Asked for the ABO group answers
it returned the ABO group question. Answer lists are published through
ef=AnswerLists, which does carry them: 883-9 now returns list LL2419-1 with
Group A / B / O / AB, and each row carries the datatype that says whether an
answer list should exist at all. answer_lists_found is reported separately from
count so that matched codes without a list cannot read as lists.
Both changes keep every key these operations already returned; the added keys
are AnswerLists, datatype, answer_lists_found and note. The schemas gain those,
plus the fields_unavailable pair that search_tests and search_forms already
emitted without declaring. Two test_examples that could only ever show the
failure mode are replaced with ones that exercise the working path and the
honest-empty path.
Tests: nine of the ten new assertions were run against the pre-fix class and
fail on it; the tenth is a labelled control that must pass on both. The mapping
of ef values is covered with two rows so that attributing one code's
permissible values to another cannot pass.
* Fix Round 49: impossible inputs scored confidently at the top of the range
Fix Round 48 closed the bottom of every calculator's input range and left the
top open, so the same class survived at the other end. Reproduced through the
CLI before this change:
ClinicalCalc_MELD_Na {"bilirubin":1.0,"creatinine":1.0,"inr":1.0,
"sodium":134000}
-> status success, "MELD-Na 6: low (~1.9% 90-day mortality)"
ClinicalCalc_Child_Pugh {"bilirubin":1.0,"albumin":1000,"inr":1.0}
-> status success, "Class A (score 5): well-compensated disease"
and sodium 5 mmol/L, which is not compatible with life, was clamped into the
MELD-Na formula and reported as a risk band rather than refused -- zero is not
a floor for a concentration, so Fix-R48's must_exceed=0 could not catch it.
The bound belongs to the quantity, not the call site: bilirubin means the same
thing in Child-Pugh and MELD-Na, four of the eight bounded quantities are read
by more than one calculator, and looking the range up by parameter name means a
calculator added later inherits it by naming its input. Each maximum sits an
order of magnitude beyond the most extreme value reported in a living patient
-- 1000 mg/dL for bilirubin against a highest-recorded 80, 200 years for age
against a verified 122 -- and a control case of a genuinely crashing patient
(creatinine 8.0, bilirubin 45, INR 6.5, sodium 118) is asserted to still score,
so the bounds are strict without refusing a real crisis.
Cost: a value beyond the bound now hard-errors instead of being scored. That is
the intent for a unit mix-up, but it is a refusal rather than a wrong answer,
and a future quantity whose real range exceeds these numbers would be refused
too.
creatinine is deliberately excluded. Refusal is the right protection only where
the calculator cannot disclose, and both calculators taking a creatinine
already do: Fix-R46 gave CKD-EPI a caveat naming units, and MELD-Na bounds
creatinine per UNOS and reports the bounded value. Adding a maximum silently
reversed that shipped decision -- round 46's own test is what caught it -- so
the entry was withdrawn and a test now pins the disclosure behaviour of both.
Separately, MELD-Na's components reported a sodium the formula had not used.
The sodium term applies only above a MELD of 11 and applies a value bounded to
[125, 137], but sodium_used named the raw argument, so a patient with sodium
118 was told the score used 118 when it used 125, and a patient below the
threshold was told a sodium was used when none was. Its three siblings already
reported the bounded values they used, and the breakdown exists to make the
score auditable. sodium_used now holds what entered the formula (null when the
term did not apply), sodium_reported holds the argument, and a note explains
any difference.
* Fix Round 49: a disclosure that contradicted the rows it was attached to
Three personas independently hit the same thing: LOINC_search_tests returns
rows reading METHOD_TYP "PhenX", "ISE", "LC/MS/MS" or "Confirm", directly above
a note asserting that METHOD_TYP is "returned empty for every code and their
emptiness carries no information about this code".
Fix-R48 built that list by probing field-by-field against a single code, 2823-3
(serum potassium), and generalised its empty answer to the whole index.
Re-measured over 570 rows drawn from 19 unrelated search terms:
METHOD_TYP 260/570 populated (45.6%)
SYSTEM 0/570
SCALE_TYP 0/570
CLASS 0/570
STATUS 0/570
TIME_ASPCT 0/570
COMMON_TEST_RANK 0/570
The other six survive the wider sample; METHOD_TYP was never absent. It is a
costly field to disclaim -- it is what separates a presumptive immunoassay
screen from a confirmatory LC/MS/MS quantitation -- and the note was telling
callers to disregard it. The disclosure is now also checked against the rows it
is attached to, so no field can be named unavailable while any returned row
carries a value for it. That is the cheap invariant that would have caught this
at runtime rather than three rounds later. And it is no longer emitted at all
on a zero-result response, where it described the fields of no codes.
Also, from review of this round's own two commits:
MELD-Na now discloses every bounded input, not just sodium. Reporting only the
post-clamp value is what let creatinine 900 mg/dL come back as creatinine_used
4.0, status success, with 900 appearing nowhere -- the same defect this round
had just called unacceptable for sodium, left in place for its three siblings.
A single bounded_inputs_note names every input whose supplied value differs
from the value used, so one breakdown no longer carries two disclosure
conventions. The comment excluding creatinine from the bounds table asserted
that MELD-Na "reports the bounded value"; that is now true of what the caller
is actually told, not just of a number in the payload.
The Clinical Tables response parser reads its ef fields off slot 2 rather than
being handed the field list a second time, since naming that list twice at one
call site is a way for ef values to vanish silently, and it adopts the null
hardening its sibling client clinical_tables_tool.py already had in place of a
length guard that could not fire. The sodium parameter description no longer
restates the numeric bound that the table holds. A comment claiming the API
recognises only question/form/panel is corrected: form_and_section is also
accepted, and panel is the inert one.
One assertion added earlier this round could never have failed -- the error
path returns no data key, so "score" not in result["data"] was true by
construction. It now asserts the absence of data itself. Of the five new
assertions here, all five fail against the pre-change class; two of those fail
on the changed signature rather than on behaviour, which is weaker evidence,
and the three that matter fail behaviourally.
* Fix Round 49: close what validation found in this round's own changes
Validation of the three commits above found two Medium defects, both in the
guard those commits introduced, and both invisible to the tests shipped with
them.
The runtime cross-check that refuses to name a field unavailable while a
returned row carries a value for it was enforced in one of two call sites.
`_get_code_details` returns the row rather than the envelope, so it rebuilds
the disclosure itself and passed only the field list. With `results` defaulting
to empty, `not any(...)` is vacuously true and the check never ran: a row
carrying SYSTEM "Ser/Plas", SCALE_TYP "Qn", CLASS "CHEM" and STATUS "ACTIVE"
came back in the same dict as a note asserting all four are empty for every
code. Latent live, since those four really are empty in this index -- but it is
the exact mechanism that produced the METHOD_TYP defect this round set out to
fix, and the comment above the shared parser claimed every operation was
covered.
Worse, the test whose docstring read "The bite: the claim is checked against
the rows, not just asserted" probed with METHOD_TYP -- which the same commit
had just removed from the static list, so the static list excluded it unaided
and the row argument was never consulted. Deleting the cross-check entirely
left all 50 tests green. It is the pattern this harness keeps rediscovering: a
test that covers the case that did not change. It now probes with SYSTEM, which
the static list does name, so only the row check can withhold it; a second test
proves one populated row among several is enough to retract an index-wide
claim; a third covers the details path directly with a row that must have one
field withheld and another still named. Deleting the cross-check, or reverting
the details call site, or making the datatype hint unconditional now each kill
tests -- verified by mutating all three.
Also from that review:
The refusal message rounded the offending value through %g, so sodium
49.999999999 was refused with "must be at least 50, got 50" -- a contradiction,
in the one message these bounds exist to make actionable. The bound is still
formatted that way, being a round number written by hand; the value is not.
The sodium floor was the one bound not an order of magnitude clear of reality.
Survivable hyponatremia is reported into the high 70s mmol/L, so 80 sat on top
of patients who exist. It is now 50, still far above the sodium of 5 that
motivated it.
The 45.6% populated rate quoted for METHOD_TYP did not reproduce; an
independent sample of the same size put it at 26.7%, because which codes a term
matches varies. Both are recorded rather than averaged. Only the zeroes are
load-bearing: these entries claim a field is never populated, and the evidence
for that is that no sample has ever found a value. The test now demands a
measurement exist for any field added to the list.
Finally, the no-answer-list note referred callers to `datatype` for the
explanation. When upstream sends no ef block at all, no row has a datatype, so
the note pointed at something absent. It is appended only when the rows carry
one, and two tests cover the ef shapes that were correct but unexercised: a
slot-2 list shorter than the code list, and a null slot 2.
* Fix Round 50: counts and filters that described a page instead of the query
ClinGen_get_variant_classifications reported `total: 25` for PAH, a gene with
817 expert-curated classifications -- a 32x understatement presented as a
complete success. Re-probing the Evidence Repository API showed two causes,
neither visible from the code alone:
* `classifications` pages at 25 when `matchLimit` is unset, and returns no
total, so `len(items)` counted one page and called it the answer
* `variant` is not an endpoint parameter at all. It is accepted and ignored
-- a deliberately bogus `variant=ZZZNOTAVARIANT` returns the same rows as
sending no filter -- so the request degraded to an unfiltered listing and
the tool narrowed it client-side over whatever page came back
The second cause was the worse one. CAID CA16020993 (PAH c.1315+1G>T) is row
401 of PAH's 817 and is classified Likely Pathogenic by the Phenylketonuria
VCEP, but sat outside the 25-row page, so the tool answered "No variant
classifications found ... Not all genes have active VCEPs" -- a false negative
delivered with a confident and incorrect explanation.
The endpoint does implement `caid`, `variationId` and `hgvs`, each an exact
server-side filter (verified live: all three resolve CA16020993 to one row,
and `hgvs` matches protein-change strings such as p.Arg408Trp). Variant
lookups now name whichever one fits the identifier supplied, so no client-side
narrowing remains. `total` is now the query's count, `returned` the published
count, and the gap between them is disclosed rather than left to be inferred.
PANTHER_ortholog had the same shape in miniature: matchortho returns a list
when several orthologs match, and `mapped[0]` discarded the rest even under
`ortholog_type='O'`, whose own description promises "all orthologs". Human
ABCB1 -> mouse returns Abcb1b and Abcb1a; only Abcb1b was published, losing
half of the P-glycoprotein pair that makes the mouse the standard model for
human ABCB1. `mappings` and `total_mappings` are additive; `mapping` still
holds the first match so existing callers are unaffected.
Verified live before and after against both upstream APIs. Each new assertion
was confirmed to fail with the fix reverted, except the two that deliberately
pin unchanged behaviour.
* Fix Round 50: state the row cap beside the total on every ClinGen listing
Sweeping the rest of the ClinGen family for the shape fixed in the previous
commit found it three more times in the same file. `_get_gene_validity`,
`_get_dosage_sensitivity` and the actionability listing each published
`data: curations[:100]` next to `total: len(curations)` with no flag, no
`returned` count and no way to page.
Confirmed live: `ClinGen_get_gene_validity {}` reported `total: 3659` directly
above exactly 100 rows, on a tool whose description promises a "comprehensive
list". The cap is deliberate -- the gene-validity CSV is thousands of rows --
but saying `3659` beside 100 rows and nothing else leaves the caller to detect
the gap by counting.
All four listings now go through one `_published` helper, so the classification
handler and its three siblings disclose truncation identically instead of three
copies of a slice drifting apart. Each carries a hint naming the parameter that
actually narrows that listing, since a truncation flag with no remedy only
relocates the dead end.
Verified live before and after. Every new assertion was confirmed to fail with
the fix reverted except the one pinning that a narrowed query stays unflagged.
* Fix Round 50: MeSH_get_descriptor answered for records that do not exist
NLM's linked-data server returns HTTP 200 with a bare `{ }` for any well-formed
but unassigned MeSH identifier; only malformed ones 404, and that path was
already handled. The tool mapped the empty body onto its full field layout and
published the result as a success, so `D999999` came back as a complete-looking
record with `label: ""`, `tree_numbers: []` and `active: null`. A descriptor
that does not exist was indistinguishable from one whose label is blank.
Separately, descriptor records carry the name in `label` while Term and Concept
records carry it in `prefLabel`. Reading only `label` discarded a name the API
plainly supplies: `T010724` returns `prefLabel: "Dysphagia"`, and the tool
published `label: ""` while at the same moment reporting `type: "Term"` -- it
had already determined the record was not a descriptor and still reported
success with the name thrown away.
Between them these made an empty `label` ambiguous across three unrelated
situations. Now an empty body is an error naming the identifier and pointing at
MeSH_search_descriptors, `prefLabel` is read as the fallback name, and a record
that resolves to something other than a topical descriptor says so and names
the tool that resolves the descriptor owning it.
Both bodies confirmed live at id.nlm.nih.gov before and after. Topical
descriptors are unchanged, with a test pinning that they gain no note.
* Fix Round 50: say that serious-event reaction rows are not a breakdown of the total
FAERS_filter_serious_events publishes total_serious_events beside
top_serious_reactions, and the two count different things: the total counts
reports, while each reaction row counts reports mentioning one term. A single
report lists several terms -- confirmed live that warfarin death report
10037626 carries four -- so the rows overlap and routinely sum past the total.
The numbers were right; nothing said so. PRUSSIAN BLUE + death publishes
total: 1 above four rows of count 1 with truncated: false, which reads as four
fatal cases rather than one report carrying four reactions. The stratification
operation in this same module already spells the hazard out at length, so the
family disclosed it in one place and not the other, and the arithmetic has been
filed as a defect in the count more than once -- including in the brief for
this round, which is what prompted re-deriving it at the API rather than
changing anything.
Additive: one coverage_note field, no number changes.
* Fix Round 50: close what the quality review found in this round's own changes
Four review passes over the round's diff. Three findings were substantive:
Truncation disclosure used a vocabulary unique to this file. The repo already
publishes `truncated` in 26 files and `truncation_note` in 15, with two shared
implementations (cbioportal_tool._truncation_fields, clinvar_tool.
_truncation_disclosure). The new keys `data_truncated` / `data_truncation_note`
/ `total_is_a_lower_bound` / `total_note` each greped to exactly one file, so a
caller reading truncation generically would have needed a spelling that existed
only here, and one response could carry six keys for one idea. Renamed to the
convention and folded the request-cap case into the same two keys, so
`_published` now reports `truncated` explicitly either way.
PANTHER_gene_info had the same collapse `_ortholog` was fixed for, 180 lines
above it in the same file: `gene_data[0]` under a comment that says "multiple
genes". Confirmed live that gene_id "TP53,BRCA1" echoed both names back while
publishing only BRCA1's PTHR13763 -- one gene's family and GO annotations
served as the answer for another. Publishing one instance of a two-instance
class makes the pattern read as fixed when it is half fixed. `genes` and
`total_genes` are additive; the three top-level fields still describe the first
match.
The empty-result notes pointed at `clinvar_search_variants`; the registered
name is `ClinVar_search_variants`, so the remedy offered to a caller who had
just hit a dead end was itself a dead end.
Also corrected a docstring claiming ~1s response times: measured live, asking
for every row costs 1.5 MB / ~5s for PAH and 8.1 MB / ~15s for RUNX1, the
repository's largest gene, against this tool's 120s timeout. That is the price
of `total` being a count, and the docstring now states it rather than quoting
the old page-sized figure.
Two findings were declined and are recorded in the round report: lifting
`_published` into shared infrastructure (the review measured 42 undisclosed
sites across 32 files -- real, but far past this round's scope), and declaring
`coverage_note` in FAERS_filter_serious_events' return_schema, which is
`{"type": "object"}` with no properties at all, where naming one field and
omitting eight would read as a contract it does not have.
* Fix Round 50: close what validation found in this round's own changes
Three substantive defects, all introduced by this round.
MeSH_get_descriptor's new note was factually false for three record classes.
It fired for anything that was not a TopicalDescriptor and asserted the record
"has no tree numbers or annotation of its own" -- true only of Term and
Concept. PublicationType, GeographicalDescriptor and Qualifier are indexed in
their own right: D016428 returns tree V02.600 and a full annotation, so the
note printed a denial directly above the populated fields contradicting it,
and offered an entry-term remedy to a record no descriptor owns. Worse, the
test file covered only Term -- the single class for which the claim happened
to hold -- so the falsehood was invisible to a green suite. The note is now
written per class, and PublicationType has a fixture.
A whitespace-only `variant` defeated the guard that exists to prevent an
unbounded request. `" "` is truthy, so it satisfied "at least one filter",
then stripped to an empty `hgvs=` and sent exactly the unfiltered listing the
docstring says must never be sent -- measured at a full 120s timeout, with the
cost paid upstream too. Arguments are now normalised before the guard rather
than after it.
PANTHER_gene_info published `gene_symbol`, a key `geneinfo` entries do not
have, so it was null on every row while being newly declared in the schema.
The identifier the caller typed lives in `mapped_id_list`, and PANTHER does
not return rows in input order -- "TP53,BRCA1" comes back BRCA1 first -- so
the previous commit's fix was only half done: it stopped dropping genes but
left rows the caller could not attribute without decoding an accession string.
Rows now carry `input_id`, plus `family_name`/`subfamily_name`, which the
original inline code had also discarded.
Also corrected: the no-result note called the lookup "exact" when `hgvs`
matches on substring; the `variant` parameter description never mentioned that
CAIDs and ClinVar VariationIDs are accepted, which is the headline fix and was
undiscoverable from the schema, and its test_examples exercised only `gene`,
so no example ever ran the identifier routing; PANTHER_ortholog's description
still promised a single ortholog; and MeSH's `note` was undeclared in its
return_schema while this same round declared PANTHER's new keys.
Each of the seven new assertions was confirmed to fail with its fix reverted.
* Fix Round 51: FAERS answering about a different product than the one asked for
Three fixes, all in the wrong-entity seam: a query that matches something other
than the drug named, and reports its data as that drug's.
1. FAERS drug-name counts now disclose how much of the answer is a different
product (additive; 18 count tools).
Every FAERS drug-name lookup searches the union of FAERS_DRUG_NAME_FIELDS,
and two of those three fields are openFDA's SPL annotation rather than the
text the reporter wrote. openFDA tags a drug entry with EVERY brand and
generic name registered for its active ingredient, so a brand-name query
returns every product sharing that ingredient. The union itself is correct
and stays (dropping a field undercounts SEROQUEL and XELJANZ); what was
missing was any statement of what it swept in.
Measured live 2026-08-12 against meta.results.total, "named" =
patient.drug.medicinalproduct alone, "matched" = the union:
IOSAT 6 / 1,395 99.6% named a different product
CYANOKIT 238 / 4,119 94.2%
SUBLOCADE 5,134 / 28,481 82.0%
ADVIL 67,159 / 264,100 74.6%
XGEVA 36,039 / 51,441 29.9%
OZEMPIC 66,161 / 66,161 0.0%
MAKENA 22,729 / 22,729 0.0%
CYANOKIT is the cyanide-poisoning antidote; hydroxocobalamin is also a
routine vitamin B12 supplement, and 3,881 of its 4,119 matched reports are
B12 reports (sampled: safetyreportid 10016092, medicinalproduct
HYDROXOCOBALAMIN, drugindication MICROCYTIC ANAEMIA). Before this,
FAERS_count_reactions_by_drug_event{"medicinalproduct": "CYANOKIT"} answered
ACUTE KIDNEY INJURY 211 against total_reports_matching_query 4,119 with
nothing saying the denominator was mostly vitamin recipients.
The OZEMPIC and MAKENA rows are why the share is measured per call rather
than warned about statically. Responses gain reports_naming_queried_drug,
reports_matched_by_name_resolution_only and, only when the query really did
widen, drug_name_scope_note. All three are declared in the return_schema of
the 18 tools that emit them, since agents plan off the schema.
Cost: one extra openFDA request per drug-name count call (three, not two),
skipped when no drug name was passed, when the name searches a single field,
when the facet is empty, or when the denominator is unavailable. Worst-case
latency rises from 55s to 80s; both probes fail soft.
2. OpenFDA_search_drug_events no longer discards the drug it was given
(contract change; 1 tool).
drug_name and reaction were honoured only when a raw `search` was absent, so
supplying both counted the whole of FAERS. Measured live, {"drug_name": D,
"search": "serious:1", "count": <reaction facet>} returned a byte-identical
payload topped by DEATH 846,360 for "levetiracetam", for "lacosamide" and
for the nonexistent "ZZZ_NOT_A_DRUG_XYZ" -- each "status": "success". They
are now AND-ed, which can only narrow: levetiracetam gives SEIZURE 14,934,
lacosamide 5,969, the nonexistent drug 0. The raw-search-only and
drug-name-only paths are byte-for-byte unchanged.
3. FAERS_search_serious_reports_by_drug can now actually ask for serious
reports (additive; 1 tool).
Despite its name the tool had no `serious` parameter and no seriousness
field in its search map, and returned byte-identical output to its
unfiltered sibling. Measured over the first 100 reports, non-serious was 12%
for acamprosate, 24% levetiracetam, 39% ibuprofen and 79% gadobutrol. The
parameter its five sibling tools already had is added, via the existing
HUMAN_TO_FDA_MAP["serious"] mapping; the default still returns both classes,
so the description, return_schema and skill reference now say so instead of
implying otherwise. The generated SDK wrapper is updated to match, so the
parameter is reachable from the Python API.
* Fix Round 51: close what validation found in this round's own changes
Validation of the round-51 commit found two defects in it and three inaccurate
claims. All five are closed here; the two defects now have regression tests that
fail when the fix alone is reverted.
1. The negative-difference guard published the figure it said it suppressed.
`reports_naming_queried_drug` was written immediately after the scope probe
and was never removed on the bail-out, so a probe disagreement shipped a
subset count LARGER than the superset it is drawn from -- simulated at
named=5,000 against matched=4,119, the envelope carried
reports_naming_queried_drug 5,000 beside total_reports_matching_query 4,119,
contradicting both the comment ("Publish nothing rather than a figure that
cannot be true") and the return_schema. Both keys are now written after the
check; a failed probe still publishes an explicit null, which is a different
statement from "not applicable".
2. `_drug_name_fields`' documented fallback was not real on the parent path.
The accessor promises that a config which forgets its name map still asks
the same question as every other FAERS tool, but only the multi-drug
subclass went through it: the parent read `self.search_fields` directly and
dropped the drug name entirely, while the scope probe -- which keys off
DRUG_PARAMETER rather than off the map -- still applied it. With the map
removed the facet searched `search=` (nothing at all) and the note then
blamed SPL annotation for 899,900 reports. The parent now uses the accessor,
so the promise holds where it is made.
3. `OpenFDADrugEventsTool`'s class docstring and the `OpenFDA_search_drug_events`
tool description both still said the tool accepts a raw `search` string OR
the convenience parameters, thirty lines above the code that AND-s them --
so nothing an agent reads said the combination now works, which was the
point of the fix.
4. Two wording corrections: the disclosure reuses the denominator as its
denominator, not "as its own numerator"; and the return_schema for
`reports_naming_queried_drug` listed two of the four conditions under which
the key is absent, where the runtime docstring lists all four.
tests/unit + tests/integration: 5,641 passed, 36 skipped, 0 failed.
* Fix Round 52: a safety inference computed over a cohort that is 94% a different drug
FAERS_calculate_disproportionality built its 2x2 contingency table from a
drug-name union that also searches openFDA's SPL annotation, which tags a
report with every brand and generic name registered for the active ingredient
of a product the report named. A brand query therefore collects every sibling
product sharing that ingredient, and the tool published an ROR labelled with
the queried brand.
Measured live 2026-08-12/13. CYANOKIT, a cyanide antidote, has 4,119 reports in
that union and 238 under the reported product name; the top names in the union
are HYDROXOCOBALAMIN 3,882, ATORVASTATIN 775, BISOPROLOL 538 -- vitamin B12
supplementation and its patients' polypharmacy. Against DEATH:
union cohort a=50 b=4,069 ROR 0.287 [0.217, 0.379]
reported-name cohort a=12 b=226 ROR 1.24 [0.694, 2.216]
The union interval lies entirely below 1, so the tool told a poison control
physician that death is reported disproportionately LESS often for a cyanide
antidote. Restricted to reports that named CYANOKIT the answer is inconclusive.
Not a shifted number: the opposite verdict, with a confidence interval on it.
The union stays the analysed cohort. The size of the gap cannot distinguish
brand contamination from a generic correctly collecting its own brands --
TOFACITINIB is 93.0% non-naming and CYANOKIT is 94.2%, and the extra reports
are genuine exposure in the first case and a different drug in the second -- so
no rule can pick a cohort automatically. What changes is that the split is now
measured per call and published as `cohort_scope`, with the ROR restricted to
reports that named the drug alongside it when the two differ. Additive: no
existing figure moves; one clause is appended to `note`. FAERS_compare_drugs
carries it on each arm, where it matters more, because the verdict there is a
ratio of two cohorts.
Also in this change:
- The probes are paid for. `_get_faers_count` now asks with `limit=0` instead
of `limit=1`, which returns the same `meta.results.total` without a report
body: one analysis moved 212,426 bytes before and 3,157 after, while going
from 4 requests to 6. It also shares the FAERS count tools' report-total
memo, so a second analysis of the same drug costs 3 requests, not 6.
- That memo is new, and closes a cost the count tools' own disclosure
introduced: a four-tool CYANOKIT workup sent 12 requests for 6 distinct
questions, each probe fetched four times. It now sends 6 and repeats nothing.
Built on the package's own LRUCache, keyed on (endpoint, search) rather than
on the built URL so the two modules' spellings of one question collapse to
one entry and no api_key ends up in a process-global dict key. Successes are
cached with a 300s TTL; failures never are, so one rate-limited moment cannot
stick. Not single-flight: concurrent misses still duplicate, and the 12->6
figure is measured for sequential calls.
- ClinicalTrials.gov reports totalCount only on a first page, and the fallback
`data.get("totalCount") or len(studies)` filled the gap with the page size.
query_cond "silicosis" at page_size 3 reported total_count 20 on page 1 and 3
on page 2; mesothelioma reported 64 then 5. Paging a query to the end left
the caller holding a denominator equal to their last page. Now null plus a
note saying where the real total is, matching the sibling tool. The guard is
`is None`, not falsiness, so a genuine totalCount of 0 survives.
27 tests added, 21 of which fail when the fix they cover is reverted; the other
6 assert that existing behaviour did not move.
* Fix Round 52: close what validation found in this round's own changes
The validator ran the full unit suite, which this round had not, and re-derived
every numeric claim in the previous commit against the live API. It found 20
regressions and one false claim. All are closed here.
20 unit-test regressions, all introduced by the previous commit:
- 13 came from the new report-total memo being a process global with no reset
hook. A test that let a probe succeed handed its answer to a later test that
stubbed the same probe to FAIL, and the assertion under test is usually "a
failed measurement reads as null, not as a number" -- so the leak supplied
exactly the number that hides the bug. test_faers_count_facet_coverage
asserted a null denominator and got 4741, cached by an earlier test in its own
file. The new test file had its own fixture; the six pre-existing FAERS files
had no reason to know the memo exists. The reset is now autouse in
tests/conftest.py.
- 7 came from the limit=1 -> limit=0 switch and the two extra probes breaking
mocks that keyed on the literal "&limit=1" or supplied a response sequence
sized for four probes. Updated in place; the behaviour they cover is unchanged.
tests/unit is now 5351 passed, 0 failed, 0 errors, 28 skipped.
tests/integration is unchanged from before this round: the same 3 failures in
test_mcp_protocol.py and 7 errors in test_smcp_http_server.py, both pre-existing
on the base commit.
The false claim: the previous commit said keying the memo on (endpoint, search)
lets faers_analytics_tool reuse entries the count tools put there. It does not,
for any single-word drug name -- which is every drug in the worked examples. The
two modules spell the same question differently: faers_drug_name_clause always
quotes the drug name, while the count tools' _render_clause quotes only when the
value contains a space. Verified: "SODIUM CHLORIDE" collapses to one entry,
CYANOKIT does not. Normalising the quoting in the key would be wrong rather than
cosmetic, because quoted and unquoted are genuinely different openFDA queries
for a hyphenated name -- a bare hyphen is a Lucene operator, which is why
faers_drug_name_clause quotes at all. So the claim is corrected in all four
places it was made, and what the memo actually buys is stated instead: repeat
calls within a module. A second analysis of the same drug still costs 3 requests
rather than 6, and the count tools' own 12-to-6 saving is unaffected. The
limit=0 change, which is the larger part of the payback, was verified exact:
212,426 bytes before, 3,157 after.
The test that claimed to prove cross-module sharing was hollow -- it seeded the
memo through the analytics module's own key builder, so it could only show the
tool reading back what it had written. Replaced with three tests that assert
what is true, including one that pins the non-sharing so the comments cannot
drift back.
Also from the validator:
- faers_analytics_tool._get_faers_count defaulted a missing meta.results.total
to 0. That was already wrong; memoising it would have made a fabricated zero
sticky for the TTL. It now returns None, routing into the same "count query
failed" path as a transport error.
- FAERS_calculate_disproportionality's return_schema was {"type": "object"}, so
the round's new output object was documented only in prose. cohort_scope and
its members are now declared.
Corrections to the previous commit message: the mesothelioma figure quoted there
as "64 then 5" is not reproducible -- that query returns total_count 604 today.
The silicosis case (20 then 3) is confirmed, as is the underlying behaviour that
ClinicalTrials.gov omits totalCount on a paged response. The test count should
have read 28 added, 22 of which fail on a reverted tree, not 27 and 21.
* Fix Round 53: totals invented from the rows, and disclosures that described a different measurement
Four fixes, all of the same shape: a number or a sentence that was not
measured from what it claimed to describe.
1. ClinicalTrials_get_field_values invented its own facet size.
`uniqueValuesCount` fell back to `len(all_values)` when ClinicalTrials.gov
did not send one, and four lines later `upstream_truncated` was derived
from that fallback -- so the test was `len(all_values) < len(all_values)`,
False for every input, and any field without an upstream count certified
itself complete.
Measured live over the whole /api/v2/stats/fieldValues payload
(2026-08-13): 149 of the 418 fields carry neither `topValues` nor
`uniqueValuesCount` -- 92 INTEGER, 29 DATE, 28 BOOLEAN. All 269 STRING and
ENUM fields carry the count, so the fallback fired only where it was wrong.
Those 149 returned `unique_values_count: 0`, `values: []`,
`truncated: false`: a positive claim that the field has no distinct values.
They also each carried a summary that was parsed past and dropped --
trueCount/falseCount for BOOLEAN, min/max/avg for INTEGER, min/max/formats
for DATE. HasResults is the case that matters: a caller asking how many
registered studies have posted results was handed an empty list, while
upstream held 79,677 true and 518,832 false, summing exactly to the
registry total.
BOOLEAN facets are now materialized from the two counts. INTEGER and DATE
report `unique_values_count: null`, `truncated: true` and a new
`value_summary` carrying the range. The coverage note for those fields
previously asserted, in one sentence, both "leaving 598,509 studies
represented" and "The rows sum to 0."
2. The FAERS cohort disclosure covered two operations out of six.
Round 52 added `cohort_scope` to the two inferential operations. Four
siblings build the identical union cohort from the identical `_drug_clause`
and published nothing: `_stratify_by_demographics`, `_filter_serious_events`,
`_analyze_temporal_trends`, `_rollup_meddra_hierarchy`.
`_analyze_temporal_trends` is the sharpest: it emits a verdict. Live,
CYANOKIT (a cyanide antidote, 4,119 reports of which 238 named it) returned
trend "Increasing", percent_change 19900.0 -- the trend in vitamin B12
supplement reporting, published under the antidote's name.
The split moved into a shared `_cohort_split`; the restricted-ROR arm did
not move with it, since there is no restricted form of a stratification.
Each call site supplies prose describing its own output. Cost: one extra
openFDA request per operation for stratify and rollup, two for serious
events and temporal trends, all `limit=0` and memoised per process.
3. PubMed read one of NCBI's two disclosure containers.
esearch reports dropped terms in `warninglist` when the search ends with
zero hits and in `errorlist` when terms were dropped but hits REMAIN. Only
`warninglist` was read, so the disclosure fired exactly when the caller
could not be misled and stayed silent exactly when they could. Measured
live: "benzene hematotoxicity NQO1 GSTT1 rs180056600" returns 4 articles
with errorlist.phrasesnotfound ["rs180056600"] and a null warninglist;
"zzzqqqxyz toluene diisocyanate nonexistentterm12345" returns 1,477 with
two thirds of the query discarded. Both now report `terms_not_found`; an
unrecognised [field] tag reports `search_fields_not_found`.
4. Two ClinicalTrials.gov disclosures described a different measurement.
`relaxed_match_check` counts a probe built from the original wording plus
every other filter, then published only the rewritten parameters as
`relaxed_query`. Live, query_intr="levetiracetam" +
query_term="subcutaneous palliative" published `{"query.intr":
"levetiracetam"}` beside a 0 and the words "a genuine absence of matching
trials" -- while that displayed query matches 253 studies on its own. It
now publishes the query that was counted.
The coverage note's "The rows sum to 481,189" is a statement about the
whole facet; with page_size=2 the reader sees two rows summing to 324,064.
The sentence now says which rows it means.
* Fix Round 53 (validation): correct a false cost claim, remove a redundant request, harden three null paths
Applied from this round's own quality review of its first commit.
A false claim in the round's own code. `_cohort_split`'s docstring said "ONE
extra openFDA request per operation". Only `_stratify_by_demographics` pays
one. `_rollup_meddra_hierarchy` and `_filter_serious_events` pay two, because
a `count=` response carries no meta.results.total and the serious-filtered
total measures a different cohort. The docstring now gives the count per
operation, with the reason each needs what it needs.
A second inverted claim, same commit. The comment at `_filter_serious_events`
said probing the serious subset "would cost a request to answer the same
question". It would cost ONE added request (the numerator; `total_serious` is
already its denominator) against the two spent here. The union pair is bought
for memo reuse across sibling operations, not because it is cheaper, and the
comment now says that.
A redundant request removed. `_analyze_temporal_trends` fetched a union total
it already held: the `count=receivedate` facet is unpaged and complete, so
`sum(yearly_counts.values())` is both free and the more faithful denominator --
it is exactly the reports the trend was computed over. Measured live
2026-08-13 that the two agree, because `receivedate` is populated on every
report: CYANOKIT 4,119/4,119, IVERMECTIN 6,367/6,367, RIFAPENTINE 521/521,
OZEMPIC 66,161/66,161, HYDROMORPHONE 130,429/130,429. Net cost of Fix-R53-2
is now +1 request for stratify and temporal, +2 for serious events and rollup.
A regression this round introduced, found by the full unit run rather than by
the targeted one. `test_faers_rollup_pt_truncation` pins the request budget of
`_filter_serious_events` at 2; the disclosure makes it 4. The assertion is
updated to 4 rather than deleted -- it is the guard that catches a fifth
request appearing by accident -- and now also checks that three of the four
are `limit=0` probes carrying no report bodies.
Three null paths hardened, none of them reachable today, all of them resting
on invariants that spanned two functions:
- the coverage note's no-ranking branch is guarded on `unique_values_count
is None` rather than on `value_summary`, so a BOOLEAN missing its
trueCount/falseCount cannot fall through into the completeness prose that
would tell the caller zero rows "partition the studies that record it";
- sections 3 and 4 of that note state their own `isinstance` precondition
instead of relying on the early return above them to have fired;
- the falsy-denominator guard moved from four call sites into
`_cohort_split`, which also removes a divergence the four had already
drifted into -- three emitted `cohort_scope: null`, one omitted the key.
`cohort_scope` is now declared in `return_schema` for the three operations
that emitted it with no contract entry at all, following the precedent set by
FAERS_calculate_disproportionality of declaring the one key that matters.
Also drops a redundant early return in `_search_warning_metadata` that the
existing empty check below it already subsumes.
* Fix Round 53 (validation 2): close four contradictions validation found, three of them this round's own
Behaviour is unchanged except where noted; these are corrections to claims.
A no-op rewrite, and a comment that was wrong about Python. This round changed
`truncated` from `page_truncated or upstream_truncated` to `True if
page_truncated else upstream_truncated`, justified by "`upstream_truncated` is
tri-state, so `or` would coerce an unknown facet size to False". Python's `or`
returns its second operand when the first is falsy, so `False or None` is
None: the two forms agree on all six inputs, the rewrite was a no-op, and no
test could have distinguished them. Reverted to the original expression, with
a comment stating why it needed no change.
A test whose name asserted something unreachable.
`test_page_size_truncation_still_wins_over_an_unknown_facet_size` used a
fixture carrying `uniqueValuesCount: 3` -- a known size -- and the case its
name described cannot occur at all, since an unknown size implies zero rows
and zero rows cannot be page-truncated. Renamed to what it tests.
The genuinely reachable unknown-size case now has a test, and it earns its
place: a BOOLEAN field arriving without trueCount/falseCount has no rows to
count, no upstream count and no range to fall back on. Reverting the coverage
note's guard to the `value_summary` form this round shipped first sends that
payload into the completeness prose -- "they partition the studies that record
it", "The rows sum to 0" -- which is what widening the guard to
`unique_values_count is None` was for. The widening was load-bearing, not
defensive.
An arithmetic error in the cost disclosure this round wrote to replace an
earlier wrong one. The per-operation table sums to six additions; the sentence
below it said seven. Corrected, and the memo saving is now stated as measured
rather than as a single number: across all six operations on CYANOKIT + DEATH
in one process, 11 requests against 11 unchanged -- the four descriptive
operations add three and disproportionality then drops from six to two on the
probes they already answered. A drug-only workup is +1. Three is the worst
case, not the typical one.
The inverted cost claim corrected in the previous commit was still live in the
test file that commit edited. `test_serious_events_says_which_cohort_it_
measured` still said probing the narrower cohort "would spend a request to
answer the same question", so source and test disagreed about the same
measurement. The test docstring now matches the source: the wider cohort is
chosen for memo reuse, not because it is cheaper.
A live figure quoted from the wrong query. The Fix-53A-1 comment said the old
`relaxed_query` value matches 253 studies on its own; sent verbatim to
/studies it returns 306. 253 is what that query returns after the tool's own
AREA rewrite -- the query the comment is contrasting it against.
* Fix Round 54: NCBI esearch answered a broader query than asked, silently
esearch drops search terms it cannot match and answers the REMAINING query,
returning a full result set for a question the caller never submitted. Round 53
taught PubMed to read NCBI's disclosure of this. Every other eutils database
behaves identically and eleven other modules parsed esearch responses without
reading either disclosure container.
Measured live across fourteen eutils databases, each probe compared against the
same query with the nonsense terms deleted: all fourteen reported both dropped
phrases in errorlist.phrasesnotfound and returned a count IDENTICAL to the
query with those phrases removed -- the dropped terms constrained nothing.
All fourteen stayed silent (errorlist: null) when nothing was dropped.
The parsing now lives in one place, ncbi_eutils_tool.esearch_query_disclosure,
which carries the measurements; pubmed_tool delegates to it. Wired into six
modules covering seven tools. Three of them were worse than silent: they named
a field for the query that ran and put the SUBMITTED query in it --
NCBI_search_nucleotide and NCBI_SRA_search_runs (data.search_term) and
SRA_search_experiments (data.query_used). Additive: the key appears only when
NCBI reported something, and costs no extra request, since the containers are
already in the response every caller parses.
The sweep is not complete and pubmed_tool now says so: clinvar, dbsnp, pmc, geo
and unified_guideline_tools remain unwired, pmc and icite because they return a
bare list with nowhere additive to put the disclosure.
Also, a hyphenated drug name reached openFDA unquoted. _render_clause quoted a
value only when it contained a SPACE, and a hyphen is a Lucene operator, so a
single-token hyphenated name was reparsed into a union of DIFFERENT drugs while
the response reported in prose how many reports "named" the product.
faers_drug_name_clause had always quoted and gave this exact case as its
reason, so the two paths disagreed on the one input each was written about, and
a third comment called the divergence deliberate.
medicinalproduct:sumatriptan-naproxen 124792 unquoted vs 13 quoted
medicinalproduct:CO-TRIMOXAZOLE 24806 unquoted vs 1416 quoted
FAERS_count_reactions_by_drug_event reported 219,048 reports for
sumatriptan-naproxen -- sumatriptan (51,295) OR naproxen (172,094) -- for a
product with 1,438 reports under its real brand name; 852,859 for the
non-existent MANNITOL-SULFATE; and 141 for an invented product, affirming that
141 reports named it. All three now return the correct count, 0 for the latter
two. Found independently by two personas.
Quoting is now the default with the Lucene range as the only carve-out. The
cost was measured over every other value shape these builders send -- drug
names, generic/brand name, single and multi-word reaction terms, all eight
coded HUMAN_TO_FDA_MAP values, occurcountry, numeric age, and the single-date
receivedate form: quoted and unquoted return identical totals for all of them,
so no query changes except the ones that were wrong. The range carve-out was
re-confirmed the same way (197,676 unquoted, HTTP 404 quoted).
faers_drug_name_clause now calls _render_clause instead of restating the rule,
so the drift cannot recur, and the memo comment that called the divergence
deliberate is corrected: both modules now spell the same question identically
and share one cache entry per drug instead of two.
Nine pre-existing assertions pinned the old unquoted query strings; each was
updated, not deleted, and every structural property they guard -- group
parenthesization, AND/OR nesting, encode-once, range-unquoted -- still holds.
* Fix Round 54 (validation): a zero-hit search is not a broadened query
Validation found that `outputmessages` must not gate the disclosure. NCBI
emits `warninglist.outputmessages: ["No items found."]` on EVERY zero-hit
search, including one it executed exactly as submitted. Measured live:
term "cystic fibrosis AND 1500[dp]"
-> count 0, errorlist null,
warninglist {"phrasesignored": [], "quotedphrasesnotfound": [],
"outputmessages": ["No items found."]}
which produced `query_not_executed_as_submitted: True` and the warning "No
items found." The query WAS run as asked; it simply matched nothing, so the
flag asserted the opposite of the truth, and it was loudest exactly where it
was most wrong. Round 53 shipped this for PubMed alone; this round would have
spread it to seven tools, where every empty MedGen, GEO, SRA and nuccore
search would have gained a block claiming its query was not run as submitted.
Only an actual alteration -- a dropped phrase, an ignored phrase, a dropped
term or an unrecognised field -- now creates the block. `outputmessages` is
still carried alongside one of those, so the zero-hit case that DID drop a
term keeps its disclosure; both directions are pinned by tests, each proven to
fail when the guard is reverted.
Also corrected, all found by the same validation, all authored by this round:
* the shared docstring claimed unaffected responses keep their shape, which
the `outputmessages` gate made untrue on every empty search;
* the unwired-module inventory said "eleven other modules" and omitted
`clinical_society_tools`, whose `_pubmed_search` takes a caller-influenced
term and discards both containers -- it is twelve, and both it and
`icite_tool` are now named;
* `_render_clause` said a range is "the only carve-out" while the code has
two, the second being non-string values, which a test four lines below the
one asserting "the ONLY exemption" already pinned. Both now say so;
* the nuccore test fixture was labelled verbatim but spliced the count from
the "BRCA1 human" probe (7359) onto the errorlist of the "BRCA1" probe
(66893). Corrected to the probe it claims to be;
* the taxonomy fixture cited a measurement absent from the table it defers
to. That fifteenth, zero-hit probe is now recorded there;
* the trimmed PubMed test header still narrated a probe whose fixture was
deleted.
tests/unit + tests/integration: 3 failed, 7 errors, all pre-existing on the
branch base (test_mcp_protocol, and one test_smcp_http_server fixture),
verified by stashing this round's changes and re-running.
* Fix Round 55: result sets truncated without saying so, and pointers to tools that do not exist
CPIC_list_drugs returned 200 of 324 drugs and said nothing. The missing
124 include efavirenz and clozapine, and the omission is not theoretical:
a persona in round 54 concluded from this tool that efavirenz has no CPIC
guidance. Upstream reports `content-range: 0-323/*`; the endpoint had a
paging value hard-coded in its URL template while its own schema also
declared one, so it was sending `limit=200&limit=5`.
Two class invariants now guard the general case rather than the instance:
no endpoint may hard-code a paging value its schema also declares, and no
GeneOntologyTool may declare a parameter absent from its endpoint
template, which `_build_url` can only ever ignore. Both pass repo-wide.
The FAERS_search_* detail family returned a bare array with no total, so
a caller could not distinguish ten reports from the first ten of
thousands -- `rifampin`+`methadone` returned 10 rows against an upstream
total of 35. They now return an envelope carrying `count` and
`total_available`, using the vocabulary `BaseRESTTool` and
`CBioPortalRESTTool` already share rather than inventing a fifth spelling
for the same idea.
Two dead pointers were found by replacing a heuristic with a rule. The
existing pointer test used difflib similarity, which measurably would
have missed 5 of the 9 names the file already pins; a registry-derived
rule (second segment must be a verb shared across at least ten vendor
prefixes) catches those and surfaced two nothing had caught:
ChEMBL_get_molecule_by_chembl_id, quoted in four --help epilog examples
where a user copy-pasting it would exclude nothing, and
UniProt_get_uniref_search in a tool description. Real names are
ChEMBL_get_molecule and UniProt_search_uniref.
FDADrugInteractionDetailTool held 318 lines byte-identical to
FDADrugAdverseEventDetailTool, and this change had been paying every edit
into both copies. It now subclasses and overrides only the query builder,
the same relationship FDACountAdditiveReactionsTool already has with its
parent: 366 lines removed, 56 added.
Verified: CPIC returns 324 with efavirenz and clozapine present, matching
upstream exactly; the six FAERS tools re-run live with identical totals;
46 new tests pass and each fails on revert.
* Fix Round 56: a search that never searched, and results that hid their own obsolescence
WHO_Guidelines_Search returned byte-identical results for every query.
The query was slugified into who.int/health-topics/<slug>; confirmed live
that this 404s for anything WHO does not treat as a health topic
("methadone", "clozapine" and the nonsense token "zzqqxxwubble" all 404,
while "tuberculosis"/"malaria"/"trachoma" return 200). On a 404 the tool
scraped WHO's generic recent-guidelines listing and returned those as the
answer, each tagged is_guideline: true, with nothing marking the query as
unmatched. A differential sweep over all ten guideline search tools found
this was the only one whose output did not vary with the query (SIGN and
CTFPHC return empty for these terms because those small national sets
genuinely lack them, which the sweep confirmed against terms they do
cover).
Topic misses now search WHO IRIS, WHO's official institutional
repository, over its DSpace REST API -- confirmed genuinely
query-responsive: methadone 1027 hits, clozapine 324, trachoma 3964,
"zzqqxxwubble" 0. IRIS has no "guideline" document type, so is_guideline
there reflects the record rather than being asserted for every hit, and
each result reports in matched_via which backend answered it.
Mondo_get_disease on an obsolete term returned success with a fully empty
record -- indistinguishable from a live disease with no annotations. The
only signal was the word "obsolete" inside the name. This is reachable,
not hypothetical: GenCC returns disease_curie MONDO:0014978 for PADI6,
which feeds straight in. The Monarch payload already answers it
(deprecated: True, versus None for the live term) and the tool discarded
the field; the same field was discarded on the /search rows, so a search
listed an obsolete term beside its replacement with nothing to tell them
apart. Obsolete terms now say so and, via OLS4's term_replaced_by, point
at the current term (MONDO:0014978 -> MONDO:1010200, which carries the
PADI6 gene). Costs one extra request on the deprecated path only; live
lookups are unchanged.
ClinVar published a url that described no request: the bare
esummary.fcgi endpoint, identical for every variant and an error when
replayed, because the query lived in params=. Now reports the URL as
sent. A repo-wide AST sweep found 11 sites across 7 files with this
shape; the remaining 10 are listed in the round report, unverified and
not touched here.
All 14 new tests fail when their fix is reverted, per assertion.
* Fix Round 56 (quality): one meaning for is_guideline across both WHO backends
Follow-up from the round's review pass, which found the previous commit
had left is_guideline meaning two different things in adjacent branches
of the same class: derived from the record on the new IRIS path, still
hardcoded True on the health-topic path. Both feed one result list, so a
consumer filtering on the field got an answer that depended on which
backend happened to answer.
Both backends now emit through one _result() shaper. Measured cost, live:
tuberculosis 5/10 rows keep is_guideline True (the consolidated
guidelines; the operational handbooks correctly drop to False), while
trachoma 0/2, malaria 0/5 and onchocerciasis 0/3 go to zero -- those
topic pages list epidemiological updates, procurement guidance and
surveillance toolkits, none of which are guidelines. Callers filtering on
is_guideline see a large drop in yield; the previous yield was asserted
rather than established. The flag is a title/document-type test, which
will miss WHO guidance published as handbooks or manuals, and the tool
description now says so.
Also from the review:
- _replaced_by is MONDO-only. The OBO PURL it builds is wrong by
construction for Orphanet (orpha.net/ORDO, OLS4 ontology "ordo"), EFO
and OMIM, all of which reach _normalize_curie, so those would have
spent a request to learn nothing.
- its timeout is capped at 10s independently of the Monarch timeout, so a
slow OLS4 cannot stall an answer that is already complete.
- the 0.5s sleep now spaces the second who.int request instead of
delaying the first request of every call.
- the per-item closure in _search_iris is a module-level _dc_value.
- corrected this round's own test docstring: chebi_tool and hpo_tool
disclose silent ID substitutions, they do not resolve replacements.
15 tests, all failing when their fix is reverted.
* Fix Round 56 (validation): declare the fields the round added, and correct two claims
From the round's validator pass.
- Monarch_get_entity and Monarch_search now emit `deprecated`, but only
mondo_tools.json declared it; monarch_new_tools.json listed every other
field and omitted this one, so the schema under-described the payload.
- WHO_Guidelines_Search declared `items: {type: object}` with no
properties at all, so none of matched_via, document_type, date_issued
or is_guideline was described anywhere a caller would look. Declared,
with is_guideline stating plainly what it does not detect.
- The WHO description claimed topic pages are used "when the query names
a WHO health topic". _topic_slug also retries on the query's first
significant word: live, "malaria vaccine" is answered by the whole
malaria topic page. matched_via already disclosed the broadening in the
payload; the description now does too.
- Corrected this round's own test docstring, which cited
api-v3.monarchinitiative.org while the tool calls
api.monarchinitiative.org. Re-checked both hosts: deprecated: True on
each, so the finding stands, but the citation now names the host under
test.
- Scoped the ClinVar test's claim to the success and rate-limited paths.
The two exception paths hold no response object and still report the
pre-request URL, which the previous framing did not admit.
* Fix Round 57: empty results that hid a dead identifier, and a URL that described no request
Round 56 fixed Mondo_get_disease to report deprecated/replaced_by, having
found Monarch's payload already carried the field. Its siblings answer the
same dead identifier with an empty success. Confirmed live on
api.monarchinitiative.org for three separate obsolete CURIEs, each with a
populated replacement one hop away: MONDO:0014978 -> MONDO:1010200 (2
phenotypes, PADI6), MONDO:0002906 -> MONDO:0019340 (190 phenotypes),
MONDO:0001111 -> MONDO:0018896 (83 phenotypes). Reachable, not
hypothetical: GenCC_search_gene {"gene_symbol": "PADI6"} hands back
MONDO:0014978, verified in this tree.
These endpoints return rows and nothing else, so there is no deprecated
field in the response to surface -- obsolescence has to be asked for
separately. Hence a hook on the empty path rather than a response shaper.
Mondo_get_disease_phenotypes, MonarchV3_get_histopheno,
MonarchV3_get_mappings and MonarchV3_get_associations now say the CURIE is
obsolete and name its replacement. Cost: one /entity request (measured 259
ms) only when the result set is empty, plus the OLS4 lookup (355 ms) only
when the term really is obsolete; non-empty results and non-MONDO CURIEs
are untouched.
The same shape one family over: HPO_get_diseases_by_phenotype and
HPO_get_genes_by_phenotype returned an empty success for a merged term
while HPO_get_term, in the same module, already detected the merge.
HP:0003114 -> 0 diseases; its replacement HP:0001626 -> 4962. The JAX
annotation endpoint answers a merged ID with HTTP 200 and empty arrays, so
upstream genuinely has nothing and the tool must disclose rather than
recover. The detection is the existing one: /api/hp/terms/<id> silently
serves the replacement's record, so a requested-vs-returned id mismatch is
the merge. The note text moved into a shared helper so both callers keep
one wording.
Round 56 fixed ClinVar's bare esummary.fcgi URL and listed ten more sites
unverified. Verifying them found the listed KEGG and HAL sites are not
instances -- KEGG builds its query into the path, so its published URL is
already complete (rest.kegg.jp/find/pathway/glycolysis), and HAL's is a
per-document link. The real instance is the shared NCBI E-utilities base,
reachable from four tools across three modules: geo_search_datasets,
geo_get_dataset_info, dbsnp_search_by_gene and NCBI_search_nucleotide all
published the bare endpoint, identical for every query, and replaying it
returns "Empty term and query_key - nothing todo". Now reports the URI as
sent; the replayed URL reproduces the request exactly. E-utilities takes no
credentials on these paths, so nothing secret is exposed.
PubMed_Guidelines_Search fetched the number of matching guidelines and
discarded it with a dead expression statement: "therapeutic plasma
exchange" returned 3 rows while esearch reported 94. It already returns a
dict envelope, so publishing metadata is additive here, unlike its four
sibling guideline tools whose bare-list returns keep them deferred.
retrieved is reported separately from returned because the rows are
filtered client-side against the query terms, so only total > retrieved is
upstream truncation.
All four fixes are additive; no contract changed. 31 new tests; each fix's
assertions were proven to fail against its own reverted source.
* Fix Round 57 (quality): one home for three duplicated idioms
Follow-up to the round-57 fixes, from the simplify pass. No behaviour
change except where noted.
The "resolved request URI, else the endpoint" helper existed three times
with two different coercion rules: tcia_tool's _requested_url, the new one
in ncbi_eutils_tool, and base_rest_tool's echo_request_url gate. The first
two were byte-equivalent, so they now share request_url() in base_tool.py.
base_rest_tool's gated variant is left alone -- defaulting it on is a
behaviour change across every BaseRESTTool, recorded as a follow-up rather
than smuggled into a cleanup.
The Mondo deprecation note was built twice, and the wording had already
drifted between the two copies in the same round. Both now call
_deprecation_metadata(), which takes the clause naming what is empty so one
sentence serves a stripped record and a stripped result set. This also
carries the OLS4 timeout cap, which _obsolescence_metadata had copied and
recomputed, to both callers.
The three row-returning Monarch endpoints had grown identical
build-metadata / probe-if-empty / envelope triples; they share _rows().
HPO's merge disclosure was factored down to the note string only, leaving
the three-key dict written twice. _merge_disclosure() now owns the keys and
the id-mismatch test, so _get_term and _merge_metadata cannot drift.
Two real improvements fell out of the review rather than the round:
_merge_metadata was passing the full 30s timeout where its Monarch twin
caps at 10s, and the JAX annotation payload carries genes, diseases and
assays together -- so a non-empty sibling array already proves the term
resolves, and the probe is now skipped in that case instead of spending a
round-trip to learn nothing.
PubMedGuidelinesTool._search_pubmed_guidelines returned a 2-tuple whose
first element was either a list or an error dict, so run() unpacked a value
it discarded on one branch and two error paths padded a {} nobody read. It
now returns the finished envelope and run() returns it directly.
That last change broke test_pubmed_guidelines_envelope.py, which stubbed
the private helper's old return shape -- found in the full suite, not in
the targeted re-run. The Fix-R9E-1 guard it encodes is still correct and
still wanted, so the three tests were rewritten to stub the HTTP session
instead of the helper; they now assert what run() hands a caller, which is
what R9E-1 was about, and no longer break on an internal refactor.
Skipped, with reasons: memoizing the deprecation probe (process-global
state across tests, against this repo's hermeticity rule); dropping
`truncated` as derivable (sibling search tools publish it, so consistency
wins); wiring esearch_query_disclosure into the guideline search (a
behaviour addition, not a cleanup); publishing totals for the four sibling
guideline tools (contract change, deferred).
* Fix Round 57 (validation): pin the shared note, and make the probe guard match its comment
From the round's validator pass. All three were reported as observations,
not gate failures; the suite was green before and after.
The Mondo deprecation note's tail changed wording when Fix-R57-4 moved the
sentence into a helper, and nothing caught it: the existing test asserted
only that "obsolete" and the replacement CURIE appeared in the string. HPO's
equivalent wording was already pinned, so the asymmetry meant Mondo's could
drift again silently. Both clauses are now pinned verbatim -- the one naming
what is empty is exactly the part that differs per caller, so it is the part
worth holding.
The HPO probe guard's comment claimed the annotation payload's four arrays
are all checked; the code checked three. Rather than narrow the comment, the
guard now checks medicalActions too, so a term annotated only with medical
actions no longer spends a round-trip to learn it is live.
Both PubMed error-path tests raised a bare OSError, which lands in the
blanket handler rather than the requests.RequestException branch the test
names describe -- so the documented branch was asserted only via the
catch-all. Parametrized over both.
Targeted suite: 229 passed, 0 failed.
* Fix Round 58: three values parsed and dropped, one of them naming the wrong species
BVBRC_search_specialty_genes selected `organism` -- BV-BRC's reference
annotation organism -- and not `genome_name`, the genome the hit was found
in. The two disagree routinely, so "which species carries blaKPC" was
answered with the wrong species: sp_gene row genome_id 550.1655 reported
organism "Klebsiella pneumoniae" for a genome named "Enterobacter cloacae
strain 1-RC-17-04409-1", contradicting its own adjacent taxon_id 550. The
sibling BVBRC_search_amr already selects genome_name; select it here too and
keep `organism` in place.
gnomad_get_gene_constraints asked only for gnomad_constraint and
exac_constraint, which gnomAD leaves null for mitochondrially-encoded genes
because it scores them under mitochondrial_constraint. MT-ND4 therefore
returned success with both null and no note, reading as "unconstrained" for
a gene gnomAD scores at oe_lof_upper 0.022. Request the field, both union
branches (protein and RNA), and say plainly when a gene has none of the
three rather than leaving nulls to be misread. The live query lives in the
shipped config, so the fix is there; the class default is kept in step.
UniProt_id_mapping hardcoded failed_ids to [] and reported the returned page
size as mapped_count, so Q9F663+NOTAREALACC99+P0DTC2 came back
"mapped_count 25, failed_ids []" with one valid accession and one typo'd one
silently absent, against an upstream x-total-results of 2236. Read failedIds
and the total, and stop treating the 303-redirected status page as final --
the resolution Fix-R20A-1 already applied to the sibling
UniProtIDMappingTool, which is a second copy of this logic. Q9F663 now
returns 83 of 83 rather than 25.
Return schemas updated to match, including gnomad_constraint's nullability,
which the old schema forbade.
* Fix Round 58: guideline searches reported a page of rows as the whole corpus
Six guideline backends publish a match count. Five parsed it and dropped it
with a bare expression statement -- Europe PMC `data.get("hitCount", 0)`,
TRIP `int(total.text) if total is not None else 0`, OpenAlex
`data.get("meta", {})` -- and NICE and WHO IRIS never read theirs. Each tool
then returned a bare list, so there was nowhere to put the figure even once
read, and `limit` rows were indistinguishable from everything that matched.
Measured live against the hosts the tools call: NICE "infection" is 1057
documents, of which the tool served 15; TRIP vancomycin 12638; WHO IRIS
tuberculosis 28435; OpenAlex 18266; Europe PMC 9972; PubMed 94.
Round 57 fixed PubMed_Guidelines_Search alone and deferred the rest rather
than leave the family half-converted. That left it worse than half: the new
envelope validated against neither branch of the tool's own declared
return_schema (`oneOf: [array, {required: [error]}]`), and
examples/workflows/clinical_guidelines_search.py, which unwraps `guidelines`
and `results` but not `data`, silently dropped every PubMed row.
So convert the whole set to the {status, data, metadata} envelope that
PubMed_Guidelines_Search and EuropePMC_search_articles already use, and fix
all six return_schemas -- including PubMed's, which was wrong before this
change. `retrieved` is separate from `returned` because several of these
filter rows client-side, so only total > retrieved is upstream truncation.
TRIP's <count> is deliberately not used: limit=3 answers <count>20</count>,
a page size, which is the defect being removed.
GIN, CMA, SIGN and CTFPHC keep their bare-list contract. Their sources
publish no match count, so an envelope could only report total ==
len(results) -- manufacturing the same defect in six more tools. The split
is by whether a real total exists, and is stated in the guide.
Also fixes a NICE test that asserts guidance ids start with ng/cg. NICE
serves in-development guidance under /guidance/indevelopment/<id> and ranks
one into "diabetes" today, so that assertion fails on unmodified main; this
was confirmed by stashing the change and re-running.
* Fix Round 58 (validation): restore the entry objects a wider page had flattened
The validator caught a regression in the round's own UniProt change. For
`to_db=UniProtKB` the job's 303 redirect lands on
/idmapping/uniprotkb/results/, which streams full entries; the generic
/idmapping/results/ returns bare accession strings. Re-fetching the generic
URL to get a bigger page therefore replaced every {accession, id, gene_name}
object with a plain string -- P04637 came back "P04637" instead of carrying
P53_HUMAN. Re-fetch through the URL the redirect actually landed on, which
keeps the shape while still asking for a full page, and skip the second
request entirely when the status response already carried every row (its
x-total-results and failedIds are on that response too). P04637 -> PDB still
returns 295 of 295, against 25 before the round.
`to` as a bare string was never admitted by the declared return_schema even
though the tool has always returned it for non-UniProtKB targets, so the
schema now carries both forms -- the same "output matching neither branch"
defect this round set out to fix, found one layer down.
Quality pass over the round's own diff:
* `_guideline_envelope` now pairs `truncated` with a `truncation_note`.
Every other truncation discloser in the repo says what to do about it;
a bare boolean states the fact and withholds the remedy.
* Dropped two empty-result early returns in Europe PMC and TRIP that
produced byte-identical output to the normal exit, and the PubMed
envelope closure, now a bare partial application over two call sites.
* `_note_absent_constraints`/`_CONSTRAINT_KEYS` moved to module level in
gnomad_tool.py beside `_missing_callset_note` and `describe_dataset`,
which handle the same genre of concern; also fixes a ruff-format
violation the validator flagged.
* Deduplicated a six-fold copy-pasted unwrap in test_guideline_tools.py.
* Removed test_both_organism_fields_reach_the_caller: it stubbed the
request helper with a hand-built row, so the select_fields list under
test was never consulted and it passed against pre-fix code.
* test_guideline_search_totals.py now asserts every search tool in the
config is classified, so a new one cannot land unguarded.
Declared BVBRC `genome_name` and gnomAD `note` in their return schemas,
which the validator found undeclared.
* Fix Round 59: CPIC_get_recommendations dropped rows without saying so
CPIC_get_recommendations applies a documented default `limit` of 50 but
reported only `count`, which is the size of the returned page. Confirmed live
that guideline 100416 (CYP2D6/OPRM1/COMT and opioids) has 66 recommendation
rows, so the default response was `count: 50` with no note and no total --
indistinguishable from the guideline having exactly 50, while 16 rows were
dropped. Codeine, tramadol and hydrocodone guidance all lives on that
guideline.
tu run CPIC_get_recommendations '{"guideline_id": 100416}'
before: count 50 (no total, no note)
after: count 50, total_count 66, offset 0,
note "Showing recommendations 1-50 of 66 ... offset=50 ..."
This is the same defect CPICGetAllelesTool in the same module already fixed;
its class docstring describes it. That tool's Content-Range helper is hoisted
to module level and reused, and the near-identical paging sentence both tools
build is now one `_paging_note` helper -- the alleles note comes out
byte-identical, verified live. The exact server-side COUNT is requested only
on the unfiltered path: when gene/phenotype filtering is applied the request
already pulls limit=1000 and the total that matters is the number of matching
rows, counted locally, so asking PostgREST for a count there would be work
thrown away. Purely additive -- no existing key changes value.
CPIC_search_gene_drug_pairs documented `limit` as "(default 50)". There is no
such default: confirmed live that an unfiltered search returns all 635 pairs
and a CYP2D6 search returns all 81. Returning everything is the better
behaviour, so the description is what was corrected.
Not done, deliberately: the nine CPIC return_schemas were NOT rewritten to
describe the {status, data, ...} envelope. That change was made, then reverted
after it was proven to break `tu test` 2/2 on CPIC_get_gene_info. Per issue
#246 the repo convention is that return_schema describes the inner `data`
payload -- cli.py validates `result["data"]` against it, and
tests/tools/test_return_schema_data_shapes.py asserts an envelope-shaped
schema must fail. 319 of the 328 envelope-returning configs already follow
the payload convention. The nine CPIC schemas were correct as shipped.
* Fix Round 60: Crossref searches reported a page of works as the whole corpus
CrossrefRESTTool._process_response extracted message["items"] and set
`count` to len(items), discarding message["total-results"] -- the only number
Crossref publishes for how many works actually match. With the documented
`limit` defaults (10 for works, 20 for funders/members) that made the page
size look like the size of the result set, on all four Crossref list
endpoints.
tu run Crossref_search_works '{"query": "CRISPR gene editing", "limit": 3}'
before: count 3 (no total, no note)
after: count 3, total_count 1037336,
note "Showing results 1-3 of 1037336 ... offset=3 ..."
tu run Crossref_search_members '{"query": "university", "limit": 3}'
before: count 3 after: count 3, total_count 4032
tu run Crossref_list_funders '{"query": "cancer", "limit": 3}'
before: count 3 after: count 3, total_count 746
Swept all eight CrossrefRESTTool configs. Four are list endpoints and all
four publish total-results upstream (works, funders, members, types); the
other four are detail lookups whose message carries no `items` and are left
byte-identical. /types is the one list endpoint that never diverges -- it
returns all 30 rows -- so it reports total_count 30 and correctly emits no
truncation note.
`offset` is added to the three searchable list tools so the note's advice is
actionable; verified live that Crossref pages on it (funders "cancer"
offset=0 -> Canceropôle PACA, Instituto Nacional de Cancerología; offset=2 ->
Cancerfonden, Syöpäsäätiö) and that the note counts from the caller's offset,
not from zero. The offset is read back off the URL requests actually
assembled, so it cannot drift from what was sent.
Purely additive: `data`, `count` and `url` keep the values they always had,
and total_count/note are omitted entirely when Crossref sends no usable
total. return_schema is untouched -- per issue #246 it describes the inner
`data` payload, and the new keys live in the envelope.
* Fix Round 60: a merged UniProt taxon was answered as the one that was asked for
UniProtTaxonomy_get_taxon fetched rest.uniprot.org/taxonomy/{id} and read
taxonId straight out of the body. UniProt answers a merged (retired) taxon
with an HTTP 303 to the node it was merged into, so requests followed the
redirect and the tool returned a different taxon than the caller asked for --
with the requested id appearing nowhere in the response, making the
substitution undetectable.
tu run UniProtTaxonomy_get_taxon '{"taxon_id": "46170"}'
before: taxon_id 1280, "Staphylococcus aureus" (46170 absent entirely)
after: query_taxon_id 46170, taxon_id 1280, merged_from 46170,
metadata.note "...merged it into 1280 ... the protein counts
are for the merged node, which may sit at a higher rank..."
Taxon 46170 is Staphylococcus aureus subsp. aureus, the subspecies node many
older BioSample/BioProject MRSA records are filed under; 1280 is the species.
Silently swapping one for the other inflates the protein-count denominator
any per-subspecies estimate is built on. UniProt publishes the merge plainly
-- `HTTP/2 303 ... location: /taxonomy/1280?from=46170`, and with redirects
disabled the body is
`{"taxonId":46170,"inactiveReason":{"inactiveReasonType":"MERGED","mergedTo":1280}}`
-- so the information was available and simply discarded.
Same shape as the obsolete-identifier disclosure already shipped for Mondo,
Monarch and HPO: answer with the current record, but say so. Detection
compares the requested id against the returned one, which needs no extra
request, so the fix costs nothing on the wire. String and integer ids compare
equal ('9606' and 9606 are one taxon) so an active lookup can never be
mislabelled a merge, and a non-numeric id degrades to no comparison rather
than an exception.
Purely additive: every field that was there before keeps its value, and
query_taxon_id is echoed on the happy path too so a caller can always join
request to response. The sibling UniProtTaxonomy_search is untouched.
* Fix Round 60: HGNC alias resolution note read 'a alias symbol'
The symbol-resolution note was templated as "a {relation}", and one of the
two relation labels starts with a vowel, so every alias hit -- the more
common of the two -- produced "a alias symbol".
tu run HGNC_fetch_gene_by_symbol '{"symbol": "CAH1"}'
before: "...listed as a alias symbol of 'CYP21A2'..."
after: "...listed as an alias symbol of 'CYP21A2'..."
tu run HGNC_fetch_gene_by_symbol '{"symbol": "CYP21"}'
before/after: "...listed as a previous symbol of 'CYP21A2'..." (unchanged)
Cosmetic only -- no key, value or control flow changes -- but this note is
the tool's disclosure that it answered with a different gene than the one
asked for, so it is worth having it read as written prose. The existing test
asserted only that 'alias symbol' appears in the note, which the defect
satisfied; it now pins the article, and a new case pins the consonant-initial
label so the helper cannot regress the other direction.
* Fix Round 60: OpenFoodFacts_search_products documented a default it never applied
page_size was described as "Default: 10". No default was ever applied: the
schema declares none, so BaseRESTTool._build_params has nothing to inject and
Open Food Facts' own default for /cgi/search.pl is what callers get.
tu run OpenFoodFacts_search_products '{"search_terms": "almond milk"}'
documented: 10 products
actual: page_size 50, 50 products
Swept both Open Food Facts tools that document a page_size default. The
sibling OpenFoodFacts_filter_products_by_tags runs against a different
endpoint and returns exactly the 20 it documents -- confirmed live -- so only
one of the two descriptions was wrong, and the correction is deliberately not
applied to both.
Returning 50 is the better behaviour (a caller who wants 10 passes 10), so
the description is what was corrected rather than the behaviour -- no
response changes value. The test pins the two descriptions against each other
so the wrong number cannot come back, and asserts this tool still declares no
schema default, since adding one would override upstream's and silently make
the description wrong again.
Not changed: this tool already reports Open Food Facts' true match count as
'count' (2577 for "almond milk" against a 50-row page), so it does not have
the page-size-as-total defect.
* Fix Round 60 (review): adopt the repo disclosure vocabulary; close a regression
Applied the findings from the simplify review and the tool-validator run.
Crossref:
- `note` -> `truncation_note`, plus a machine-readable `truncated` bool, so
a caller never has to string-match prose to learn it got a page. `note` was
also the key BaseRESTTool.run already uses for `fields.empty_result_note`,
i.e. the same key with two meanings in one class hierarchy.
- the sentence moves into a named module-level `_truncation_fields()`, so it
has one home rather than being an f-string buried in `_process_response`.
- the note no longer advises a page that cannot be fetched: Crossref caps
offset+rows at 10000 (measured: offset=9000 succeeds, offset=9999 returns
HTTP 400), so for a larger result set it now says only the first 10000 are
reachable by paging and points at 'filter'. Confirmed live that the caveat
appears for the 1037336-match query and not for the 746-match one.
- Crossref_search_works.limit gains the minimum/maximum its two siblings
already declare, since the note advertises "max 100".
Regression found, proven and closed: dropping `TypeError` from the offset
parser broke tests/unit/test_scientometrics_depth.py::test_parses_member_list.
That suite drives the tool through a MagicMock response whose `url` attribute
is a truthy Mock rather than a string, and urlparse raises TypeError on it --
turning the whole call into status: error. Both URL parsers now check
isinstance(url, str) explicitly, which is load-bearing rather than defensive
typing, and a new test pins the MagicMock path so it cannot recur.
UniProt taxonomy:
- the disclosure now reads the evidence UniProt publishes instead of
inferring it. The redirected URL carries ?from=<old> and the inactive stub
carries inactiveReason.mergedTo; when either is present the note states the
merge, and when only the ids differ it says UniProt did not state why. The
previous version asserted "UniProt has merged it into N" for any mismatch,
which is a claim it had not checked -- the same hedge ChEBI's redirect
disclosure already makes. Still zero extra requests.
- `query_taxon_id` is normalized, so it is directly comparable to
`taxon_id` without the caller coercing "9606" against 9606.
- `common_name` becomes ["null", "string"], matching the sibling
UniProtTaxonomy_search schema. Pre-existing, but this round promotes 46170
as the flagship example and 1280 has no common name, so the one example the
description advertises did not validate.
- `_as_taxon_int` is annotated Optional[int] rather than Any.
HGNC: the a/an choice becomes a two-entry table beside _RELATION_LABEL rather
than a general English article rule, since the input is those two fixed labels.
OpenFoodFacts: the schema now declares default: 50 rather than only describing
Open Food Facts' unversioned server default, so BaseRESTTool sends it
explicitly and the number is owned here. Verified live the response is
unchanged (page_size 50, count 2577) and an explicit page_size: 10 is still
honoured.
Long comments that merely restated the test docstrings are trimmed to the
mechanism and the decision; the live transcripts now live only in the tests.
All 8 Crossref tools, both UniProt taxonomy tools: built-in examples green.
* Fix Round 61: OMA paging parameters that were declared but not applied
OMA_get_orthologs forwarded `per_page` to /protein/{id}/orthologs/, which
accepts it, returns HTTP 200, and ignores it. Measured against P04637,
per_page of 3, 20 and 100 each returned all 157 orthologs, and the response
carries no Link, X-Total-Count or content-range header to page with. The
schema promised "default: 20, max: 100"; a caller asking for 3 received 157,
and the repo's own test_examples ask for 10.
The limit is now applied after fetching, since the full set is already in
hand, and the response says so: `count`, `total_count` and `truncated`, the
vocabulary round 60 settled on, plus a `truncation_note` naming a request
that actually works. `total_orthologs` is removed rather than redefined --
it meant "rows returned", the two coincided only while nothing was sliced,
and a key that silently changes meaning is worse than one that is absent.
No cap is applied here: with no server-side paging, capping at the
advertised 100 would strand orthologs 101-157 rather than merely defer them,
so the parameter description drops a "max" that was never enforced anyway.
Separately, all three nullable paging parameters crashed on their own
declared value. oma_tools.json types them ["integer", "null"], and each call
site then ran min(value, 100), raising TypeError: '<' not supported between
instances of 'int' and 'NoneType'. That reached the user as "Unexpected
error querying OMA" naming neither the parameter nor the cause. Confirmed
live on OMA_get_orthologs, OMA_resolve_xref and OMA_get_genome_pair_orthologs;
one `_page_size` helper now treats absent and null alike and tolerates the
strings CLI arguments arrive as.
Scope is one endpoint, not the module: /pairs/{g1}/{g2}/ does honour
per_page (measured: 3 -> 3 rows, 20 -> 20, each with a Link header), so
OMA_get_genome_pair_orthologs keeps forwarding it and gains only the null
fix, and OMA_resolve_xref already sliced client-side and disclosed its total.
Verified live after the change: per_page=3 -> 3 rows, default -> 20,
per_page=null -> 20, per_page=157 -> 157 with truncated false; pairs still
3 -> 3 and 20 -> 20; xref still 5 rows with total_matches 4357. 24 new tests
pass, 9 of them fail when the behaviour is reverted; full unit suite 5596
passed, 27 skipped.
* Fix Round 61 (correction): the null-argument fix is hardening, not a user bug
The previous commit claimed the TypeError on a null paging argument "reached
the user as Unexpected error querying OMA". That is wrong and this corrects
the record rather than quietly restating it.
execute_function.py:3115 strips None arguments before dispatch -- "None means
not provided -- simply omit such keys" -- so run_one_function never delivered
one. Verified against the pre-fix code through that path: all three OMA tools
returned success with a null paging argument. The crash is reachable only by
constructing the tool class directly, as this repo's own unit tests do. The
fix is kept, because a tool that accepts its own declared argument on only one
entry point is a trap for the next caller, but it is hardening and is now
labelled as such in both the helper and the test module.
Found while sweeping the rest of the repo for the same shape: 59 min(<arg>, N)
sites fed by arguments.get, 34 of them with no null guard, 14 (tool, param)
pairs where the parameter is also declared nullable. All 7 reachable ones were
live-tested through run_one_function and none crashed, which is what prompted
re-testing the OMA claim by the same route. That sweep is a negative result:
this class of defect is contained by the framework, and no further module
needs the change.
What the previous commit got right is unaffected and was re-confirmed through
run_one_function: on the pre-fix code OMA_get_orthologs returned all 157
orthologs for per_page=3 and for per_page=20, exactly as a user would hit it.
|
||
|
|
cf5566c3d6 |
Fix Round 33: tools that answered without the data they were asked for (#508)
* Fix Round 33: DNA translation ignored the genetic code the caller asked for
DNA_translate_reading_frames declared a genetic_code parameter with no enum
and never read it. Passing genetic_code=2 (NCBI vertebrate mitochondrial)
returned status=success with the standard-code translation, so ATGTGATTAAGATAA
came back as "M*LR*" when code 2 gives "MWL**" -- a spurious stop at codon 2
and a missing stop at codon 4.
That matters because the codes differ precisely at the codons a caller would
be checking: code 2 reads TGA as Trp rather than stop, and AGA as stop rather
than Arg. Someone translating a mitochondrial gene got a confidently wrong
protein with no indication that the requested code had been dropped.
Only the standard code is implemented, so reject anything else at input rather
than substituting a different table silently. The rejection is declared in the
schema (enum) so the constraint is visible before the call, and re-checked in
the handler so callers that construct the tool class directly, bypassing schema
validation, get the same actionable error.
* Fix Round 33: Reactome ID query accepted species/type filters it could never apply
Reactome_query_by_ids advertised species and types as optional filters. Neither
was ever transmitted: for /data/query/ids the POST branch sends the IDs as a
plain-text body with no query string, so both sat in query_params and were
dropped. A caller asking for R-HSA-73817 and R-MMU-73817 with
species="Homo sapiens" got the mouse pathway back alongside the human one, with
nothing to indicate the filter had been discarded.
Forwarding them would not have helped. A controlled probe against
reactome.org/ContentService/data/query/ids returned a byte-identical 32445-byte
payload for every variant -- no filter, species=Homo sapiens, species=9606,
species=Mus musculus, types=Reaction, and species+types combined. The negative
cases are the decisive ones: species=Mus musculus still returned the human
record, and types=Reaction still returned two Pathways. The endpoint performs no
server-side filtering at all, so the parameters are unimplementable by any
routing change.
Reject them at input rather than returning unfiltered results that look
filtered, and point the caller at what does work: each record carries
speciesName and schemaClass for client-side filtering, and
Reactome_list_top_pathways / Reactome_get_events_hierarchy take species as a
genuine path parameter. The guard is keyed on the endpoint template, so those
sibling tools are unaffected.
The properties stay in the config, with descriptions that now lead with the
non-support, so the generated Python wrapper keeps its signature and a caller
passing species gets the actionable error instead of a bare TypeError. The
target-research skill reference listed species as a working argument for this
tool and would have steered an agent straight into the error, so it is corrected
in all three copies.
* Fix Round 33: BindingDB returned no affinities for any target because of the query parameter name
Every BindingDB lookup sent plural query-parameter spellings where BindingDB's
REST spec documents singular ones, and joined multiple identifiers with a
semicolon where the spec uses a comma. The endpoints do not reject the wrong
names: getLigandsByUniprots answers HTTP 200 with an empty affinities list, and
getLigandsByPDBs answers HTTP 500. So the tools reported status=success with
zero records for targets that have tens of thousands.
Controlled probes, identical URLs except the parameter name:
?uniprots=O43613 -> HTTP 200, affinities: []
?uniprot=O43613 -> HTTP 200, 10,460 records
?uniprot=O43613;O43614 -> 0 records
?uniprot=O43613,O43614 -> 3,229 records at cutoff=10
?pdbs=4S0V -> HTTP 500
?pdb=4S0V -> HTTP 200 with data
After the fix, HCRTR1 (O43613) returns 10,460 affinities, HCRTR2 (O43614)
12,846, and EGFR (P00533) 29,169 - all previously empty.
The wrong parameter name also produced a false diagnosis that outlived it. The
module docstring, an inline comment, a user-facing error message and the
broken_apis registry all asserted that BindingDB's REST tier was down or that
the PDB route was "failing for all structures", and sent callers to substitute
databases for an outage that does not exist. Those claims are removed: only the
singular getLigandsByUniprot endpoint genuinely hangs, which is why every
operation still routes through the plural endpoint.
getTargetByCompound additionally namespaces its payload with a bdb. key prefix,
which the shared affinities parser did not recognise, so that endpoint returned
an empty list even when the request succeeded. The prefix is now normalised,
which also preserves bdb.species - hits are frequently non-human orthologs and
that distinction matters when reading an affinity.
The similarity parameter on getTargetByCompound is deliberately left as-is. The
spec names it cutoff, but the endpoint returns the same five records under
either spelling, at every value from 0.4 to 0.99, and with the parameter absent
entirely, so renaming it would change no behaviour while invalidating two
existing tests. That the endpoint ignores the threshold at all is noted for
follow-up rather than worked around here.
Empty results now carry a note distinguishing a genuine no-data answer from a
rejected query, so a future parameter defect cannot masquerade as absent data.
* Fix Round 33: Orange Book reported empty searches as a dead endpoint, and CTIS accepted status filters it ignored
Two tools that turned a caller's reasonable query into a confidently wrong
answer, in opposite directions.
FDA_OrangeBook_search_drug: openFDA answers HTTP 404 to mean "zero results",
not "endpoint missing". With no branch for that, every empty search surfaced as
a raw client error with the upstream URL leaked, so a caller could not tell "no
such product" from "the API is down". Separately, drug_name was aliased only to
the active-ingredient field, so a brand name passed to the most natural-sounding
parameter could never match: searching drug_name="Quviviq" failed with a 404
while brand_name="QUVIVIQ" returned NDA214985 from the same index. drug_name now
covers both name forms, and an empty result is reported as an empty result. The
404-as-empty handling follows the precedent already established in
faers_analytics_tool.py rather than inventing a second convention.
CTIS_search_trials_filtered: the status schema admitted any string, so an
invalid value reached the API unchecked and came back as status=success with
zero trials, echoing the bad value in search_criteria as though it had been
honoured. The trap was sharpened by the tool's own description, which describes
statuses in words ("ongoing/recruiting"), so the most natural guess produced a
confident "there are no such trials". Verified against the same query with a
valid code: status="bogus_nonsense_value" returned 0 records where status=[4]
returns 5. Status values are now validated at input and rejected with the list
of valid codes and their meanings.
Both changes are limited to inputs that previously produced a wrong or
misleading answer; valid queries are unaffected.
* Fix Round 33: Pharos target lookup omitted the diseases and ligands its description promised
Pharos_get_target's description states it "Returns Target Development Level
(Tdark/Tbio/Tchem/Tclin), protein family, diseases, ligands, and druggability
data". The GraphQL selection hard-coded eight scalar fields and never requested
the disease or ligand ones, so FOLH1 came back with name, sym, uniprot, tdl,
fam, novelty, description and publicationCount only. Target-to-disease was not
reachable from any Pharos tool, since the sibling runs the reverse direction.
The data was one field name away: querying diseaseCounts and ligandCounts for
the same target returns 240 ligands, 1 drug, and ~23 disease associations led by
prostate cancer at 1195. The selection now requests them, so the tool returns
what it advertises.
Separately, Pharos_get_ligand_targets reports activity values as pActivity
(-log10 molar), not nanomolar, and said nothing about it. A Ki printed as
9.46 is pKi 9.46 = 0.35 nM, so a reader taking it as nanomolar is wrong by ten
orders of magnitude; the value cross-checks exactly against the suvorexant FDA
label's stated 0.35 nM at OX2R. The sibling Pharos_get_target_ligands already
documents the convention, so its wording is carried across to the description
and the value field, with the conversion spelled out. The numbers are unchanged
-- this part is documentation only.
* Fix Round 33: add offline regression tests for the Orange Book, CTIS and Pharos fixes
Each pins the specific failure the fix removed, with the transport mocked so
they cannot flake on an external outage: that an openFDA 404 is read as an
empty result rather than a dead endpoint and that drug_name reaches both name
forms; that an invalid CTIS status is rejected at input instead of returning a
successful-looking empty trial list; and that Pharos_get_target requests the
disease and ligand fields its description promises.
* Fix Round 34: the framework still blamed the caller for the parameter it injects
Round 26 added a guard so the `operation` key ToolUniverse injects into the
caller's arguments is not reported back as an unrecognized caller
parameter. That guard was computed *after* the `jsonschema.validate` call,
so it never existed in the `except jsonschema.ValidationError` handler --
which is precisely the branch that runs when validation fails and the
blame message is built. The handler fell back to the raw
`filtered_arguments`, re-introducing the exact behaviour the guard exists
to prevent:
ARCHS4_get_gene_expression {"gene_symbol": "ATM"}
before: 'gene' is a required property
(unrecognized parameter(s): 'gene_symbol', 'operation')
after: 'gene' is a required property
(unrecognized parameter(s): 'gene_symbol')
`operation` is named there despite the caller never sending it and having
no way to remove it, which sends people looking for a parameter that is
not theirs. Two personas hit this independently in round 33.
The binding is moved ahead of `jsonschema.validate`, and the two blame
paths in the error handler use it. `jsonschema.validate` itself still
receives the full `filtered_arguments`, so no accept/reject outcome
changes -- only which keys the message names.
Blast radius measured rather than argued, since this is framework code on
the path of all ~2,600 tools: a differential sweep ran
`validate_parameters` over every `test_examples` entry in the repo before
and after -- 4,354 examples, **0 newly rejected, 0 newly accepted**. The
sweep does not exercise the improved message because valid examples do
not reach the failure path; the ARCHS4 reproduction above covers that.
* Fix Round 34: gnomAD structural variants answered in the wrong assembly
gnomad_get_sv_by_gene and gnomad_get_sv_by_region accept a reference_genome
parameter, but their GraphQL query hardcoded structural_variants(dataset:
gnomad_sv_r4). That callset is GRCh38-only, so reference_genome only moved the
gene/region selector: a GRCh37 request came back with a GRCh37 gene span and a
byte-identical GRCh38 SV list in the same payload. For RHD both assemblies
returned the same 162 variants, several of them whole-chromosome-spanning
complex calls at pos 1769047 that are not in the gene under either build.
gnomAD's API keeps the SV callset independent of the gene's reference genome,
and it does publish a GRCh37 callset: gnomad_sv_r2_1 returns 13 correctly placed
SVs for RHD. The dataset now follows the requested assembly (GRCh38 ->
gnomad_sv_r4, unchanged and still the default; GRCh37 -> gnomad_sv_r2_1) via a
generic fields.derived_variables lookup rather than a tool-name special case.
The two callsets use disjoint variant-id formats and neither resolves an id from
the other, so gnomad_get_sv_detail would have rejected every id a GRCh37 caller
now receives. It routes on the id shape as well, and keeps resolving r4 ids
exactly as before.
r2_1 records leave homozygote_count, major_consequence, hemizygote_count,
consequence, chrom2, ac_hom and ac_hemi null, and populate pos2/end2 on
breakends; the return schemas were widened to the shapes actually observed.
GRCh37 callers now get 13 SVs where they previously got 162 belonging to the
other assembly.
* Fix Round 34: BindingDB advertised a similarity threshold it does not apply
BindingDB_get_targets_by_compound documents similarity_cutoff as a 0-1
threshold and echoes the caller's value back as "similarity", so a tightened
cutoff reads as a filtered result set. BindingDB's getTargetByCompound endpoint
ignores it: four chemically diverse compounds, both parameter spellings, values
from 0.4 to 1.0 and omission all return byte-identical results (imatinib 395
rows every time, gefitinib 356, aspirin 30, caffeine 48).
The value is still accepted and forwarded, but the parameter description and a
note on every response now say plainly that it was not applied, so callers
filter the returned affinities themselves instead of trusting the echo.
* Fix Round 34: CPIC recommendations were filtered by the wrong drug identifier
CPIC_get_recommendations filtered the recommendation table with
eq.RxNorm:<rxnormid>, but CPIC keys that table on drugid, and 19 of its 324
drugs are not in the RxNorm namespace. Two opposite failures followed.
Gentamicin's drugid is ATC:D06AX07, so the RxNorm filter matched none of the 33
rows on the MT-RNR1 guideline and the tool answered that CPIC has no
recommendation for it -- for the archetypal aminoglycoside, on the guideline
about aminoglycoside-induced hearing loss. It now returns CPIC's three rows,
including the strong recommendation to avoid aminoglycosides in carriers.
Lumiracoxib and ramosetron have no rxnormid at all, so the filter was skipped
and no drug filter was sent. Asking for lumiracoxib returned all 42 rows of the
NSAID guideline, every one of them belonging to celecoxib, ibuprofen, meloxicam,
piroxicam, flurbiprofen, lornoxicam or tenoxicam, presented as lumiracoxib's own
prescribing guidance. Upstream returns nothing for its identifier, which is what
the tool now reports.
The filter uses the drugid CPIC publishes, the mechanism the working sibling
CPIC_search_gene_drug_pairs already used. Drugs whose drugid is RxNorm-namespaced
are byte-identical to before. The no-rows note now names the identifier the
absence was established against, and a separate branch covers the case where no
identifier resolves rather than asserting absence.
* Fix Round 34: dbSNP labelled an allele count as a sample count
dbSNP's global_mafs encodes each study as allele=frequency/count, where the
count is chromosomes carrying the allele. The frequency tools parsed that token
into a field called sample_count and documented it as 'Sample count'. For GJB2
c.35delG a clinician read 'freq 0.0024, sample_count 12' and concluded the
estimate rested on 12 people; it is 12 carrier chromosomes out of 5008.
Arithmetic settles it on both rsIDs tested: 1000 Genomes has 2504 samples and
5008 alleles, and 0.1505591 x 5008 = 754.0 exactly, the reported token, while
x 2504 gives 377. TOPMED freeze 8 on rs429358 is decisive at 132,345 samples:
0.1553138 x 264,690 = 41,110, the reported token.
sample_count keeps its value for compatibility and is documented as the legacy
misnomer; allele_count carries the same number under the right name. dbSNP does
not publish the denominator in global_mafs, so study size still cannot be
recovered from this response, and both descriptions say so rather than implying
otherwise.
dbsnp_get_variant_by_rsid returned an unlabelled position and discarded the
previous-assembly coordinate. It now labels the assembly and returns both
builds; for rs80338939 those differ by 574 kb.
The three dbSNP return schemas described a nested /beta/refsnp payload the tools
have never returned; they now describe the flat esummary shape actually emitted.
* Fix Round 34: a page of results reported as the total, and stats read from keys that do not exist
ReactomeContent_search computed total_results as the length of one unpaginated
upstream page, so a search matching 455 human pathways reported 10 -- exactly
Reactome's default page size. The schema already documented the field as 'Total
number of results', so this was a miscomputation of an existing field rather
than a change of meaning; it now carries upstream numberOfMatches. The response
also gained count, start, has_more and a truncation note, and the tool exposes
rows/start so the other 445 are reachable at all. Reactome's offset parameter is
spelled 'Start row', with a space: passing 'start' is accepted and silently
re-serves page 0, so the caller-facing name is mapped to the wire name and a
test pins the spelling.
ClinicalTrials_get_database_stats read totalStudiesCount and averageByteSize;
the API publishes totalStudies and averageSizeBytes. Both fields were null on
every call while largest_studies populated from the one correctly-named key, so
the tool looked healthy. They now return 597913 and 17275, matching the live
API. clinicaltrials_tool.py had the same wrong averageByteSize key with the
right totalStudies key, so the two implementations disagreed with each other;
they now agree. The percentiles and range buckets the description promises were
being dropped and are now returned.
A fixture in the schema-shape test asserted these two fields were 'legitimately
null upstream'. They never were -- that was this bug -- so the fixture now
carries the real live shape.
* Fix Round 34: FDA label deduplication destroyed distinct labels and the count did not match the list
The drug-label tools deduplicate on set_id or id or brand|generic. A guard meant
to add set_id/id checks the requested return fields, but for these tools the
openfda names arrive through search_fields instead, so it never fires. When a
label's openfda block is empty -- common for older SPLs -- every record hashed to
the literal key "|" and collapsed into one.
Asking for amifostine warnings returned a single document while meta.total said
4. Upstream has four labels with three distinct warnings texts, and the one
destroyed was the branded Ethyol label, which is the document a clinician
prescribing the radioprotectant actually wants. The dedupe key now falls back to
the record's content hash, so identical labels still collapse and different ones
survive: three records, one genuine duplicate removed.
meta.total is the upstream pre-deduplication hit count and was never reconciled
with the list, so a caller reading total 132 alongside 5 results could only
conclude 100 were returned and 32 remained. It keeps its value and meaning, and
the response now also carries result_count and duplicates_removed, which
reconcile against it.
Records whose openfda names are null carried a note telling the caller to check
exactly those fields to confirm the drug identity -- advice that could not be
followed. spl_product_data_elements, which upstream populates on precisely those
records, is now returned so the check is possible.
Pages are deduplicated independently and can overlap, which the disclosure note
now states rather than leaving a caller to sum counts across pages.
* Fix Round 34: three lists that were windows onto the data but read as the whole of it
PubMed_search_articles caps author lists at five and said nothing about it. A
paper with nine authors and a paper with five were byte-indistinguishable, so a
bibliography built from the search results was silently wrong. The full count is
in the same esummary payload the truncation is applied to, so each article now
carries author_count and authors_truncated: PMID 34202135 reports 9 and true,
PMID 41541570 reports 5 and false.
HPA_get_cancer_prognostics_by_gene returned only cancers where the association
reached significance and called the length prognostic_cancers_count, with no
denominator. For PRKDC that is 5 rows out of 31 cancers assessed. Head and neck
squamous cell carcinoma was assessed at p = 0.286 and simply absent, so a
researcher could not tell a citable negative result from missing data. The
assessed and non-significant counts are now reported, the non-significant rows
are returned in their own array with their p-values, and prognostic_summary is
unchanged.
XMLTool caps every nested list at 25 entries in raw document order. Labetalol
has 1855 interactions and nifedipine sits at index 71, so the interaction that
matters for a pregnant patient on both drugs was unreachable from either side
and the empty list read as no interaction. The cap is shared by every nested
list of every XMLTool and is unchanged; each list now reports its total, shown
count, offset and truncation, and the interactions tool takes optional
nested_contains and nested_offset so a specific pair can be looked up. Omitting
both reproduces the previous output exactly.
A test asserting exact equality on the echoed search_parameters was updated for
the two new inert echo keys, keeping its original three values pinned exactly.
* Fix Round 35: variant coordinates returned in an unnamed reference frame
MyVariant.info answers in hg19/GRCh37 by default and the tools never said so.
rs4244285 came back as chr10:g.96541616G>C while every neighbouring tool --
Ensembl, dbSNP, ClinVar, VariantValidator -- reports GRCh38 94781859 for the
same variant, a 1.76 Mb difference with nothing in the payload to say which
frame was meant. There was no way to ask for the other one either: `assembly`
appeared nowhere in mygene_tool.py or biothings_tools.json, though sixteen
other tool modules already handle it explicitly.
MyVariant.info does support assembly selection -- ?assembly=hg38 returns
94781859, matching Ensembl. All three tools now take an optional `assembly`
(hg19 default) and every response carries `coordinate_assembly`, naming both
the UCSC and GRC spelling because clinicians use both. The hg19 path is
byte-identical: the upstream parameter is omitted entirely rather than sent
explicitly. On the two id-based tools an HGVS variant_id must be expressed in
the assembly being requested -- MyVariant looks ids up verbatim and never lifts
coordinates over -- so the 404 that results from a mismatch now names that as
the likely cause instead of returning a bare request-failed string.
MyVariant also files more than one record under a single rsID, and resolving
rs267606617 (MT-RNR1 m.1555A>G, aminoglycoside ototoxicity) through /variant/
returns the metadata-only chrMT:m.1555A>G record. A tool whose entire purpose
is pathogenicity scores therefore answered "no scores" for a well-established
pathogenic variant while the sibling chrMT:g.1555A>G record carried CADD phred
9.793 and ClinVar Pathogenic assertions. The primary record is unchanged; when
an rsID resolves to a record carrying none of the requested annotation, the
sibling ids, their records and an explanatory note are added alongside it. The
extra request fires only on a path that was about to answer with nothing, and
this is not exotic -- of fifteen common pharmacogenomic rsIDs, seven resolve to
a record that under-reports what /query returns.
* Fix Round 35: a tool-computed category presented as source data, and a key the
source never populates
HPA's expression tools returned `expression_level` -- "Very high", "High",
"Medium" -- immediately beside `expression_value`, `expression_unit` and
`source_field`, where `source_field` literally names an HPA column. Every
neighbouring key was genuine HPA provenance; that one was ToolUniverse's own
banding of the nTPM number, with cut-offs at 50, 10, 1 and 0.1 that HPA does
not publish. Nothing marked the difference, so "Very high" read as an HPA
classification. The thresholds and the level strings are unchanged; each
response now carries `expression_level_basis` stating that the category is
computed by ToolUniverse and giving the exact cut-offs, so a reader can
reproduce the banding or discard it. It is placed once per response rather
than on every row, and omitted entirely when nothing was banded, so a query
that matched no tissue does not acquire phantom cut-offs. No claim is made
about HPA's own classification scheme, which was not verified.
A fifth banding site turned up during the work: HPAGetContextualBiologicalProcessTool
carried its own inline cut-offs with different vocabulary and a different
ladder, feeding both `functional_relevance` and the prose conclusion, which
made it the site where the invented bands travelled furthest. Its thresholds
and wording are preserved verbatim and it is now disclosed the same way. The
`expression_level` in HPA_get_comprehensive_gene_details_by_ensembl_id comes
from HPA's own XML level element and is deliberately left alone.
Separately, EnsemblVar_get_population_frequencies emitted `count` on every
population entry and it was null on every one of them, forever: Ensembl's
variation endpoint publishes no such field. For rs4244285 that is 148 entries,
146 keyed (allele, allele_count, frequency, population) and two keyed without
allele_count -- `count` appears zero times. A null sitting beside a populated
allele_count invites the reading "measured, sample size unknown", which is not
what the absence means. The dead key is removed from the response and the
return schema. The two TOPMed rows that genuinely lack `allele_count` keep
their null there, because that one is real missing data.
* Fix Round 35: a stratifiable subset reported as the total number of reports
FAERS_stratify_by_demographics summed the openFDA count facet and published
the result as `total_reports`. openFDA computes a facet only over records that
populate the counted field, so for ondansetron stratified by age that sum is
25,424 -- the reports where patient.patientagegroup is recorded -- against
121,838 reports actually matching the query. A clinician reading `total_reports`
concludes the drug has around 25,000 reports and understates it nearly
fivefold. Coverage is not a constant to correct for either: the same tool on
ibuprofen with gastrointestinal haemorrhage, stratified by sex, covers 3,543 of
4,008 reports, 88.4% against age's 20.9%.
The percentages are right as they stand -- a share of the stratifiable subset
is the correct denominator for a stratification -- so they are untouched, and
`total_reports` keeps its value for backward compatibility. The true total for
the same query is now fetched and reported as `total_reports_matching_query`,
the subset is repeated under the self-describing `stratified_report_count`, and
`coverage_note` names both figures, the coverage fraction and the field, and
states plainly that `total_reports` is not the drug's report count. If the
extra request fails the total reports null with an explanation rather than
turning a working call into an error.
The total is fetched through the existing _get_faers_count helper rather than a
second query builder, and the query construction both paths share is extracted
into _faers_search_query, so the facet query and the total query cannot drift
apart structurally. The first draft of this fix did duplicate the helper and
lost its 404-as-zero handling in the process, which would have reported a
genuine zero-match query as a failed lookup.
* Fix Round 35: sibling gnomAD tools answered from different callsets without
saying so, and CPIC's allele frequencies were discarded
gnomad_get_variant, gnomad_search_variants and gnomad_get_region default to
gnomad_r3 while gnomad_get_variant_populations and gnomad_get_constraint
default to gnomad_r4, and no response said which callset had answered. gnomAD
v3 is a genomes-only callset, so under the r3 default `exome` is structurally
null for every variant -- indistinguishable from "not observed". For GJB2
c.35delG, the commonest non-syndromic-deafness carrier variant, that null hides
10,423 exome allele observations out of 1,461,600 alleles, and a residual-risk
calculation run off the r3 genome AN of 151,764 instead is wrong by an order of
magnitude.
No default is changed. Every dataset-scoped response now reports the `dataset`
it used, the `reference_genome` that dataset implies, and, where the callset
has no exome or no genome component, a `dataset_note` saying the null means
"not present in this callset" rather than "not observed" and naming the dataset
that does carry the missing half. The assembly mapping was verified against
gnomAD's own reference_genome field across all fourteen dataset ids, which
showed the property holds for all six r3 subsets and that ExAC is the mirror
case, exomes only. Matching is by prefix so future subsets resolve without a
code change. gnomad_get_region sends its own reference_genome, and the value
sent now wins over anything inferred. The dataset-to-assembly table that
already existed privately in gnomad_constraint_tool.py is now shared rather
than duplicated, and gnomad_get_variant's return schema, which pinned exome to
type null as an artefact of the r3 default and would have rejected any r4
response, is widened.
CPIC_get_alleles selected four columns and dropped the per-ancestry allele
frequencies CPIC publishes -- present on 36 of 49 CYP2C19 alleles and 183 of
208 for CYP2D6 -- which is the one thing a pharmacogenomics counselling note
needs and which no other tool here provides. Frequency now passes through with
CPIC's own biogeographic-group labels verbatim, never remapped to gnomAD
population codes, alongside `strength`, `citations` and `inferredfrequency`.
CPIC distinguishes a published 0.0 from an absent value, sometimes within one
allele's frequency map, so absences pass through as null rather than zero.
`activityvalue` and `functionalstatus` are untouched: activityvalue is null
across CYP2C19 but populated on 183 of 208 CYP2D6 alleles, so it is real
gene-dependent data, and the null functionalstatus is already disclosed in the
tool description.
* Fix Round 35: a date range quoted into matching nothing, and a seriousness flag
labelled as patient status
The documented date-range syntax returned nothing. FAERS_count_reactions_by_drug_event
with receivedate "[20141001 TO 20141231]" -- verbatim the tool's own example --
came back as an empty list flagged truncated: false, meaning "no adverse events
were reported that quarter". openFDA returns 464 for that query.
The cause was a fix applied to one of two adjacent branches. The multi-field
branch already tested _is_range and left the range unquoted, carrying a comment
explaining exactly this hazard; the single-field branch three lines below saw a
string containing spaces and quoted it, which makes openFDA match the range as
a literal. receivedate maps to a single field, so every date-bounded query took
the branch the earlier fix had missed. Four clause-building sites across all
four classes in the file had the same asymmetry. Rather than adding the check
four times, the quoting rule and the OR-grouping rule each now live in one
helper used by every site, so the two paths cannot drift apart again -- the
drift was the whole defect. A negative control confirms the new tests fail
against the pre-fix code.
FAERS_count_death_related_by_drug labelled seriousnessdeath 2 as "alive".
openFDA's field reference documents only that the value is 1 if the adverse
event resulted in death and absent otherwise, and publishes no meaning for the
value 2 that its data nonetheless returns; the enumeration that does exist
belongs to the sibling `serious` field. It is a report-level seriousness
criterion, not patient status -- FAERS records outcome in
patient.reaction.reactionoutcome, where 5 is Fatal. The terms are now "death
criterion met" and "death criterion not met", and the note records that the
reading of 2 follows the ICH E2B(R2) convention rather than anything openFDA
publishes. This relabelling is the round's one contract change.
The counts also had no denominator: for acetaminophen they sum to 413,631 of
799,284 matching reports, so 48% of reports carry no flag and are absent rather
than bucketed. A reader computes a 30% case-fatality rate from them. The true
total, the flagged subset and a coverage note are now reported, using the same
key names as the equivalent disclosure in faers_analytics_tool.py, and the note
states that these counts do not support a case-fatality rate. Disclosure is
opt-in per tool, because a report has one seriousnessdeath but many reaction
terms and a coverage fraction over a reaction facet would be meaningless.
* Fix Round 35: the query ClinicalTrials.gov ran was not the query the caller
submitted
ClinicalTrials_search_by_intervention with "donor derived cell therapy"
returned zero studies with status success. ClinicalTrials.gov returns 251 for
that phrase. The tool rewrites a plain multi-word value into a quoted phrase
restricted to named AREA fields, and that rewritten query genuinely matches
nothing, so a transplant clinician searching for tolerance-induction trials is
told there are none. Nothing in the description, the parameter documentation or
the response indicated a rewrite had happened.
The rewrite is deliberate and load-bearing. Quoting exists because Essie treats
an unquoted multi-word value as implicit-OR keywords: "cervical cancer" matches
11,215 studies loose against 2,519 quoted, and the quoted set is the one
actually about cervical cancer. The AREA restriction exists because a bare
value matches the whole study record, which is how a vedolizumab search
surfaced an unrelated oncology trial, and InterventionOtherName was added
because brand names live there. None of that is changed here; the quoting and
the AREA restriction are byte-for-byte identical and all fifty-one guard tests
from those earlier fixes still pass. The failure mode is a silent false
negative, so the answer is disclosure, not removal, and the comments now say so
to stop a future reader deleting the quoting to "fix" this.
Every response now reports `executed_query` and `query_rewrites`, so what was
asked and what ran can never differ silently again. When a rewritten query
returns zero, and only then, a second probe establishes whether a relaxed
wording would have matched and reports `relaxed_match_check` naming the count
and how to retrieve those studies -- both helpers bail out on Essie syntax, so
passing the value parenthesised reaches the 251 directly. The returned studies
are not swapped: a zero stays zero, with an explanation attached. A query that
already works is unaffected and issues no probe, and the probe requests a
single field, cutting its payload from 62 KB to 167 bytes.
All four tools on the phrase-quoting path are covered. Three pre-existing tests
inspected the last mocked request and now inspect the first, since the probe is
a legitimate second call, and one asserted exact dictionary equality on the
response and now asserts its two original facts plus that no probe fires when
nothing was rewritten. No assertion was weakened.
* Fix Round 36: FAERS counts on a multi-word drug name were 8.6x too high
The two openFDA builders that take a `medicinalproducts` list percent-encoded
each drug name themselves, before the finished query went through the single
encode every openFDA query already gets. The name was therefore encoded twice.
Established live against api.fda.gov rather than inferred from the code: the
space in "SODIUM CHLORIDE" became %2520, openFDA decoded that once, and Lucene
received the literal term SODIUM%20CHLORIDE. Its analyzer split that on the
non-word "%" and the field stayed bound to the first token alone, so the query
silently widened from one product to every product whose name merely contains
"SODIUM" -- sodium bicarbonate, sodium valproate, docusate sodium.
medicinalproduct:%22SODIUM%20CHLORIDE%22 -> 74,079 + 16,715 = 90,794
medicinalproduct:SODIUM%2520CHLORIDE -> 605,620 + 173,560 = 779,180
medicinalproduct:SODIUM -> 779,180
The last two being identical is the proof the query collapsed to the first
token. The detail path had the same defect: ("SODIUM CHLORIDE", "ASPIRIN")
totalled 9,043 reports correctly encoded and 84,200 double-encoded.
This is the dangerous direction for a pharmacovigilance count. The result was
not empty and not malformed, it was richer than the truth, so nothing looked
broken -- and a disproportionality ratio built on it is inflated in exactly the
direction that manufactures a safety signal.
Drug names now go through the same `_render_clause` helper as every other
value, which Lucene-quotes a multi-word term and leaves percent-encoding to the
one place that does it. Single-token names are byte-identical to before
(ASPIRIN: 435,388 + 182,287, matching the API called directly).
Affects the six FAERS_count_additive_* tools and
FAERS_search_reports_by_drug_combination.
* Fix Round 36: ClinGen coordinates carried no frame and lost their only identifier
ClinGenAllele_lookup_hgvs returned genomic coordinates in two ways a caller
could not act on safely.
The coordinates are 0-based half-open (interbase) and nothing said so. For
NM_000059.3:c.1234C>G the tool reported start 32332711, while the same record's
own external_records.MyVariantInfo_hg38 is chr13:g.32332712C>G. Reading `start`
as a genomic position is a silent off-by-one, and there was no label anywhere in
the output or the description to warn against it.
Upstream also supplies a `referenceSequence` per allele, which the tool dropped.
That mattered most for the row that carries no assembly at all: upstream it has
no referenceGenome and no chromosome, so after the drop it rendered as
chromosome null, reference_genome null, coordinates 22232/22233 -- numbers in a
frame with nothing left to identify it. referenceSequence (RS002532, NG_012772.3
/ LRG_293) was that row's only identifier.
Both are now surfaced: `reference_sequence` on every genomic and transcript
allele, and an explicit `coordinate_system` label on every row that carries
coordinates. The descriptions and return_schema state the convention and the
start+1 rule.
Purely additive. No existing field is renamed or removed and no existing value
changes -- `start` is still interbase, it is now labelled rather than
renumbered. Verified against reg.genome.network directly.
* Fix Round 36: gnomAD gene/transcript coordinates named no assembly
Round 35 made gnomad_get_variant echo the callset it actually answered from.
The three sibling tools that return coordinates -- gnomad_get_gene,
gnomad_get_transcript, gnomad_search_genes -- were left out because they take
no `dataset` parameter. They still return `chrom`/`start`/`stop`, and still
never said which assembly those numbers were in.
The ambiguity is not academic. The same gene, same tool, same arguments:
gene(gene_symbol:"BRCA2", reference_genome: GRCh38) -> start 32315086
gene(gene_symbol:"BRCA2", reference_genome: GRCh37) -> start 32889611
574 kb apart, and nothing in the response distinguished them. A caller
comparing these against GRCh37 variant calls had no way to notice.
The assembly is now taken from gnomAD itself rather than assumed: schema
introspection confirmed `Gene` and `Transcript` both expose a
`reference_genome` field, so the query asks for it and the tool reports what
the API answers. `GeneSearchResult` has no such field, so gnomad_search_genes
echoes the assembly its request carried -- still the value in play rather than
a constant, and it matters there too (gene_search "BRCA2" returns
ensembl_version 17 on GRCh38 and 10 on GRCh37).
Purely additive: no field renamed or removed, no value or default changed, the
requests otherwise byte-identical apart from selecting one extra field.
One pre-existing test pinned the exact old response shape
(`data == {"gene": {"symbol": "GJB2"}}`) and could only pass while the assembly
stayed hidden. Its real intent -- that dataset-scoped keys are not injected
into queries that have no dataset -- is kept as a dedicated test.
* Fix Round 36: an advertised ClinicalTrials filter that could never work
ClinicalTrials_get_field_values documented `query_cond` as "Optional condition
filter to restrict value counts". The /stats/fieldValues endpoint accepts no
such filter, so every use of the parameter was a hard upstream 400 -- 100% of
the time, since the tool was released. Two personas hit it independently this
round, both while asking the same reasonable question: what age groups do
trials for this condition cover.
Probed live across 14 parameter spellings: only `fields` and `types` return
200. Every query.*, filter.* and postFilter.* form answers
"Invalid prefix in parameter name"; `cond`, `query`, `aggFilters`, `pageSize`
and `countTotal` answer "is unknown parameter". The /stats/field/values alias
behaves identically. The endpoint counts values across the whole registry and
cannot be scoped at all.
The parameter is now rejected at input, before any request is built, with an
error that names the working alternative (search_studies with query_cond, then
tally the per-study fields it does return) and is honest that StdAge is not
among them.
The parameter is deliberately KEPT in the schema rather than removed. This tool
silently drops unrecognized arguments, so deleting `query_cond` would have
turned a loud 400 into a silent registry-wide answer that the caller believes
is condition-scoped -- trading a visible failure for a wrong answer dressed as
success.
Input validation only. The unfiltered call is byte-identical before and after
(ADULT 551190, OLDER_ADULT 453760, CHILD 115847), pinned by a test that asserts
the outgoing params are exactly {"fields": "StdAge"}.
* Fix Round 36: FAERS facets that sum above their own total said nothing about it
openFDA's `count=` facet counts VALUES, not reports. A field nested under
patient.reaction[] or patient.drug[] is recorded once per reaction or per drug,
so one report lands in several buckets and the rows sum ABOVE the number of
matching reports. Measured live for vasopressin, 4,741 matching reports:
patient.reaction.reactionmeddrapt 29,070 = 613%
patient.drug.medicinalproduct 71,874 = 1516%
patient.drug.drugindication 21,895 = 462%
patient.drug.drugadministrationroute 9,273 = 196%
patient.drug.drugcharacterization 8,266 = 174%
patient.reaction.reactionoutcome 5,514 = 116%
occurcountry 3,949 = 83%
patient.patientagegroup 906 = 19%
Only one tool in this file disclosed a denominator. The rest returned the rows
bare, so the obvious next step -- case fatality = 1,477/4,741 = 31% for the
outcome facet -- silently divides a per-reaction numerator by a per-report
denominator. The distortion is invisible in the rows and, worse, runs in BOTH
directions: report-level fields sum below the total instead, because reports
missing the field are dropped from the facet rather than bucketed as unknown.
A caller could not tell which case they were in.
Eleven count tools now report `total_reports_matching_query`,
`stratified_report_count` and a `coverage_note`, reusing the disclosure and the
fail-soft `limit=0` denominator probe that already existed here. Each tool
declares its count field's cardinality, so the note names the actual field, the
actual direction, and whether the rows double-count -- rather than a generic
warning that would be wrong half the time.
The six FAERS_count_additive_* tools are deliberately excluded: their run()
builds its own query and never reaches this code path, so declaring the keys
there would be a claim the tools do not honour. A test pins that they carry
neither.
Purely additive -- every existing row, result_count, limit and truncated value
is byte-identical, pinned by a test that runs the same tool with disclosure
forced off and asserts the added key set is exactly the three disclosure keys.
One pre-existing test asserted the opposite contract (that reaction tools must
NOT disclose a denominator). It could only pass while the hazard was hidden, so
it is overturned with the reason recorded. Three other test files needed a stub
for the denominator probe, which goes through request_with_retry rather than
requests.get and so escaped their existing mocks.
* Fix Round 36: Open Targets reported a row count it had not returned
OpenTargets_get_drug_adverse_events_by_chemblId answered CHEMBL521 with
`count: 55` sitting immediately beside 25 rows, and nothing anywhere in the
response flagged the gap. Either number read alone is misleading: the caller
takes 55 as the size of what they were handed, or takes the 25 rows as the
complete set. The 30 dropped signals for ibuprofen include toxic epidermal
necrolysis, urticaria, systemic lupus erythematosus and hepatic enzyme
increased -- the rows a safety reviewer is there for.
The 25 is Open Targets' own default page size, not anything set here. Probed
against the API directly, bypassing ToolUniverse: page omitted -> 25 rows;
page explicitly null -> 25 rows; page size 60 -> 55 rows. The tool declares
`page` as an object, so the flat-`size` default injection in GraphQLTool never
applies and $page reaches the API as null.
Every `count` + `rows` container now carries `returned` and `truncated`, with a
`metadata.truncation_note` naming the real total, the page size, and the exact
argument to retrieve the rest. The note also says plainly that rows missing
here are absent from THIS RESPONSE only and are not evidence of absence from
Open Targets.
The disclosure lives in OpentargetTool.run, so it covers the whole family. Also
found silently truncating and now disclosed: publications by disease
(25/106,922), by target (25/347,754), by drug (25/41,523), target interactions
(25/8,626), diseases-phenotypes (25/5,638), associated targets (50/6,289),
target-disease evidence (25/153). Containers that were already complete get
`truncated: false` and no note. baselineExpression keeps its own page-index-aware
flag -- the generic pass skips containers that already carry `truncated`, so an
index-blind flag cannot overwrite a correct one.
The page-size hint clamps to 3,000: the API rejects larger
("you used size 8626 but the size must be between 0 and 3000"), so the note
never advertises an argument that would fail.
Purely additive. Rows, their order, count, criticalValue and the page size are
unchanged; the description edits keep the original text verbatim as a prefix.
* Fix Round 36: a label section absent by format read as "this drug has no warnings"
FDA_get_warnings_by_drug_name answered Giapreza with status success,
meta.total 1, and:
warnings: null boxed_warning: null
Read literally, angiotensin II has no warnings. It is a vasopressor whose
defining safety issue is thrombosis, and the label says so -- just under a
different key. Confirmed against openFDA directly: the record carries no
'warnings' and no 'boxed_warning' field at all, and does carry
warnings_and_cautions = "5. WARNINGS AND PRECAUTIONS There is a potential for
venous and arterial thrombotic and thromboembolic events in patients who
receive GIAPREZA. Use concurrent venous thromboembolism (VTE) prophylaxis."
The cause is a label-FORMAT split, not missing data. Modern PLR-format labels
(2006-) file this content under warnings_and_cautions and carry no 'warnings'
section; legacy and OTC labels do the reverse. A tool that reads only one of
the two returns a structurally-null answer for every drug of the other vintage
and reports it as success.
When a requested section is absent, the response now says so explicitly, names
which related sections the matched label does carry, surfaces that content
under its own section key, and points at the sibling tool. The note ends with
the sentence that matters: do not read a null section as "this drug has no
warnings".
Purely additive. `warnings` and `boxed_warning` keep their existing null
values; nothing is renamed or removed. A drug whose 'warnings' section IS
populated is byte-identical to before and gets no note -- verified live on
aspirin as a control.
* Fix Round 36: declare the Open Targets disclosure keys the code already emits
The row-truncation disclosure added earlier this round emits `returned`,
`truncated` and `metadata.truncation_note`, and the tool descriptions were
rewritten to promise them -- but the return_schemas were only partly updated.
`metadata.truncation_note` was declared in no OpenTargets schema at all. This
round introduced that gap; closing it here.
Four of the affected schemas turned out to describe ONLY an error payload
(`get_target_interactions_by_ensemblID` and the three publications tools), with
no success shape whatsoever, so declaring the disclosure keys meant declaring
the container they live in as well.
The disclosure helper is generic, so the audit covered the whole family rather
than the tools named in the original report: 11 tools now declare the keys.
Nothing hard-failed before -- no schema sets additionalProperties: false -- but
a description that promises a key the schema does not declare is precisely the
docs-versus-behaviour drift this harness exists to catch.
No description was edited: every promise checked out against what the code
actually emits. The keys are correctly absent when nothing is truncated, and
`target_disease_evidence` on a disease with no evidence rows emits neither,
because the empty list is pruned before the disclosure pass.
Strictly additive: 592 insertions, 0 deletions, no Python touched, no wrapper
needing a hand-edit (no description changed and every root schema type stayed
"object"). Live responses for all eight tools are byte-identical before and
after under sort_keys serialisation.
Also declared `metadata.datasource_rename_note` on get_evidence_by_datasource,
which was likewise emitted but undeclared.
Left unfixed and recorded: get_drug_indications_by_chemblId,
get_known_drugs_by_drug_chemblId, get_approved_indications_by_drug_chemblId and
get_associated_diseases_by_drug_chemblId emit at `drug.indications` while their
schemas name a different container, and get_associated_targets_by_drug_chemblId
declares a `linkedTargets` container its query never produces. Those are
pre-existing naming mismatches and correcting them means altering existing
properties rather than adding to them.
* Fix Round 37: the CLI build test rewrote a tracked source file
tests/tools/test_tu_cli.py::TestBuild called the real cmd_build, which runs
generate_lazy_registry.main() -- and that wrote straight into the tracked
src/tooluniverse/_lazy_registry_static.py, with no parameter to redirect it.
Step 2 also littered the caller's CWD with .tooluniverse/coding_api. Running
the suite therefore dirtied the working tree, the same class of defect fixed
for generate_tools in #497.
The orchestration test now stubs both generators and asserts what it is
actually about: that both steps ran, that step 2 received the resolved output
directory, that both labels reached stderr and that stdout stayed clean.
Stubbing alone would have left generate_lazy_registry with no coverage at all,
so main() gains an optional output_path whose default is byte-identical to the
previous hardcoded path, and a new test runs the real generator into tmp_path.
Verified: the build tests pass, and _lazy_registry_static.py, tools/ and the
working directory are all untouched afterwards.
* Fix Round 37: entities_ratio sat next to found/total but was neither
Reactome's per-pathway 'ratio' is the pathway's share of the entity universe
for its species -- entities_total / 16251 for human. Emitted as entities_ratio
directly beneath entities_found and entities_total, it read as found/total, the
enrichment coverage a reader of these results actually wants.
The error runs both ways, so it is not a bias a caller could mentally correct:
TP53 Regulates Transcription of DNA Repair reports 0.0052920 against a true
coverage of 0.08140, understating 15.4x, while Generic Transcription Pathway
reports 0.0968556 against 0.00572, overstating 16.9x. Because the divisor is
constant within a species, ranking pathways by entities_ratio ranks them by
size and carries no information about the submitted identifiers at all.
Additive: entities_ratio keeps its exact value as a deprecated alias, and the
two quantities it was confused for now have their own names -- entities_coverage
(found/total) beside the counts, pathway_size_fraction_of_reactome away from
them. p_value and fdr remain the enrichment statistics.
Also completes the item schema for the five tools sharing this formatter;
ReactomeAnalysis_token_result declared no items at all, so paging past the
first page returned rows with none of this documented.
* Fix Round 37: FAERS_compare_drugs judged two signals without showing either
The tool returned a verdict -- 'LORAZEPAM and DIAZEPAM show similar-strength
detected signals' -- with no case counts anywhere in the payload. An ROR of 23.5
built on 35 co-reported cases and one built on thousands are not comparable
evidence, and the verdict sentence flattened that distinction away. Recovering
the counts took a second and third call, and nothing said they were needed.
_compare_drugs already calls _calculate_disproportionality per drug, which
builds the 2x2 table and then dropped it, so this is a pass-through rather than
new arithmetic or an extra request. The table is forwarded verbatim, keeping the
key names identical to the sibling single-drug tool.
Adds comparison_caveat, which names any arm resting on fewer than 25 co-reported
cases. 25 is where an arm's own 95%% interval becomes narrower than the 1.5x band
the verdict uses to separate 'similar-strength' from 'stronger': with b, c and d
in the tens of thousands upward, exp(1.96/sqrt(a)) < 1.5 gives a > 23.4.
Additive: comparison, ROR/PRR/IC and both signal_detection blocks keep their
exact values. Verified the arms match the standalone tool cell for cell --
lorazepam a=6564, diazepam a=4310, ROR 2.394 and 2.31 unchanged.
* Fix Round 37: six FAERS multi-drug count tools never reached the coverage disclosure
FDACountAdditiveReactionsTool.run() returned _build_count_envelope directly, so
the six additive tools disclosed truncation but never the denominator their rows
should be read against. FAERS_count_additive_adverse_reactions for VASOPRESSIN
sums to 18,379 across its 100 rows while only 4,676 reports match the query --
393%%. Nothing in the payload said so, and dividing a row by that sum to get a
share is the reader's obvious next step.
The class had drifted into a near-copy of its parent: its own query building,
its own URL, and a requests.get with no timeout at all. It now overrides only
_build_search_query and delegates run() to the parent, inheriting the 30s
timeout, the 404-as-no-matches path, the truncation envelope and
_add_coverage_disclosure together. Net 89 lines removed.
The drug list is withheld from the dict handed to the parent rather than popped,
because run() passes the same dict to the facet and to the denominator probe;
popping would have left the probe counting reports for the filters alone.
Each config declares count_field_cardinality matching the sibling single-drug
tool for the same field, and the three disclosure keys are now declared in
return_schema -- reversing the earlier pinning test, which asserted these tools
must not claim a disclosure they could not make. They can now.
Also restores hermeticity in test_openfda_drug_name_encoded_once.py: it mocked
requests.get only, while the probe goes through request_with_retry ->
requests.request, so enabling disclosure made eight tests hit api.fda.gov for
real. Verified with a guard on requests.request that the whole unit suite now
makes no outbound call.
* Fix Round 37: a label lookup for a vaccine returned naproxen, reported as success
FDA_get_drug_label for 'yellow fever vaccine' returned Naproxen -- brand, generic,
boxed warning, contraindications, all NSAID -- with status success and metadata
echoing the vaccine name back. FDA_search_drug_labels answered 'typhoid vaccine'
with isoniazid and eculizumab. Nothing in either payload could be used to detect
the substitution.
_query_drug_fields tried each field quoted, then unquoted. openFDA binds the
field prefix to the FIRST token only; the rest become free text over the whole
document. Measured: generic_name:"yellow fever vaccine" is NOT_FOUND, while the
unquoted form returns 87,153 -- 33.3%% of the 261,639-label corpus -- although
every token bound to the field yields at most 15 + 4 + 0 = 19. The extra hits are
'fever' and 'vaccine' matching anywhere in the label text.
The fallback's stated purpose, catching salt forms, was never real: quoted and
unquoted return identical counts for single-token names (tofacitinib 29/29,
mefloquine 3/3), because openFDA analyses these fields, so the quoted form
already resolves TOFACITINIB CITRATE. Gating the fallback on a single token
would also have missed co-trimoxazole -- one whitespace token, two Lucene terms,
which previously returned a sunscreen.
Every term is now bound to the field: exact phrase first, then all tokens AND-ed
inside the field. This only fires after the phrase query returns nothing, so it
cannot dilute precision, and it adds the recall the old form pretended to --
'trimethoprim and sulfamethoxazole' returned SILICEA, a homeopathic remedy, and
now returns SULFAMETHOXAZOLE AND TRIMETHOPRIM.
The indication path built its query from raw user text the same way five lines
away; both now route through one phrase helper.
Contract change, narrowly: a multi-word name with no match returns not-found
with a suggestion instead of success with a wrong drug.
* Fix Round 37: ClinVar region search reported its own page size as a count
n_candidates was len(idlist) from an esearch pinned at retmax=500, so it read
500 for every busy window regardless of how many records matched. Measured
against Entrez directly: the 2 kb window has 942 matching records and the
default 4 Mb window 15,327, so the tool inspected 3.3%% of the candidates while
reporting a confident-looking 500.
The note then advised raising 'margin' to catch larger spanning variants, which
made recall strictly worse -- a wider window matches more records, the set is
still cut at 500 ordered by start position, so the far-upstream CNV the widening
was meant to find is pushed out. Measured 2 -> 1 -> 1 variants as margin grew.
Entrez indexes VLEN, the variant length. A record starting upstream can only
reach the region by spanning more than one base, so the upstream pad now
excludes single-base records. The default window's candidate set falls from
15,327 to 253 in the same single esearch -- no additional requests.
The filter is written as a negation rather than a positive length range because
2 records in that window carry no VLEN at all, and one of them is variation
1703527, the 1.5 Mb copy-number loss this tool exists to find. A positive range
would have dropped exactly the variant class it was meant to rescue.
Recall now rises with margin instead of collapsing: 8, 9, 11 variants, the last
including a 216 kb and a 1.8 Mb 11p11.2 deletion. Against an exhaustive scan of
all 942 records in the 2 kb window the fixed tool returns all 8 overlapping
variants, up from 2.
Reports total_available, truncated and truncation_note, matching the names
already used by cbioportal_tool for the same concept.
* Fix Round 37: the shipped skills still taught entities_ratio as found/total
tool_parameters.md stated it outright -- 'entities_ratio: Found/Total ratio' --
and an agent-facing instruction file outranks a return_schema description in
practice, so the skills were teaching the very misreading the tool fix removes.
The sample response disproved its own prose: entities_found 3, entities_total 47,
entities_ratio 0.00291, where 3/47 is 0.0638 and 47/16251 is 0.00289. The sample
values were consistent only with the size-fraction meaning, and were slightly off
under either reading, so they are recomputed rather than preserved.
Four unique files, each duplicated under skills/, plugin/skills/ and
plugins/tooluniverse/skills/. All twelve corrected together and verified
byte-identical across the three trees by checksum -- four distinct hashes, one
per unique file.
* Fix Round 37: an absent label section was blamed on the caller's spelling
FDA_get_pharmacogenomics_info_by_drug_name for codeine reported meta.total 533
with every returned row null, and a note reading 'No drug named codeine was
found; showing the closest-spelling candidate(s) (CODEINE, CODEINUM) instead'.
The whole openFDA corpus holds 524 labels with a pharmacogenomics section, so a
count of codeine labels having one cannot be 533; the true answer is 0. Same for
abacavir (66), carbamazepine (222) and azathioprine (43).
Stage C of the fallback chain built its query from spl_product_data_elements
without the _exists_ guard that stages A and B both apply, so it answered a
different question -- labels whose product blob mentions the drug -- and that
query's total became meta.total. Traced by capturing the outbound URLs: the
fourth request for abacavir is search=(spl_product_data_elements:"ABACAVIR")
with the guard gone.
The note was wrong twice over: it offered 'ABACAVIR' as the closest-spelling
candidate for 'abacavir', and abacavir plainly has labels -- 23 of them. It sent
readers to fix a spelling that was never wrong.
Codeine and abacavir are the textbook pharmacogenomics drugs, CYP2D6 and
HLA-B*57:01, both carrying FDA boxed warnings driven by genotype. That
information lives in the boxed warning, not in SPL section 12.5, so openFDA is
genuinely empty here -- and the old output read as 'no pharmacogenomic concern'.
All three stages now share one _guarded() helper so a future stage cannot omit
what C omitted. The suggestion states the name matched, names how many labels,
says the SECTION is absent, warns that this is not evidence of no concern, and
names the sections those labels do carry with the tools that return them.
Contract change: these drugs now yield the existing NOT_FOUND envelope instead
of success with null rows. Every row removed was null, and the rows also
violated the tool's own return_schema, which declares pharmacogenomics as an
array with no null. The legitimate misspelling path improves rather than
regresses -- contraindications for 'ibuprofin' went from 2,683 total with 4 of
14 rows populated to 697 with 20 of 20. Controls unchanged: warfarin 12 of 12,
ibuprofen clinical pharmacology 259/20.
* Fix Round 37: declare the suggestion key FDA_get_drug_label now returns
The not-found branch gained a suggestion in
|
||
|
|
f790bd5aa6 |
Fix Round 15: Ensembl species-default, Reactome/KEGG/NCBI non-human silent failures, FAERS rate-limit masking, PubMed/translate-id docs
Role-play testing across new personas (forensic genetics, pain-management
pharmacogenomics, livestock breeding genomics) surfaced several places where
a tool returns a plausible-looking wrong answer instead of a clear error,
which is worse than crashing outright because it looks like real data.
- ensembl_tool.py: EnsemblRESTTool auto-defaulted species=homo_sapiens into
query params even when routing a stable-ID lookup (/lookup/id/{gene_id}),
which doesn't need or want a species filter since stable IDs are already
species-specific. This silently broke every non-human Ensembl gene lookup
chained from a symbol resolver (confirmed with bovine DGAT1) with a
misleading "ID not found" error. Now only forwards species when the
caller explicitly supplied it.
- reactome_tool.py: Reactome's /data/mapping/ endpoints return HTTP 404
NOT_FOUND as their genuine "nothing found for this identifier" signal,
which the tool treated as a hard error instead of an empty result --
breaking pathway/reaction lookups for non-human protein orthologs that
simply have no directly curated Reactome entries.
- kegg_ext_tool.py: KEGG_get_gene_pathways silently returned an empty
pathway list, indistinguishable from a real negative, when gene_id was
passed as "org:SYMBOL" (e.g. "bta:DGAT1") rather than KEGG's numeric ID
form. Now resolves the symbol the same way the no-colon path already did.
- ncbi_datasets_tool.py: NCBIDatasets_get_gene_by_symbol returned a silent
empty success for taxon tokens NCBI itself flags as ambiguous (e.g.
"cattle" resolves above species level), with no way to tell that apart
from a genuine zero-match. Now surfaces NCBI's own warning message.
- faers_analytics_tool.py: FAERS_calculate_disproportionality's internal
count fetcher swallowed all exceptions -- including HTTP 429 rate limits
-- and returned 0, so a rate-limited window produced "Insufficient data:
a=0, b=0, c=0, d=0" for well-known drug/event pairs instead of surfacing
the real upstream failure.
- fda_drug_adverse_event_tools.json: documented that
FAERS_count_additive_adverse_reactions combines multiple medicinalproducts
with OR (union), not AND (co-occurrence) -- intentional aggregation
behavior, but easily misread as combination-risk counting since a sibling
tool uses AND semantics for the same-shaped input.
- pubmed_tools.json: documented that PubMed's esearch ANDs every keyword by
default, so a realistic multi-concept query silently returns 0 results
even when relevant literature exists (confirmed: an 11-word query returned
0 on PubMed but multiple relevant hits on EuropePMC for the same string).
- translate-id.md: the skill's own worked example used the wrong tool name
casing and parameter name, so following its instructions literally failed
immediately.
Checked all 9 other open round PRs (#478-486) for overlap before fixing;
skipped one duplicate (DrugBank interaction-list bloat, already covered by
round 13's PR #485). Every fix reproduced live against the real upstream
API, and re-verified after the change with no regressions (targeted
pytest: 103 passed; shipped test_examples for every touched tool still
pass).
|
||
|
|
be3ea656d1 | Fix self-review intent routing | ||
|
|
6115648636 |
Add gene liability evaluation skill (#404)
* Add gene liability evaluation skill * Keep gene liability skill host-agnostic |
||
|
|
089eb8e630 |
Fix: post-1.4.0 bug-hunt + onboarding-experience gaps (consolidates #391 + #392) (#393)
* Fix Round 140: honor HPO_search_terms max_results and Enrichr empty-libs default
HPO_search_terms: the JAX ontology search endpoint sizes result pages with the
`limit` query parameter, not `max`. The tool sent `max`, which the API silently
ignores, so every search was capped at the API default of 10 regardless of the
requested max_results, and `total_results` reported 10 even when 100 matches
existed. Send `limit`, coerce max_results robustly (handles null / 0 / negative
/ over-max without crashing), truncate defensively, and surface the true total
match count as `total_available`.
enrichr_gene_enrichment_analysis: an explicit empty `libs` list -- which is the
tool's own documented example -- queried zero libraries and returned an empty
enrichment wrapped in status:success. Empty or omitted `libs` now falls back to
the default library set. Also return `data` as a structured object instead of a
double-JSON-encoded string, fix `return_schema` to describe the real shape, and
add request timeouts so a stalled upstream cannot hang indefinitely.
Adds regression tests for both tools.
* Fix onboarding and install-experience gaps found in a 1.4.0 dogfood
Addresses papercuts a non-CS user hits between "pip install" and a first
working tool call.
Fix: the setup guide's first example used the wrong parameter.
`tu run FAERS_count_death_related_by_drug '{"drug_name": ...}'` fails
parameter validation; the tool requires `medicinalproduct`. Corrected in all
three skill trees (skills/, plugin/skills/, plugins/tooluniverse/skills/).
This is the scripted "it works!" moment, so it errored for every user who
followed the guide verbatim.
Feature: `tooluniverse-doctor` and `tu status` now distinguish config-loaded
from runtime-ready. Loading a tool registers its JSON config; it does not
install the tool's dependencies. On a base install both reported
"2599 available / 0 unavailable / All tools loaded successfully" while
ml/visualization/bioinformatics tools failed at call time. New
`tooluniverse.extras` module reports which optional dependency groups are
missing, with per-group install commands. The all-clear message now appears
only when nothing failed to load AND every group is installed. Tool counts
are presented as an upper bound, since a few tools use an optional package
only as an enhancement and degrade gracefully. `tu status --json` exposes a
`missing_extras` field.
Docs: make `uv` an explicit prerequisite for the SDK install path and warn
against system pip. On Homebrew Python 3.13/3.14, `pip install` fails with
PEP 668 `externally-managed-environment` and `python3 -m venv` fails at
`ensurepip`; both reproduce on a current macOS box. Added troubleshooting
entries for both errors plus the "loads but fails when run" case.
Docs: lead the chat-mode setup with the two genuinely low-friction paths
(ask an existing AI agent to read setup.md, or the Claude Code plugin
one-liner) and demote hand-editing JSON to a documented fallback with a
copy-paste-safe snippet and a validation command, since "JSON syntax error"
is a top reported failure.
Docs: note that `[all]` excludes singlecell, smolagents, client, and build;
reconcile the `--refresh` vs plain `uvx` args difference between README and
the setup skill by documenting the tradeoff; and record that PyTorch
Lightning warnings during ADMET calls are normal.
Document why the base install carries `markitdown[all]` rather than
narrowing it: `convert_to_markdown` accepts an arbitrary URI with no format
allowlist, so any dropped converter becomes a silent runtime failure, and
markitdown is imported unconditionally at module scope by
unified_guideline_tools.py.
Adds tests/unit/test_extras.py, which also keeps the extras mapping in sync
with pyproject.toml.
|
||
|
|
95ea136ff9 | Release tooluniverse 1.4.0 (#385) | ||
|
|
beea85fa6b | feat(skills): add tooluniverse-antigravity-plugin skill (#354) | ||
|
|
25517a665e |
Document tu build's undocumented side effect on the installed package (#361)
Continuing the "declared vs runtime reality" audit, this time on the CLI
surface: actually ran every documented example command from
setup-tooluniverse's CLI reference table (tu status/list/find/grep/info/
run) against the current source (not the stale shared venv) -- all worked
as documented, no discrepancies.
`tu build` was the one real gap. Its own top-level help ("Rebuild the
static tool registry") and its --output flag help ("Directory to write
coding-API wrapper files into") each describe only ONE of the two things
it actually does -- confirmed by reading cmd_build() and
generate_lazy_registry.py directly: every `tu build` call unconditionally
overwrites src/tooluniverse/_lazy_registry_static.py in place inside the
installed package, THEN writes coding-API wrappers to --output. Neither
help surface, nor the setup-tooluniverse skill's CLI table, mentioned the
registry side effect -- a user running `tu build --output ./my_tools`
expecting output scoped to that directory would not expect their installed
package's internal registry file to also get rewritten.
Updated the top-level help, the --output flag help, and the skill doc row
to state both effects. Verified via `tu --help` / `tu build --help` from
this fixed source that the descriptions now read correctly.
Also ran `tu test UniProt_get_entry_by_accession` (another documented
example) -- the call succeeds and returns real data, but fails a
return_schema check. Not fixed here: that's a tool-schema drift, squarely
the territory of the project's existing tool-fix harness (50+ rounds
already), not a "docs describe something nonexistent" bug like the CLI
gap above.
|
||
|
|
e9d8d8164b | Release v1.3.2 | ||
|
|
8228a7cd78 |
Fix plugin/skill sync drift: unreachable skills, stale content, sync gaps [release:patch] (#358)
* Fix plugin/skill sync drift: unreachable skills, stale content, sync gaps [release:patch] Auditing why tooluniverse-self-review was invisible (fixed separately via plugin auto-update) surfaced several related drift bugs in how skills reach the shipped Claude Code / Codex plugins: - 12 real skills (self-review, phewas, mendelian-randomization, fastq-qc, clinical-risk-scoring, product-safety-surveillance, natural-product-dereplication, and 5 protein-variant-mechanism skills) existed but were never added to the router's routing table, so natural- language queries could never dispatch to them. Added routing table entries for all 12. - tooluniverse-claude-code-plugin's "What you get" table was missing 3 of 8 slash commands (verify-references, self-review, setup-keys). - plugin/skills/ (shipped) had gone stale relative to skills/ (canonical) for 4 skills — missing LDSC heritability/genetic-correlation tools and sequence deep-learning variant-effect tools (AlphaGenome, Enformer, Borzoi, ChromBPNet, Evo2) that are already registered and real. Verified every referenced tool actually exists in the registry before syncing. - plugin/sync-skills.sh and scripts/sync-codex-plugin-skills.sh were both missing tooluniverse-cs-setup from their exclude lists — the next time either script ran (its documented, expected maintenance workflow), it would have shipped Claude-Science-only install instructions (conda env, host.skills.* API — meaningless outside that host) into both the Claude Code and Codex plugins. Fixed both exclude lists before that could happen; confirmed via an actual run that neither leaks it now. Re-ran plugin/sync-skills.sh from the fixed canonical source to regenerate both shipped mirrors cleanly. * Fix 5 stale tool-name references found in a full skill-vs-registry sweep Swept all ~150 skills' backtick-quoted tool-name-shaped tokens against the full tool registry (2,822 active tools). After filtering false positives (Enrichr gene-set-library names, env vars, code parameters — all verified by reading context, not just pattern-matched), 5 genuine stale references survived, each CLI-verified against the registry before fixing: - tooluniverse-precision-oncology: ChEMBL_get_drug_mechanisms_of_action_by_chemblId -> ChEMBL_get_drug_mechanisms (also fixed the param name shown) - tooluniverse-variant-interpretation: ClinGen_gene_validity -> ClinGen_get_gene_validity; ClinGen_dosage -> ClinGen_dosage_by_gene (matched on param signature) - tooluniverse-statistical-modeling: FAERS_count_patient_reaction -> FAERS_count_reactions_by_drug_event (matched on param + description) - tooluniverse-rare-disease-diagnosis: STRING_interactions -> STRING_get_interaction_partners - tooluniverse-protein-interactions: SASBDB list claimed "5 tools" but listed SASBDB_get_entry three times under three fictional purposes (metadata / structural models / scattering data) plus a nonexistent SASBDB_download_data. Collapsed to the real 2 tools; SASBDB_get_entry's actual description already covers all three claimed purposes in one call. Neighboring tools in each table were spot-checked and are real -- these were isolated bad entries, not whole-table corruption. Also swept the ~150-skill corpus for tool-name PREFIXES (source/database groupings, 3+ tools) with zero textual mention anywhere: 69 registered integrations (~340+ tools -- euhealthinfo, EBIProteins, HFInference, BMRB, NeuroMorpho, etc.) have no skill referencing them at all. Spot-verified two of the largest (euhealthinfo, EBIProteins) are real, substantial integrations with no alternate-phrasing mention either. Not fixed here -- writing curated guidance for 69 domains is a much larger scope decision, reported for follow-up rather than attempted here. These tools remain reachable via find_tools/grep_tools, just without curated skill guidance. |
||
|
|
5d63bbc247 |
Add /tooluniverse:verify-references slash command
Checks whether cited references are real and accurately described against Crossref, DataCite, PubMed, OpenAlex, Semantic Scholar, and EuropePMC, with domain-specific routing (ArXiv/DBLP/InspireHEP/Zenodo) and a bounded web-search fallback for content the structured databases miss. Flags retracted papers and mismatched fields (wrong journal/year/ authors), not just missing ones. Accepts a single citation or a full .bib file / pasted reference list. |
||
|
|
ebe80d4147 |
Fix: document ArXiv_search_papers AND-query and date-filter semantics (#286)
* Fix: document ArXiv_search_papers AND-query and date-filter semantics Space-separated keywords in ArXiv_search_papers are ANDed together, so adding synonyms to broaden a search silently narrows it instead -- a query built from several relevant terms can collapse to 0-2 hits. date_from/date_to also filter on submittedDate (the paper's first version), so a recently-revised paper can still be excluded if it was originally submitted earlier. Neither behavior was documented. Clarify both in the tool's parameter descriptions (propagated to the generated wrapper), add a test_example demonstrating the OR workaround, and add unit tests pinning down the query-building and date-range behavior. Also add the same guidance to the literature-sweep skill, plus a note that SemanticScholar_search_papers 429s under load like CORE_search_papers already documented. * Fix: use domain-neutral examples in ArXiv tool/skill docs Replace the illustrative "AI Scientist" / autonomous-research-agent query examples with generic ones (machine learning / KRAS G12C, matching literature-sweep's own existing worked example) -- this tool and skill are general-purpose, not specific to any one research topic. |
||
|
|
9115346de8 |
Document how to enable plugin auto-update in Claude Code setup skills (#285)
Third-party marketplaces like tooluniverse default to no auto-update, so installed plugins silently go stale until someone runs `claude plugin update` by hand. Add a copy-pasteable step to the install flow that flips the local autoUpdate flag in known_marketplaces.json right after install. |
||
|
|
701afa3ae0 |
Add tooluniverse-biomedical-fact-lookup skill (#279)
Grounds factual biomedical MCQ answering (gene/drug/variant/disease/pathway
lookups, "according to <database>" questions) in ToolUniverse database tools
instead of model memory. Covers routing (MSigDB C6/C3:MIR:MIRDB/C3:TFT:GTRD,
DisGeNET+UMLS, OMIM, MGI, Ensembl, ClinVar, PubTator3 fallback), MSigDB set-name
conventions, gene-disease differential ("in X but not Y") recipes, and
deterministic computational procedures (ORF counts forward-strand+nested,
restriction digest fragments, genetics segregation/gamete ratios) with an
explicit compute-then-match-the-option discipline.
Companion test_snippets.py exercises every embedded code block against synthetic
fixtures (no dataset-specific values).
|
||
|
|
9b7ff91ddb |
Release tooluniverse 1.3.1: OpenTargets migration follow-through + full-registry drift fixes (#268)
* Fix ThreeDBeacons_get_annotations: treat "no annotations of type" as empty, not error
The 3D Beacons annotations endpoint returns HTTP 404 when a valid protein has
no annotations of the requested type (e.g. P04637 has DOMAIN annotations but
no BINDING annotations). The tool routed that 404 through the shared handler
and reported "No structures found for protein ..." — a misleading error for a
legitimate empty result on an annotations query.
_get_annotations now treats a 404 as a successful empty result (annotation_count
0, annotations [], explanatory note) and only raises for genuine failures, so
both test_examples pass and callers can distinguish "no data of this type" from
a real error. Non-404 HTTP errors still surface as errors.
Surfaced by scripts/test_new_tools.py; unrelated to the OpenTargets 1.3.1 work.
* Fix stale return_schema in 6 tools surfaced during 1.3.1 validation
scripts/test_new_tools.py flagged six tools as schema-invalid because their
return_schema no longer matched the live API response (the tools return data
fine; only the declared contract was stale):
- PharmGKB_get_variant_annotations: 'significance' changed from string to an
object {id, resource, term, termId} -> allow object/string/null.
- OpenFDA_search_drug_shortages: 'therapeutic_category' is now a list -> allow
array/string/null.
- OpenFDA_search_food_adverse_events: 'date_started' can be null -> nullable.
- OMA_get_genome_pair_orthologs: 'oma_group' is '' when a protein has no OMA
group -> allow string.
- ols_search_ontologies: schema only described the error branch (never the
'results' success case) and lacked 'status', so the validator unwrapped to
data=None -> describe {status, results, error}.
- ols_find_similar_terms: schema described pre-v4 fields
(terms/total_items/showing) instead of the current output -> describe
{status, term_iri, source_label, ontology, similar_terms, total, note}.
All six now pass scripts/test_new_tools.py with 0 schema-invalid.
* Fix 8 more tools from the full-registry drift scan
A resumable full scan (2779 tools) surfaced further schema/example drift. All
fixed (tools returned data fine; the declared contract or example was stale):
- ZFIN_get_gene_expression: test_example was wrapped in {description, arguments}
instead of the flat arg form, so validation saw gene_id missing -> flatten.
- DepMap_get_cell_line: cell-line metadata (model_id, model_name, tissue,
cancer_type, msi_status, ploidy) can be null -> make nullable.
- Pharos_get_ligand_targets: target 'fam' can be null -> nullable.
- alphafold_get_annotations: region 'unit' can be null -> nullable.
- DailyMed_get_spl_by_setid: return_schema described only error fields (never
the {status, xml} success case) -> describe the real envelope.
- ols_get_efo_term_descendants: schema didn't account for the {status, data}
wrapper -> describe the real envelope.
- Crossref_search_members: schema branch was a bare array, but the tool returns
{status, data:[...]} -> describe the real envelope.
All pass scripts/test_new_tools.py with 0 schema-invalid.
* Fix 6 more tools from the completed full-registry scan
The full 2779-tool scan (now complete) surfaced a final batch of clean drift:
- FlyBase_get_gene_expression, RxTerms_search_drugs, HealthConditions_search,
DiseaseNames_search: test_examples wrapped args in {arguments: {...}} (the
same anti-pattern as ZFIN), so validation never saw the required param ->
flattened to the standard flat example form.
- DBAASP_get_peptide: DBAASP now returns objects/arrays for several fields
(nTerminus, cTerminus, synthesisType, complexity as {name,...}; smiles as a
list) -> widened those return_schema field types.
- VariantValidator_format_genomic_to_transcripts: the per-variant block's
additionalProperties required object, but the API puts errors: [] there ->
allow array/string/null.
All pass scripts/test_new_tools.py with 0 schema-invalid. (ADMETAI's 8 scan
"failures" were false positives from the parallel scanner hitting thread-unsafe
torch; the tools work single-threaded.)
* Fix miRBase_get_mirna: RNAcentral /rna/{id}/{taxid} now serves HTML
The RNAcentral /api/v1/rna/{URS}/{taxid} path now returns an HTML species view
instead of JSON, so _get_rna failed with "Expecting value: line 1 column 1".
- Fetch the JSON record from /rna/{URS}/?format=json (sequence, length, ...)
and derive the species-specific rna_type/species/description from
/rna/{URS}/xrefs/?format=json&page_size=100 (filtered by taxid), reading them
from each xref's `accession` sub-object.
- The record's `publications` field is a URL, not a count -> only populate
publications_count when the API returns an integer.
- distinct_databases is now a list, not a string -> widen the return_schema in
mirna_tools.json and lncrna_tools.json (same RNAcentral field).
miRBase_get_mirna/_publications/_xrefs pass with 0 schema-invalid. (The separate
miRBase_search_mirna is unaffected: EBI Search rnacentral is returning 500
upstream, which is not a client-side fix.)
* Add OpenAlex API-key support (keys required since 2026-02-13)
OpenAlex discontinued the polite-pool `mailto` mechanism and now returns HTTP
503 "anonymous search temporarily unavailable" without an API key. Add support
for the `api_key` query parameter (free key at openalex.org/settings/api),
read from the OPENALEX_API_KEY environment variable and injected into every
OpenAlex request (both OpenAlexTool and OpenAlexRESTTool paths).
- openalex_tool.py: `_with_api_key()` helper adds api_key to all request params.
- openalex_tools.json: declare optional_api_keys: ["OPENALEX_API_KEY"] on all
10 OpenAlex tools.
- .env.template: document OPENALEX_API_KEY.
With a key configured the tools work again; without one they degrade to the
upstream 503 (unchanged).
* Rewrite CellMarker tool to use the bulk download (JSP API removed)
CellMarker 2.0 restructured its site and removed the JSP search endpoints this
tool scraped (Marker_table.jsp / CONTROL now 404, and the server 403s the
default python-requests User-Agent). The only public interface left is the bulk
marker download.
The tool now downloads Cell_marker_All.xlsx once (primary domain, with the
published IP mirror as fallback), caches it on disk, loads it into a normalized
DataFrame, and answers all four operations locally:
- search_by_gene, search_by_cell_type, list_cell_types, search_cancer_markers
The record schema (species, tissue_class, tissue_type, cell_type, cell_name,
cell_marker, source, supports) is unchanged, so the return_schemas still
validate; `supports` is derived as the number of curated records backing each
marker/cell/tissue assignment. First call downloads ~10 MB (96k markers);
subsequent calls are served from cache.
All 4 CellMarker tools pass scripts/test_new_tools.py with 0 schema-invalid.
* Remove unused _RECORD_COLUMNS constant in cellmarker_tool
* Release tooluniverse 1.3.1
* Register OPENALEX_API_KEY in the api-key catalog
The Sync API key catalog check requires an api_key_info block for every key
referenced in optional_api_keys/required_api_keys. Add the OPENALEX_API_KEY
info block (define-once, on the first OpenAlex tool) and regenerate
api_keys_catalog.json and .env.template.
* Declare openpyxl dependency for .xlsx reading (CellMarker/dataset tools)
* Sync plugin manifests and docs version to 1.3.1
|
||
|
|
e2520a9610 |
Add /tooluniverse:self-review command + tooluniverse-self-review skill (#263)
Port the Qworld Recursive Expansion Tree (RET) criteria-generation method into the ToolUniverse plugin as a self-review capability for both Claude Code and Codex. - New canonical skill skills/tooluniverse-self-review/SKILL.md: given a task/goal/question, derive weighted YES/NO success criteria via the RET (scenarios -> perspectives -> criteria), then review supplied work against them (what is missing / done well), or emit a definition-of-done checklist when no work is supplied. Faithful port; no external tool calls. - New Claude slash command plugin/commands/self-review.md -> /tooluniverse:self-review. - Synced the skill into both plugin packagings (plugin/skills + Codex plugins/tooluniverse/skills, disable-model-invocation stripped for Codex). - Documented self-review (and previously-undocumented setup-keys) in plugin/README.md so every command is listed. Tests: Codex plugin suite 143/143; Claude plugin README command-documentation test now passes. |
||
|
|
373be25d76 |
Add 28 new tool families (67 tools): local-compute analysis, ML-model gateways, and knowledge-base APIs (#260)
* Add canonical VCF variant-statistics tool (bcftools-backed)
Adds VCFStatsTool with three deterministic, local-compute operations over a
user-supplied VCF/BCF:
- VCF_summary_stats: records/SNPs/indels/MNPs, multiallelic sites, ts/tv,
per-sample het/hom/missing
- VCF_count_variants: counts after PASS/QUAL/region/expression filters
- VCF_normalize: split/join multiallelics (+ optional left-align against a
reference FASTA), reporting counts before vs after
All 12 existing VCF-related tools are database lookups (query a variant by
ID/region); none compute statistics from a user's own file, so agents rolled
ad-hoc parsers that disagreed on multiallelic splitting and indel
left-alignment. Routing those questions through bcftools stats/norm makes the
SNP/indel counts reproducible. Registered in default_config.py and
_lazy_registry_static.py; flagged requires_local_input; variant-analysis skill
points at the tools as the structured one-call form of its bcftools recipe.
* Add ROC/AUC analysis tool + compute-tool roadmap
ROC_analysis (ROCAnalysisTool): deterministic, local-compute diagnostic-accuracy
analysis for any binary classifier or continuous biomarker. Takes scores + 0/1
labels (inline arrays or a CSV) and returns AUC with a bootstrap 95% CI, the
Youden-optimal cutoff and its sensitivity/specificity, optional metrics at a
fixed cutoff, and a downsampled ROC curve. scikit-learn under the hood; no API
key; never raises. Generic by construction — conventions (positive label,
cutoff) are parameters, output is the standard sklearn result, no task-specific
assumption baked in. Registered in default_config.py and _lazy_registry_static.py;
diagnostic-test-evaluation skill points at it. 9 unit tests + 2 runnable
test_examples pass.
docs/COMPUTE_TOOL_ROADMAP.md: evidence-based map of the remaining gap. ToolUniverse
covers knowledge/DB access comprehensively (568 APIs, ~2534 tools) but only a
handful of tools compute on the user's own data; ~86 analysis scripts across 33
skills are not callable tools. The roadmap prioritizes promoting the genuinely
general ones to tools and lists net-new gaps. Includes a "generality gate":
promote the method, not the script — engine-wrappers that expose conventions as
parameters qualify; benchmark answer-sweepers (emit-every-variant-to-match-GT)
and convention/format-hardcoded scripts are explicitly excluded.
* Fix ROC tool: drop scikit-learn dependency, use pure NumPy
scikit-learn is not a core ToolUniverse dependency (numpy is), so the
sklearn-backed version returned an error on a default install and failed CI.
Reimplemented AUC as the tie-aware Mann-Whitney rank sum (verified identical to
sklearn.metrics.roc_auc_score across random tie-heavy cases) and the ROC sweep
in pure NumPy. Tool now runs with no optional deps; 9 unit tests + 2
test_examples pass, and it works with sklearn import blocked.
* Roadmap: document the framework's optional-dependency design (lazy-loader graceful skip + required_packages + pyproject extras)
* Roadmap: correct compute-tool inventory (58 across 17 classes, not 5) and drop already-existing candidates
A find_tools/registry audit found the compute layer is far more populated than
the first draft claimed: 58 local-compute tools across 17 classes, not '5 of
2539' (that count only included requires_local_input file tools, missing every
inline-data compute tool). Survival, dose-response/IC50, enzyme kinetics, PK NCA,
population genetics, meta-analysis, DESeq2, sequence utils, and drug synergy all
already exist as tools and were removed from the build list. Added a
search-before-you-build audit table; narrowed Part A to find_tools-confirmed gaps
(edgeR/limma, expression PCA, FASTQ QC, single-cell compute, dN/dS, methylation
density, network proximity, GATK calling). ROC remains a genuine gap (shipped).
* Add Network_proximity tool (Guney/Barabasi 2016, networkx)
Network_proximity (NetworkProximityTool): deterministic closest-distance network
proximity between two node sets (e.g. drug targets vs a disease module) with a
degree-matched random Z-score and empirical p. Confirmed no existing tool via a
find_tools/registry audit before building.
Pure networkx + numpy (both core deps) — no network call, no API key, never
raises. General by construction: the caller supplies the graph (inline `edges`
or a 2-column `edgelist_path`) plus `targets`/`disease_genes`, so it works for
any interactome/metabolic/custom network and does not bake in a database or
species (unlike the skill's STRING-downloading CLI script). Registered in
default_config.py + _lazy_registry_static.py; network-pharmacology skill points
at it. 8 unit tests + 1 runnable test_example pass.
* Generalize Network_proximity: measure family + domain-neutral node sets
Make the tool cover its method family and decouple it from one use case:
- add `measure`: closest (Guney 2016, default), shortest (all-pairs mean), and
separation (Menche 2015 s_AB; <0 = overlapping modules) — instead of only the
closest measure.
- rename params to domain-neutral `set_a`/`set_b`, keeping `targets`/`disease_genes`
as aliases, so the tool works for any two node sets (two pathways, two marker
sets, …), not just drug-target vs disease.
Output keys generalized (value/measure, n_set_a_in_network, missing_set_a). Still
pure networkx+numpy, deterministic, never raises. 12 unit tests + 1 test_example
pass.
* network-pharmacology skill: wire STRING->Network_proximity (verified end-to-end) + full-interactome caveat
Smoke-tested STRING_get_network -> Network_proximity: edge rows' preferredName_A/B
(gene symbols) map straight to [A,B] pairs and line up with symbol-based gene
sets natively. Documented the chain, the new measure/set_a/set_b params, and the
gotcha the test surfaced: a small query-centered subnetwork yields an
uninformative z>0/p≈1 because the degree-matched null collapses — use a large
interactome (high limit or NDEx_get_network) for a meaningful Z.
* Add Sequence_dn_ds tool (Nei-Gojobori dN/dS, pure Python)
Promotes the comparative-genomics skill's validated Nei-Gojobori (1986) dN/dS
implementation to a callable tool (verified bit-identical). Pure Python, general
(any two coding sequences), no existing tool. 10 unit tests pass.
* Add RNAseq_edger_limma_de tool (edgeR / limma-voom DE)
edgeR/limma-voom DE complementing the DESeq2 tool, following the rnaseq-deseq2
skill + DESeq2Tool precedent; R pipeline ported from the skill's validated
wrapper. Recovers all planted DE genes end-to-end. 5 unit tests.
* Add 5 ML-model / API tools (9 wrappers), live-verified
Discovered + built via api-tool-builder agents, each confirmed against a LIVE
API and exercised through test_new_tools.py (22 example runs, 0 failures):
- gnomad_get_constraint (GnomADConstraintTool) — gnomAD GraphQL gene constraint:
pLI, LOEUF, mis_z, syn_z, obs/exp LoF. No key. Adds LOEUF/Z fields the existing
gnomad_get_gene_constraints lacks.
- HFInference_classify_text / _embed_text / _fill_mask (HuggingFaceInferenceTool)
— HuggingFace serverless inference (router.huggingface.co): classification,
embeddings, fill-mask (works for protein LMs e.g. ESM2). Optional HF_TOKEN.
- IUPred3_predict_disorder (IUPred3Tool) — IUPred3 protein disorder/ANCHOR
prediction from a UniProt accession (prediction, not a DB lookup). No key.
- ALOGPS_predict_logp_logs (ALOGPSTool) — ALOGPS 2.1 ASNN model: logP + aqueous
logS from SMILES. No key.
- VEP_predict_pathogenicity_by_rsid/_by_hgvs/_by_region (VEPPathogenicityTool) —
Ensembl VEP with AlphaMissense + SIFT + PolyPhen-2 per transcript. No key;
surfaces AlphaMissense scores no existing tool exposed.
Registered in default_config.py + _lazy_registry_static.py; API-key catalog
regenerated. All return {status,...} envelopes, never raise, 30s timeouts.
* Add 4 more ML-model / API tools (TDC, IBM RXN, Replicate, DTU)
TDC oracles (live), DTU DeepTMHMM/SignalP via biolib (live), IBM RXN + Replicate
(key-gated, built+no-key-path-verified). g:Profiler already existed. All modules
import without optional deps (CI-safe). Central registration + catalog regen.
* Add Cellpose / TDC-datasets / SCREEN-cCRE tools + 5 HuggingFace task wrappers
Third discovery batch: HF extended with 5 task wrappers (live), Cellpose
segmentation (live), TDC dataset retrieval (live), ENCODE SCREEN cCRE (live;
Enformer/Borzoi infeasible). All CI-safe. Central registration.
* Add jPOST / RNAcentral / LIPID MAPS-LMPD / InterPro-memberDB + HF vision wrappers
Fourth discovery batch: 4 novel data-API tools (each pivoted around already-wrapped
APIs) + HF image-classification/object-detection. All live-verified, CI-safe.
* Add Rhea / PanglaoDB / IDR-search / GBIF-taxonomy / UniBind tools
Fifth discovery batch: 5 novel data-API tools (each pivoted around already-wrapped
APIs), owning skills updated. All live-verified, CI-safe.
* Add ZOOMA / RGD-strains / SGD-protein / HuBMAP-biospecimen tools
Sixth discovery batch (EBI/NAR + model-organism + spatial omics): 4 novel tools,
owning skills updated. All live-verified, CI-safe.
* Fix dtu_protein test_examples: flatten + success-only (validation pass)
The DTU agent wrote test_examples as a {description,arguments} wrapper plus an
intentional error case; the harness reads top-level keys as args and counts
error-status returns as failures. Flattened to a single flat success example.
The tool itself was already correct (verified: DeepTMHMM returns real topology).
* Fix correctness issues found in pre-merge adversarial audit
Independent correctness/faithfulness audit of the new tools surfaced real
defects (beyond "test_examples don't error"); fixed:
Real bugs:
- rnacentral_genome: RNAcentral_get_sequence/_get_publications called with only
urs_id silently routed to genome_locations (operation default not injected;
hardcoded fallback). Now each wrapper fixes its op via fields.operation.
- idr_searchengine: IDR_search_images mapped `study` to non-existent keys
(study_title/study) -> always null. Now reads screen_name/project_name (+ adds
dataset_name).
Method fix:
- network_proximity: degree-matched Z-score null was an oversimplified Guney 2016
(exact-degree bins => hubs map only to themselves; with-replacement intra-set
sampling). Rewrote: degree bins merged to >= 100 nodes, sample WITHOUT
replacement, empirical p uses (k+1)/(n+1).
return_schema correctness:
- Normalized 14 tools whose oneOf success-branch described the data array
directly instead of the {status,data} envelope (panglaodb actually failed
harness schema validation; others passed only by luck). Now all are proper
envelope schemas.
Honesty/labels:
- TDC LogP oracle relabeled (it is TDC penalized-logP, not octanol-water logP).
- IUPred3 academic/non-commercial license note; PanglaoDB 2020-freeze note;
screen_ccre staging-endpoint reliability note.
* Trim redundancy from the audit: drop ALOGPS + RNAcentral_get_publications, collapse VEP 3->1
Acting on the value-audit's clearest redundancy findings:
- Removed ALOGPS (2005-era logP/logS model; ToolUniverse already has RDKit,
SwissADME, and ADMET-AI for lipophilicity/solubility). Deleted tool + config +
registration.
- Removed RNAcentral_get_publications (duplicated the existing
RNAcentral_get_xrefs_and_pubs). Kept the genuinely-novel
RNAcentral_get_genome_locations + _get_sequence.
- Collapsed VEP_predict_pathogenicity_{by_rsid,by_hgvs,by_region} into a single
VEP_predict_pathogenicity that auto-routes by whichever input is given
(rsid | hgvs_notation | chrom+pos+alt) — 3 tools -> 1.
Kept gnomad_get_constraint (verified the existing gnomad_get_gene_constraints
does NOT expose LOEUF/mis_z/syn_z/pli, so it adds real value) and jPOST
(jPOST-native metadata not surfaced via ProteomeXchange). Net -4 tools (2756->2752).
* Complete audit trim: remove ALOGPS registration, RNAcentral pubs wrapper, collapse VEP wrappers
Follow-up to
|
||
|
|
7d6a182fda |
Bring remaining peptide-deorphanization work to main: ConoServer tool + router wiring (#258)
* Add cross-biobank PheWAS coverage (BioBank Japan, UKB-TOPMed, TPMI, Genebass) + skill
Discovery round: ToolUniverse covered only FinnGen for phenome-wide association
(PheWAS). This adds the missing cross-ancestry PheWAS resources plus a
replication skill.
New tools (all public, no auth):
- BioBankJapan_phewas_by_variant (pheweb.jp, Japanese, GRCh37)
- UKBTOPMed_phewas_by_variant (pheweb.org/UKB-TOPMed, European, GRCh38)
- TPMI_phewas_by_variant (sinica TPMI, Taiwanese, GRCh38)
- Genebass_gene_burden_phewas (main.genebass.org, UKB exome gene burden)
Implementation notes:
- Shared PheWebPheWASTool for the three PheWeb biobanks (config-selected).
- rsID -> coordinates via Ensembl in each biobank's build; multi-allelic SNPs
try every alt and select the allele actually present (PheWeb returns HTTP 200
with a null body for absent variants, so selection is by presence of phenos).
- Genebass joins phenotype descriptions from /phenotypes (cached).
- TPMI serves a cert that fails OpenSSL 3.x strict verification; a per-host
adapter clears only VERIFY_X509_STRICT while keeping full CA-chain + hostname
verification (untrusted/self-signed certs still rejected; not verify=False).
New skill tooluniverse-phewas orchestrates the four tools + FinnGen for
cross-ancestry replication, with allele-frequency/power interpretation guidance.
Validated live: rs7903146/TCF7L2 -> T2D replicated across all ancestries;
PCSK9 pLoF -> LDL/cholesterol; rs671/ALDH2 -> alcohol disorders in East Asian
biobanks but monomorphic in UKB-TOPMed. 12 unit tests, ruff clean.
Both registration points updated (default_config + _lazy_registry_static).
* Add de novo MSA + phylogeny tools (EMBL-EBI Job Dispatcher)
Discovery gap: ToolUniverse could fetch pre-computed alignments/trees (Rfam,
Ensembl Compara) but had no way to align a user's own sequences or build a tree
from them. The phylogenetics skill explicitly disclaimed MSA generation.
New tools (public, no auth; EMBL-EBI Job Dispatcher submit -> poll -> result):
- EBI_msa_align MSA via Clustal Omega (default) / MUSCLE / MAFFT /
Kalign / T-Coffee; returns aligned FASTA, Clustal,
and guide tree
- EBI_build_phylogenetic_tree neighbour-joining / UPGMA tree (Newick) from an
alignment, optional Kimura correction + gap trim
Notes:
- Per-method param mapping (Clustal Omega uses outfmt+stype; MUSCLE uses format
and auto-detects type; MAFFT/Kalign use format+stype).
- Dynamic result-type selection: only fetch result types the job produced.
- 30s per-request timeout; polls up to ~2.5 min.
Updated the tooluniverse-phylogenetics skill to chain these for de novo
alignment + NJ/UPGMA trees (still defers ML/Bayesian to dedicated tooling).
Validated live: cytochrome C human/horse/yeast -> human+horse closest, yeast
outgroup; globin fragments align with correct guide tree. 11 unit tests, ruff
clean. Both registration points updated.
* Add validated clinical risk calculators (compute, no API/key)
Discovery gap: ToolUniverse had data-retrieval tools across many domains but no
bedside clinical risk scores. These are pure-compute, deterministic, fully
testable (no network, no key) implementations of standard validated formulas,
each returning the score, a risk interpretation, and an auditable component
breakdown.
Calculators (ClinicalCalculatorTool, selected by fields.calculator):
- ClinicalCalc_CHA2DS2_VASc AF stroke risk (Lip 2010)
- ClinicalCalc_HAS_BLED AF bleeding risk (Pisters 2010)
- ClinicalCalc_CURB_65 pneumonia severity (Lim 2003)
- ClinicalCalc_qSOFA bedside sepsis risk (Singer 2016)
- ClinicalCalc_Child_Pugh cirrhosis severity / class A-C (Pugh 1973)
- ClinicalCalc_Wells_DVT DVT pretest probability (Wells 2003)
- ClinicalCalc_Wells_PE PE pretest probability (Wells 2000)
- ClinicalCalc_MELD_Na liver mortality / transplant priority (OPTN 2016)
- ClinicalCalc_eGFR_CKD_EPI eGFR, CKD-EPI 2021 race-free (Inker 2021)
- ClinicalCalc_ASCVD_risk 10-yr ASCVD risk, Pooled Cohort Eqns (Goff 2013)
Validated against published reference cases: ASCVD White-male-55 = 5.4%,
White-female = 2.1%, African-American-male = 6.1% (official ACC/AHA examples);
CHA2DS2-VASc, Child-Pugh class, MELD-Na, CKD-EPI all match. 17 unit tests.
No orchestration skill: each calculator is a standalone deterministic call with
built-in interpretation, so a wrapper skill would add little. Both registration
points updated.
* Harden discovery-batch tools: Child-Pugh input validation, PheWAS per-candidate resilience, skill doc fixes
- ClinicalCalc_Child_Pugh: reject unrecognized ascites/encephalopathy values
instead of silently scoring the best case (1 pt); add 'absent'/'slight'
synonyms and an enum on the JSON schema so bad input is caught at validation.
- PheWebPheWAS: a transient 5xx on one multi-allelic candidate no longer aborts
the whole lookup; only surface an error when every candidate fails to fetch
(distinguishes 'variant absent' from 'all fetches failed').
- Genebass: clarify the phenotype-description join-key docstring.
- tooluniverse-phewas skill: document that FinnGen_get_variant_finemapping needs
an explicit GRCh38 chr:pos:ref:alt string (not an rsID).
- tooluniverse-phylogenetics skill: correct phykit saturation column semantics
and list the actual phykit_batch_analysis 'function' enum values.
- Add unit tests for all new behavior (34 pass).
- Bump plugin to 1.2.7 for the skill content changes.
* Discovery: +10 keyless tools (openFDA safety, FAVOR, OPSIN, NCBI genomes) + product-safety skill
Net-new tools from a 5-frontier discovery survey (all public, no API key, live-verified):
openFDA expansion (5, BaseRESTTool config — reuses existing query machinery):
- OpenFDA_search_drug_shortages (current/resolved U.S. drug shortages, added 2025)
- OpenFDA_search_device_adverse_events (MAUDE), OpenFDA_search_device_recalls
- OpenFDA_search_food_adverse_events (CAERS food/supplement/cosmetic)
- OpenFDA_search_animalvet_adverse_events (veterinary)
FAVOR (1): FAVOR_annotate_variant — comprehensive GRCh38 single-variant functional
annotation (BRAVO/gnomAD/1000G frequencies, GENCODE consequence, CADD/SIFT/PolyPhen/
AlphaMissense/MetaSVM scores, GERP/phyloP conservation, ClinVar, regulatory) in one call.
OPSIN (1): OPSIN_name_to_structure — deterministic IUPAC name -> SMILES/InChI/InChIKey
parser (EBI, MIT); resolves systematic names absent from compound databases.
NCBI Datasets v2 genome module (3, extends existing NCBIDatasetsTool):
- NCBIDatasets_get_genome_assembly, NCBIDatasets_list_genomes_by_taxon,
NCBIDatasets_get_sequence_reports (assembly metadata/discovery/sequence mapping).
Skill (1): tooluniverse-product-safety-surveillance — multi-product post-market safety
(devices/food/supplements/cosmetics/veterinary/shortages), orchestrating the new openFDA
tools. Explicit negative boundary vs the drug-AE signal skills (pharmacovigilance).
- 13 new unit tests (47 pass across changed tools). All tools live-verified via CLI.
- Bump plugin 1.2.7 -> 1.2.8 for the new skill content.
* Discovery (batch 2): +6 keyless tools — Ensembl Tark, MARRVEL, EU CTIS
More clean, public, no-API-key resource gaps from the discovery survey:
- Ensembl Tark (2): Tark_get_mane_transcripts (MANE Select/Plus Clinical ENST<->RefSeq
NM mapping by gene/transcript id) and Tark_get_transcript (archived transcript record
with versions/checksums/releases). Fills the MANE/transcript-equivalence gap.
- MARRVEL (2): MARRVEL_get_gene (aggregated OMIM/HGNC/Ensembl/Entrez/UniProt identity)
and MARRVEL_get_omim_phenotypes (OMIM disease associations + inheritance) for
rare-disease/Mendelian triage.
- EU CTIS (2): CTIS_search_trials and CTIS_get_trial — the EU clinical-trials register
(Clinical Trials Regulation, since 2022), complementing ClinicalTrials.gov.
All 18 test_examples live-verified (real retrievable IDs); 13 new unit tests (60 pass
across changed tools). Tools-only — no plugin/skill content change, no version bump.
* Discovery (batch 3): +1 ClassyFire tool + cross-feed new tools into skills
Tool:
- ClassyFire_classify_by_inchikey — ChemOnt chemical taxonomy (kingdom->superclass->
class->subclass) by InChIKey; fills the chemical-classification gap.
Skill gap fixes (wire the new resource tools into the skills that should use them):
- tooluniverse-clinical-trial-matching: add EU CTIS (CTIS_search_trials / CTIS_get_trial)
for European/EEA trials — previously ClinicalTrials.gov only; description updated to
trigger on EU-trial queries.
- tooluniverse-rare-disease-diagnosis: wire MARRVEL_get_gene + MARRVEL_get_omim_phenotypes
into Phase 3 (gene panel; inheritance-aware filtering) and FAVOR_annotate_variant into
Phase 4 (one-call variant annotation), plus fallback-chain entries.
All cross-fed tool calls live-verified; +3 unit tests (63 pass). Plugin 1.2.8 -> 1.2.9
(skill content changed).
* Discovery (batch 4): +4 keyless tools — NPAtlas, ISRCTN + ISRCTN trial-matching cross-feed
- NPAtlas (2): NPAtlas_search_compounds (microbial natural products by name/InChIKey/
formula/SMILES via basicSearch) and NPAtlas_get_compound (full record by NPAID incl.
source organism + originating reference). Fills the microbial-natural-products gap.
- ISRCTN (2): ISRCTN_search_trials and ISRCTN_get_trial — the ISRCTN registry (WHO-primary,
UK/international), parsing the XML query API into structured records. Third trial source.
- clinical-trial-matching skill: ISRCTN added alongside ClinicalTrials.gov + EU CTIS;
description now triggers on US/EU/UK trial queries; merge-and-dedupe guidance via cross-ref ids.
Fixed during build: ISRCTN pagination param is 'limit' not 'pageSize' (verified live).
All 12 test_examples live-verified (real retrievable IDs); +9 unit tests (38 pass).
Plugin 1.2.9 -> 1.2.10 (skill content changed).
* Discovery (batch 5): +2 new skills orchestrating this session's tools
The tools added this session were referenced by zero skills — these two close that gap:
- tooluniverse-microbial-genome-characterization: genome-assembly discovery/QC/replicon
mapping for any organism via the NCBI Datasets genome tools (list_genomes_by_taxon,
get_genome_assembly, get_sequence_reports + taxonomy). Negative boundary vs
comparative-genomics (gene orthology) and plant-genomics.
- tooluniverse-natural-product-dereplication: NP dereplication + chemotaxonomy via
NPAtlas (producing organism + reference) + ClassyFire (ChemOnt class) + OPSIN +
PubChem. Negative boundary vs chemical-compound-retrieval and metabolomics skills.
Both built with skill-creator; every documented tool call live-verified; evals/evals.json
with 3 verifiable prompts each. Plugin 1.2.10 -> 1.2.11.
* Discovery (batch 6): +4 keyless tools — EPA Envirofacts + USDA PLANTS (new frontiers)
Two entirely new coverage frontiers (TU had nothing for either):
- EPA Envirofacts (2): EPA_search_tri_facilities (Toxics Release Inventory) and
EPA_search_frs_facilities (Facility Registry Service) — US regulated-facility lookup.
- USDA PLANTS (2): USDA_plants_get_profile (taxonomy/habit/native status) and
USDA_plants_get_characteristics (morphology/physiology/growth traits) by PLANTS symbol.
All 12 test_examples live-verified; +7 unit tests (45 pass). Tools-only, no version bump.
* Discovery cross-feeds: wire this session's tools into 9 existing skills
The tools added this session were under-referenced by existing skills. Additive doc edits
(no content removed), every newly-referenced tool call live-verified:
- FAVOR_annotate_variant -> variant-functional-annotation, regulatory-variant-analysis,
variant-to-mechanism (one-call GRCh38 comprehensive annotation fast-pass + fallback).
- Tark_get_mane_transcripts / Tark_get_transcript -> variant-interpretation,
acmg-variant-classification (lightweight MANE/transcript-namespace cross-check).
- OPSIN_name_to_structure -> chemical-compound-retrieval, organic-chemistry
(systematic IUPAC name -> structure; trade names fall back to PubChem).
- NCBIDatasets genome tools -> infectious-disease, comparative-genomics (assembly QC
context; pointers to the new microbial-genome-characterization skill).
Plugin 1.2.11 -> 1.2.12.
* Discovery (batch 8): +3 keyless tools — Allen Cell Types + iDigBio
- Allen Cell Types (1): AllenCellTypes_search_specimens — single-neuron electrophysiology/
morphology specimens (human/mouse) via the Brain-Map RMA API; filter by species/structure.
Distinct from the existing gene-expression AllenBrain tools.
- iDigBio (2): iDigBio_search_records (130M+ biodiversity specimen records by taxon/locality)
and iDigBio_get_record (full Darwin Core record by UUID).
All 9 test_examples live-verified (real UUIDs); +7 unit tests (52 pass). Tools-only, no version bump.
* Discovery (batch 9): +2 Pathoplexus/LAPIS tools — pathogen genomic surveillance
Found by a double-check coverage sweep (a genuine gap missed in the first two survey rounds):
- Pathoplexus_count_sequences: aggregated open pathogen genome-sequence counts by
organism, filterable by country/lineage and groupable by metadata field.
- Pathoplexus_get_mutations: characteristic amino-acid/nucleotide mutations above a
proportion threshold, for mutation-prevalence surveillance.
LAPIS-backed (lapis.pathoplexus.org); organisms west-nile/ebola-zaire/ebola-sudan/cchf/
mpox. Distinct from Nextstrain (phylo builds) and BV-BRC. Caught + handled a LAPIS quirk:
the /aggregated endpoint rejects limit/offset (unordered output).
All 6 test_examples live-verified; +6 unit tests (58 pass). Tools-only, no version bump.
* Discovery (batches 10-12): Open Genes + FooDB + TogoID tools
Three gaps found by coverage-sweep double-checks (each had zero TU coverage):
- Open Genes (2): OpenGenes_get_gene / OpenGenes_search_genes — curated aging/longevity
gene DB (mechanisms, evidence counts per study type). Cross-fed into the
tooluniverse-aging-senescence skill Phase 2.
- FooDB (1): FooDB_get_compound — food chemical-constituent by FooDB id (structure +
HMDB/KEGG/PubChem/ChEBI cross-refs); complements USDA FoodData Central.
- TogoID (2): TogoID_convert / TogoID_list_datasets — universal biological ID conversion
across 117 databases (DBCLS relation graph). Cross-fed into the translate-id command.
All test_examples live-verified; +14 unit tests. Plugin 1.2.12 -> 1.2.14.
* Discovery (batch 13): +1 AlphaFill tool — ligands/cofactors on AlphaFold models
Found by coverage-sweep batch 8 (AlphaFill's transplant data is only mentioned, not
exposed, by existing 3D-Beacons/ChannelsDB tools):
- AlphaFill_get_transplants: for a UniProt, the ligands/cofactors/ions AlphaFill
transplants into its AlphaFold model (by homology to PDB), aggregated by compound
with occurrence count, best local-fit RMSD, and source PDB entries. Reveals likely
cofactor/metal/ligand/drug binding the bare AlphaFold (apo) model doesn't show.
All 3 test_examples live-verified (ABL1->imatinib/STI; CDK2->ATP/MG/staurosporine);
+4 unit tests. Tools-only, no version bump.
* Discovery: cross-feed session tools into 7 more skills (close skill-orphan gap)
8 tools added this session were referenced by zero skills. Wired each into its natural
home (additive, every tool call live-verified):
- Pathoplexus (count/mutations) -> infectious-disease (pathogen genomic surveillance)
- AlphaFill_get_transplants -> protein-structure-prediction (ligands the apo AlphaFold hides)
- AllenCellTypes_search_specimens -> neuroscience
- iDigBio (search/record) -> ecology-biodiversity
- FooDB_get_compound -> metabolomics
- EPA TRI/FRS facilities -> chemical-safety (environmental exposure screening)
- USDA PLANTS (profile/characteristics) -> plant-genomics
Plugin 1.2.14 -> 1.2.15.
* Discovery: +1 skill tooluniverse-clinical-risk-scoring (closes calculator orphan gap)
The 10 ClinicalCalc_* risk-calculator tools were referenced by zero skills. This skill
orchestrates them: maps a clinical scenario to the right score(s), computes, and
interprets against per-score risk-band tables.
- AF -> CHA2DS2-VASc + HAS-BLED (anticoagulation tradeoff)
- cirrhosis -> Child-Pugh + MELD-Na; pneumonia -> CURB-65; sepsis -> qSOFA
- VTE -> Wells DVT/PE; CVD prevention -> ASCVD; kidney -> eGFR (CKD-EPI)
Negative boundary vs polygenic-risk-score / epidemiological-analysis /
diagnostic-test-evaluation. All 10 tool calls live-verified; evals with 3 prompts.
Plugin 1.2.15 -> 1.2.16.
* Fix 2 tools surfaced by test-harness role-play (both HIGH-severity, CLI-confirmed)
Feature-KRAS-001 — annotate_variant_multi_source returned EMPTY from every source
(ClinVar/gnomAD/CIViC) and falsely listed all of them in sources_with_data:
- parsers read the wrong nested paths — gnomAD record is at data.gene.gene_id (not
data.gene_id), CIViC variants at data.gene.variants.nodes (not data.variants).
- queried ClinVar by a bare protein change ('V600E') which matches nothing; now query
by gene/rsid and match the specific change with 1-letter->HGVS-3-letter expansion
(V600E <-> Val600Glu), with honest exact_match flag + gene-level context fallback.
- sources_with_data now reflects actual non-empty results, not 'did not throw'.
Feature-ClinPharm-001 — FDA_get_drug_label_info_by_field_value returned a DIFFERENT
drug (asked imatinib boxed_warning, got sunitinib/regorafenib): return_fields (a
projection) were passed as exists=_exists_ MATCH filters, so requesting a field the
record lacks -> NOT_FOUND -> fallback dropped the exact field:value and matched other
drugs. Fix: exists=None (projection-only); missing fields come back null.
+6 unit regression tests. Tools-only, no version bump.
* Add NCBI Clinical Tables tools (RxTerms, conditions, disease_names)
Closes the one partial gap vs the openai/plugins life-science-research
reference set: ncbi-clinicaltables was only covered for ICD (ICDTool) and
LOINC (LOINCTool). This adds the remaining high-value Clinical Tables
endpoints as keyless tools:
- RxTerms_search_drugs - drug-name autocomplete + strengths/forms + RxCUIs
- HealthConditions_search - problem-list autocomplete + ICD-10-CM/ICD-9 crosswalk
- DiseaseNames_search - disease-name autocomplete + UMLS CUI
New ClinicalTablesTool class + clinical_tables_tools.json, registered in
default_config.py and _lazy_registry_static.py. 6 unit tests (mocked) +
live CLI-verified. Tools-only change, no version bump.
* Close 7 capability sub-gaps vs openai life-science-research reference skills
A capability-depth audit of all 50 skills in openai/plugins
life-science-research found 7 endpoints used by reference skills that
TU's tool for that database did not expose. All live-verified + unit-tested:
- gnomad_get_variant_populations - per-ancestry allele frequencies (was aggregate-only)
- IPD_search_hla_alleles / IPD_get_hla_allele / IPD_search_cells - IPD-IMGT/HLA (no HLA coverage before)
- ProteomeXchange_get_spectrum_by_usi - PROXI spectrum/USI access (was dataset-level only)
- ChEBI_get_ontology_parents - upward ontology traversal (had children only)
- ols_get_efo_term_descendants - transitive EFO subtree (had direct children only)
- PubChem_get_substance_by_SID - depositor substance records (was CID-only)
- EpiGraphDB_get_literature_evidence - SemMedDB literature triples
New IPDIMGTHLATool class (registered both points); all other tools extend
existing classes (no new registration). 56 unit tests, all live-verified.
Tools-only change, no version bump.
* Fix GTEx single-tissue eQTL crash on variant-only query
run() injected gencode_id=None when no gene symbol was supplied, and
_get_single_tissue_eqtls iterated it -> "'NoneType' object is not iterable".
Variant-only eQTL lookups are valid (GTEx filters by variantId alone).
Fix: only inject gencode_id when a symbol is present, and coerce None->[]
in the handler. Surfaced by the openai-reference capability audit.
3 regression tests; live-verified (variant-only now returns real eQTLs).
* Targeted NGS-analysis skill gaps: edgeR/limma routes, fastq-qc skill, scRNA QC
Closes the in-scope subset of the openai ngs-analysis reference plugin
(the parts that fit TU's plan + run-if-available skill model; raw-pipeline
skills needing local binaries are intentionally out of scope):
- rnaseq-deseq2: + edgeR (QL-F/exact) and limma-voom routes with a routing
table, references/edger_limma_voom.md, and r_edger_limma_wrapper.py
(preflights Rscript+Bioconductor; emits install plan if absent, never fabricates).
- NEW tooluniverse-fastq-qc: FastQC/MultiQC/fastp/Cutadapt/seqkit QC with a
module-by-module interpretation table and trim/don't-trim decisioning;
run_fastq_qc.py preflights tools, writes only to --workdir, never overwrites
raw FASTQs, emits install plan if tools missing.
- single-cell: + scRNA-seq QC gating section (mito%, doublets, ambient RNA,
empty droplets, MAD thresholds) + references/scrna_qc.md + run-if-available helper.
All three follow the openai preflight pattern (no vaporware). Tests: rnaseq
100/100, fastq-qc 5/5, single-cell unchanged-baseline. Plugin skills synced;
version 1.2.16 -> 1.2.17.
* Apply PR #248 verification fixes from persona round
Surfaced by post-ship researcher-persona verification:
- HIGH: ols_get_efo_term_descendants/children resolved only EFO: CURIEs, so
MONDO/HP/Orphanet terms (modern EFO disease nodes are MONDO) returned 0
silently with a malformed URL. Map non-EFO prefixes to their native OBO/ORDO
IRI (purl.obolibrary.org/obo/<P>_<n>, orpha.net/ORDO/Orphanet_<n>). MONDO:
0004993 now returns its 773 descendants. Also add a disambiguation note when
total=0 (leaf/obsolete/cross-ontology) so it isn't read as 'no subtypes'.
- MEDIUM: remove vendor provenance text from user-facing rnaseq-deseq2 skill
(script docstring + reference doc).
3 new EFO regression tests (MONDO/Orphanet IRI + zero-note). Plugin skills
synced; 1.2.17 -> 1.2.18.
* Register gnomADGetVariantPopulations in static lazy registry
The new gnomad_get_variant_populations class was missing from
_lazy_registry_static.py, so it would not resolve in frozen/lazy
environments where source files are absent (it only loaded because a
sibling gnomAD tool imports the same module). Add the class->module entry
for consistency with the other gnomAD types.
* Close validation-coverage gaps: test_examples + schemas + Alliance expression fix
From the schema/test-completeness audit (1+2):
- Fix Alliance gene-expression: GET /gene/{id}/expression-summary was retired
(404), breaking FlyBase_get_gene_expression / ZFIN_get_gene_expression. Switch
to POST /api/expression (per-annotation stage+location); both now return data
(dpp 317, shha 350). Updated descriptions/params/return_schema + 3 unit tests.
- Add live-verified test_examples to: alphafold_get_annotations (P00533),
HPA_get_rna_expression_in_specific_tissues, ArXiv_get_pdf_snippets (1706.03762),
SemanticScholar_get_pdf_snippets, expression_anova_per_gene, coding_variant_fraction.
- Add return_schema (oneOf) to HMDB_search / HMDB_get_metabolite / HMDB_get_diseases.
Tools whose upstreams are down/blocked (ZINC bot-wall, T3DB Cloudflare-403,
SynBioHub-401, BindingDB-500, SwissTargetPrediction broken) or are async
job/token fetchers (Foldseek/SwissDock/DynaMut2/ReactomeAnalysis) were left
without examples for cause rather than given placeholder IDs. Tools-only, no bump.
* Revive ZINC tools: migrate zinc15 (bot-wall) -> ZINC22/CartBlanche22
zinc15.docking.org now serves a bot-verification HTML wall on every
endpoint, so all 5 ZINC tools were dead. Migrate to the official successor
cartblanche22.docking.org (no bot-wall):
- ZINC_get_compound / ZINC_get_purchasable -> GET /substance/{id}.json
(record + vendor/price catalogs). Verified: ZINC000000000053 (aspirin),
mwt 180.159, 450 vendors.
- ZINC_search_by_smiles -> async /smiles.json submit + /search/result/{task}
poll. Verified: benzene similarity search returns hits.
- ZINC_search_compounds (name) and ZINC_search_by_properties (MW/LogP range):
ZINC22 is structure/ID-centric with no such endpoints -> clean {status:error}
pointing to search_by_smiles (honest, not fabricated).
Descriptions/return_schemas updated; 9 unit tests; live-verified test_examples
for the 3 working tools. Tools-only, no version bump.
* Add 7 capability-depth tools (drug/pharmacology + pathway/interaction)
Capability-depth audit of core clusters (vs each upstream API's own endpoints)
found 7 keyless, live-verified endpoints TU's tools did not expose:
- GtoPdb_get_ligand_properties (structure + molecularProperties)
- GtoPdb_get_disease_associations (diseaseTargets + diseaseLigands)
- PharmGKB_get_drug_label_annotations (/data/label FDA PGx biomarker labels)
- PharmGKB_get_pathway (/data/pathway curated PK/PD pathways)
- PharmGKB_get_variant_annotations (/data/variantAnnotation literature-level)
- SIGNOR_connect_proteins (getData.php?type=connect causal sub-network)
- PathwayCommons_paths_between (graph?kind=PATHSBETWEEN multi-gene paths)
All reuse existing tool classes (no registration change). 49 unit tests,
all live-verified. Tools-only, no bump.
* ultracode sweep: +31 capability-depth tools across 8 core clusters
Parallel capability-depth audit (8 clusters vs each upstream API's own
endpoints) + build of confirmed keyless gaps. All reuse existing tool
classes (no new registration). 119 unit tests, 27/27 live-smoke verified.
- variant-clinical (3): VariantValidator_format_genomic_to_transcripts,
GeneBe_classify_variants_batch, ClinGenAR_lookup_by_external_id
- expression (6): GTEx single-tissue sQTLs / median-transcript / single-nucleus /
finemapping+independent-eQTL, Harmonizome_get_gene_set_members, SCXA_get_cluster_marker_genes
- protein-structure (4): InterPro_get_residue_annotations, PDBeSIFTS_get_scop_mapping,
ELM_get_interaction_domains, ProteinsPlus_protonate_structure
- literature (5): PubTator3_GetEntityRelations, openalex_search_sources/get_source,
EuropePMC_get_article_datalinks, LitVar_get_variant_details
- sequence-genomics (4): EnsemblPheno_get_by_term, Ensembl_get_transcript_haplotypes,
RNAcentral_get_xrefs_and_pubs, UCSC_list_tracks
- chem-metabolomics (4): KEGG_find_compound, metabolights_get_reference_compound,
LipidMaps_get_compound_by_xref, SwissLipids_get_children
- disease-ontology (2): HPO_get_disease_annotations, MonarchV3_phenotype_profile_compare
- cancer-genomics (3): GDC_get_mutation_frequency_by_project,
cBioPortal_get_copy_number_alterations, Progenetix_get_cnv_frequencies
Deferred: ClinVar_get_submitted_records (needs a new class). Tools-only, no version bump.
* ultracode sweep 2: +33 capability-depth tools across 8 more clusters
Second parallel capability-depth sweep (8 clusters vs each upstream API).
All reuse existing classes (no new registration). 109 unit tests, 23/23
live-smoke verified, code-simplifier clean.
- immunology (4): SAbDab structure summary, TheraSAbDab sequences, IMGT germline
FASTA, IEDB antigen-processing prediction
- microbiology-pathogen (5): Pathoplexus details/FASTA, MGnify samples/downloads,
ENAPortal run FASTQ search
- gene-regulation (4): ReMap peaks-in-region, ChIPAtlas colocalization/target-genes/
experiment-metadata
- cheminformatics-admet (4): PubChem BioAssay concise activity, UniChem connectivity,
PubChem Tox eco/human toxicity
- systems-biology-enzymes (5): ReactomeAnalysis expression/species/found/not-found,
BioModels list-all-model-ids
- structural-biology-deep (5): BMRB sequence/shift search + validation, 3D-Beacons
annotations, PDB-REDO version
- gwas-population (2): PGS Catalog performance metrics, Ensembl LD region
- proteomics (4): PRIDE projects-for-protein, PDC quant matrix, ProteomicsDB peptides,
MassIVE protein identifications
Deferred: VEuPathDB (needs new POST class). Tools-only, no version bump.
* Add 3 new-class capability-depth tools (ClinVar SCV + VEuPathDB POST)
The deferred gaps that required a brand-new @register_tool class (the
reuse-only sweep could not build these):
- ClinVar_get_submitted_records (ClinVarSubmittedRecordsTool): efetch
rettype=vcv -> per-submitter SCV records. BRAF V600E returns 44 submissions
across germline/somatic_clinical_impact/oncogenicity axes (existing ClinVar
tools only return the aggregate classification).
- VEuPathDB_search_genes_by_organism + VEuPathDB_get_gene_record (VEuPathDBTool):
WDK POST API across the EuPathDB family (PlasmoDB/ToxoDB/FungiDB/...); the
shared BaseRESTTool is GET-only. PF3D7_0417200 -> DHFR-TS verified.
Registered both classes in default_config.py + _lazy_registry_static.py.
36 unit tests, all live-verified. Tools-only, no version bump.
* ultracode sweep 3: +48 capability-depth tools across 8 more clusters
Third parallel capability-depth sweep (8 clusters vs each upstream API).
All reuse existing classes (no new registration). 144 unit tests, 26/26
live-smoke verified, code-simplifier clean (usda_plants two-source merge confirmed coherent).
- nutrition-food (4): OpenFoodFacts tag filtering, USDA PLANTS wetland/invasive/wildlife
- environment-toxicology (2): EPA TRI facility chemical releases, AOP-Wiki key event
- neuroscience (6): AllenBrain structure expression, NeuroMorpho literature/persistence,
NeuroVault atlases
- plant-agriculture (3): USDA PLANTS search, Plant Reactome participants/species-tree
- model-organism (15): SGD regulation/sequence/disease, WormBase orthologs/interactions/
disease, RGD QTLs/resolve, Alliance orthologs/interactions/alleles, PomBase orthologs/
interactions/GO, InterMine pathquery
- scientometrics (6): ORCID fundings/peer-reviews, OpenAIRE projects, Crossref members,
ROR affiliation matching
- drug-safety-clinical (6): NCI Thesaurus maps/parents, RxNorm NDC status/properties,
DailyMed SPL media/history
- cell-imaging (6): IDR images/annotations, BioImage Archive files, HuBMAP provenance,
CryoET tiltseries/depositions
Skipped (already-covered or key-gated): USDA search dup, Gramene, NIH RePORTER.
Tools-only, no version bump.
* ultracode sweep 4: +41 capability-depth tools across 8 more clusters
Fourth parallel capability-depth sweep. All reuse existing classes (no new
registration). 131 unit tests + base_rest regression, 21/21 live-smoke,
code-simplifier clean. base_rest_tool.py got a backward-compatible opt-in
fields.parse_csv branch (CSV/TSV downloads -> dict rows); non-parse_csv tools
unchanged (regression-verified, only G2P uses it).
- rna-noncoding (7): ENCORI RBP-targets/ceRNA/RNA-RNA/degradome/RBP-disease/motif-scan,
RNAcentral region ncRNAs
- taxonomy-biodiversity (8): WoRMS classification/vernaculars/distribution/synonyms,
iDigBio summary-facets/media, GBIF name-match/occurrence-stats
- protein-ptm-motifs (2): iPTMnet proteoform PPI, ScanProsite motif->proteins
- variant-prediction-scores (4): MaveDB mapped/clinical/gnomad variants, GenomeNexus dbSNP
- molecular-interactions-deep (1): OmniPath general-protein annotation geneset
- drug-target-deep (12): Pharos target-ligands/ligand-targets/expression, OpenTargets
target expression/pathways/depmap/prioritisation/cancer-hallmarks + variant effect/
consequences/pharmacogenomics + credible-set colocalisation
- metabolic-pathways-deep (5): BiGG download-model, KEGG module/reaction/enzyme,
WikiPathways pathway-metabolites
- clinical-genetics-panels (2): Gene2Phenotype download-panel, HGNC gene-family members
Tools-only, no version bump.
* ultracode sweep 5 (part 1): +20 capability-depth tools across 6 clusters
Fifth sweep — 6 of 8 clusters built (2 hit transient Anthropic API rate-limit,
re-running separately). All reuse existing classes (no registration). 77 unit
tests, live-smoke verified, code-simplifier clean, no shared-file edits.
- clinical-trials-registries (3): CTIS filtered search, ISRCTN fielded search +
enriched get_trial (27 fields vs ~10)
- ontology-mapping-services (2): OLS term xrefs, Bioregistry prefix mappings
- proteins-api-features (5): EBI Proteins RNA-editing, variation by dbSNP/HGVS,
proteins by genomic location, HPP peptides
- chemical-safety-regulatory (3): FDA GSRS substance relationships, RxClass
class-hierarchy + disease-relations
- pharmacovigilance-faers (3): FAERS count indications/drug-characterization/
reporter-qualification (bare-list, matches existing FDADrugAdverseEventTool convention)
- sequence-archives (4): GEO supplementary files, SRA run-file locator,
BioSamples relationships + facets
Tools-only, no version bump.
* ultracode sweep 5 (part 2): +8 tools — functional-enrichment + metabolomics-spectra
Re-run of the 2 clusters that hit a transient Anthropic API rate-limit in part 1.
All reuse existing classes (no registration). 23 unit tests, 7/7 live-smoke,
code-simplifier clean, no shared-file edits.
- functional-enrichment (1): Enrichr gene->genesets reverse lookup (cluster near-saturated)
- metabolomics-spectra (7): MassBank spectral-similarity/get-record/advanced-search,
MetabolomicsWorkbench studies-by-phenotype + gene-protein, GNPS library-record +
NPClassifier-from-SMILES
Tools-only, no version bump.
* ultracode sweep 6 (part 1): +2 epidemiology tools (WHO GHO + CDC aggregate)
The only cluster of sweep 6 that completed before transient Anthropic API
rate-limiting cut off the other 7 (re-running separately):
- WHOGHO_list_dimension_values: enumerate dimension values (countries/regions/
sex/age) with code->title + country->region rollup (BaseRESTTool config-only)
- cdc_data_aggregate: server-side SoQL aggregation ($select/$group/$having/
$query) on Socrata /resource/{id}.json; CDCRESTTool error envelope hardened
(top-level error key). cdc_data_get_dataset unchanged (regression-verified).
Tools-only, no version bump.
* ultracode sweep 6 (part 2): +28 tools across 7 clusters
Re-run of the 7 clusters that hit transient Anthropic API rate-limiting in
part 1 (epidemiology already shipped in
|
||
|
|
e9bd73205c |
Add Codex plugin packaging for ToolUniverse (#249)
* Add Codex plugin packaging * Wire Codex plugin into skill-sync + release pipeline; bring to parity The Codex plugin packaging lived outside the Claude plugin's release flow, so its bundled skills and version drifted (134 skills @ 1.2.4 vs Claude's 136 @ 1.2.18). This puts both packagings on one pipeline: - plugin/sync-skills.sh now rebuilds BOTH plugin/skills/ (Claude) and plugins/tooluniverse/skills/ (Codex) from the canonical skills/ tree; Codex drops Claude-Code-specific skills (tooluniverse-claude-code-plugin). - scripts/release-plugin.sh now also bumps plugins/tooluniverse/.codex-plugin/ plugin.json on release, so Codex's version tracks Claude's. One-time parity: - Codex skills resynced (135 = 136 user-facing minus the Claude-only skill); adds tooluniverse-peptide-target-deorphanization, drops stale entries. - Codex plugin.json 1.2.4 -> 1.2.18 to match the current release. - Restored skills/tooluniverse-custom-tool/references/json-tool.md: PR #194 edited only the Claude plugin copy, leaving the source missing the API-key declaration docs, which any resync would otherwise drop. Codex auto-upgrades owner/repo Git marketplaces on startup, so this reaches Codex users on next launch; Claude users get it via the version bump. * Fix Codex skill sync: delegate to the Codex-specific normalizer The prior pipeline commit rebuilt plugins/tooluniverse/skills/ with the same filter as the Claude copy, which left in the disable-model-invocation marker (rejected by Codex validation; 133 skills) and descriptions over Codex's 1024-char limit (4 skills). Have plugin/sync-skills.sh delegate the Codex copy to scripts/sync-codex-plugin-skills.sh — which strips the marker and compacts long descriptions — instead of duplicating and diverging from that logic. Also add an evals/ exclude to the Codex sync so dev eval scaffolding does not ship, matching the Claude filter. * Run the Codex plugin packaging test in CI tests/test_codex_plugin.py lived at the tests/ top level, outside pytest.ini's testpaths (tests/unit, tests/integration, tests/tools), so CI never collected it — its per-skill assertions (description <= 1024 chars, disable-model-invocation not set) silently never ran, which is why the earlier broken Codex skill regen passed CI. Move it under tests/unit/, fix the repo-root parents[] depth, and mark it unit so the Codex skill set is validated on every run. * Rename Codex marketplace from tooluniverse-local to tooluniverse The catalog name/displayName carried a 'local' suffix left over from local-dir testing, but this is the published GitHub marketplace users add via `codex plugin marketplace add mims-harvard/ToolUniverse`. Drop the misleading 'local' so it lists as 'tooluniverse' / 'ToolUniverse'. * Update Codex plugin README: add install steps, point sync at the shared script - Add an Install section (codex plugin marketplace add mims-harvard/ToolUniverse + codex plugin add tooluniverse) — the README had no end-user install steps. - Point the Development sync at plugin/sync-skills.sh, the unified entry point that rebuilds BOTH the Claude and Codex skill copies; the old instruction ran only the Codex-specific sync, so following it left the Claude copy stale. * Version plugins as <package MAJOR.MINOR>.<plugin revision> Tie both plugin versions to the PyPI package's MAJOR.MINOR while letting them carry an independent PATCH (the plugin revision). release-plugin.sh now derives the next version from pyproject.toml's MAJOR.MINOR and bumps the plugin PATCH (resetting to .0 when the package's MAJOR.MINOR advances), instead of bumping the plugin's own semver freely. This keeps Claude + Codex plugin versions coherent with the package they serve while a skill-only change ships as a PATCH bump — without forcing an empty PyPI package release. * Update RELEASING.md for the package-tracked plugin version scheme Document the <package MAJOR.MINOR>.<plugin revision> scheme, the 4 bumped plugin manifests (Codex included), and that both Claude (version-driven) and Codex (git-driven) plugins auto-update — replacing the stale level-based / 3-manifest / Claude-only instructions. * Add tooluniverse-codex-plugin install skill (Codex-only) A one-step install + troubleshooting guide for the ToolUniverse plugin on OpenAI Codex, mirroring tooluniverse-claude-code-plugin but adapted to Codex's commands (codex plugin marketplace add / plugin add -m), startup auto-upgrade behavior, and MCP-via-uvx model — and noting Codex ships MCP tools + skills only (no slash commands). Symmetric to the Claude install skill: plugin/sync-skills.sh now excludes it from the Claude packaging, while the Codex sync includes it (and drops the Claude-only tooluniverse-claude-code-plugin). Description tuned via the skill-creator trigger optimizer (perfect precision; install-skill recall is inherently low and largely platform-bound). |
||
|
|
1303e5857c |
Add peptide target deorphanization skill + ProteomicsDB meltome & ClusPro tools (#254)
Add a general 'peptide -> any protein target' deorphanization capability: given a peptide sequence (and optionally a phenotype and a hypothesized target it does NOT actually bind), find its likely real target(s). Skill (tooluniverse-peptide-target-deorphanization, both skill trees + router): - Keyless multi-route pipeline: characterization/motif (PROSITE/ELM), BLAST homology, target-class router (GPCR / ion channel / protease / cytokine receptor / integrin / ...), target-family enumeration (HGNC + InterPro general, GPCRdb for GPCRs), OpenTargets phenotype anchor, EnsemblCompara/Alliance cross-species reconciliation, ranked shortlist with evidence tiers. - Two runnable scripts: deorphanize_peptide.py (keyless Phases 1-4, seeded/seedless/ batch, cross-species interface alignment, ClusPro-ready PDB) and cofold_screen.py (optional NVIDIA-NIM co-folding). references/phases.md manual + evals/evals.json. Tools: - ProteomicsDB_get_protein_meltome (keyless TPP/meltome melting curves for CETSA/TPP target-engagement; soluble proteome). - ClusPro_submit_peptide_docking (academic-free peptide-protein docking; key-gated). Live-validated on the exendin-4 -> GLP1R control and on non-GPCR targets (CACNA1B ion channel, EGFR RTK, MMP9/CTSK proteases, IL6R cytokine receptor). 34 unit tests. Note: optional cross-checks PepCalc/AMPSphere are referenced by the skill docs and degrade gracefully (ProtParam covers properties; AMPSphere is a doc-only screen). |
||
|
|
833f065225 | Discovery batch: PheWAS + MSA/phylogeny + clinical calculators + 10 keyless resource tools + product-safety skill (#248) | ||
|
|
ba98160c88 |
Fix Rounds 010-018: tool/skill robustness, GraphQL drift, ProtVar/Dfam APIs, +3 new tools (#245)
* Fix Round 010: CADD tool used broken bihealth mirror -> silent 'No score'
Found via CLI role-play harness. CADD_get_variant_score (and the position/range
ops) queried CADD_BASE_URL = cadd.bihealth.org, which returns an empty list
(HTTP 200) for GRCh38-v1.7 — so scored variants silently reported 'No CADD
score found'. e.g. BRAF V600E (7:140753336 A>T) -> data:null, while the
canonical cadd.gs.washington.edu returns PHRED 29.8.
The tool's own docstring already cites cadd.gs.washington.edu as the API host;
only the BASE_URL constant pointed at the dead mirror. Switched it. Now BRAF
V600E -> phred_score 29.8 'deleterious (top 1%)'. cadd sweep 5/5; +2 tests.
* Fix Round 011: MGnify search returned empty on biome/size params
MGnify_search_studies and MGnify_list_analyses targeted the api/latest
base, which 301-redirects to api/v2 and drops the query string, and they
sent biome=/size= where the API expects lineage=/page_size=. The net
effect was a silent empty result (data: []) even for well-populated
biomes such as the human gut -- the tool's own documented example
returned nothing.
- Point both tools at the stable api/v1 endpoint
- Map biome -> lineage and size -> page_size (accept lineage alias)
- Add mocked regression tests asserting the parameter translation
* Fix Round 012: OMA_get_hog retired-ID guidance + refresh stale HOG examples
OMA reassigns Hierarchical Orthologous Group IDs between releases. The
tool's test_examples and docs used the retired 'HOG:E0739094' (p53) and
'HOG:E0817124' (insulin) IDs, which now return HTTP 410 -- surfaced as a
bare 'OMA API HTTP error: 410'.
- Refresh examples/docs to the current IDs (HOG:F0782425 p53, HOG:F0798498 insulin)
- Catch 410 in _get_hog and return an actionable message pointing the
caller at OMA_get_protein's oma_hog_id field
- Add mocked regression tests for both the 410 path and a valid HOG
* Fix Round 012: FourDN actionable message on 4DN expired-cert outage
The 4DN Data Portal (data.4dnucleome.org) has been serving an expired TLS
certificate (server-side lapse). All four FourDN operations leaked the raw
HTTPSConnectionPool/SSLCertVerificationError traceback as their error
string, which reads like a client problem.
- Add _format_request_error() classifying expired/invalid-cert SSL failures
and returning an honest 'transient server-side 4DN issue' message
- Route all five except blocks through it
- Certificate verification is never disabled
- Mocked tests for the SSL path and the non-SSL passthrough
* Fix Round 013: Alliance_search_genes empty on descriptive name + misleading docs
The Alliance autocomplete matches gene symbols/synonyms, not descriptive
names: 'insulin' returns GO terms and diseases (no gene), while the symbol
'INS' returns the genes. The tool returned an indistinguishable empty list,
and its description/examples falsely claimed 'insulin' returns INS.
- Add a metadata 'note' when no gene matched but other entity types did,
surfacing the matched categories and pointing at the gene symbol
- Correct the description and param doc to say symbol/synonym, not name
- Refresh test_examples to real symbols ('INS', 'shh')
- Add mocked tests for the note path and a symbol query
* Fix Round 013: RGD/Xenbase empty search, ProtVar null fields, handle_error guard
Three issues surfaced by the registry-wide test_examples sweep:
RGD_search_genes / Xenbase_search_genes returned empty for valid symbols
(e.g. 'Tp53') -- they queried Alliance with the stale 'category=gene' param
(no longer honoured -> 0 results) and read the gene id from 'primaryKey'
(now 'curie'). Fetch unfiltered, keep gene_search_result hits, read curie.
ProtVar_get_function crashed with TypeError on a UniProt payload carrying an
explicit null list (e.g. comment 'text': null) -> 'for t in None'. Treat null
lists as empty and guard a non-dict result.
_classify_exception called tool_instance.handle_error() unconditionally, but
plain (non-BaseTool) tools lack it, so any error from such a tool became a
cryptic 'has no attribute handle_error' that masked the real exception. Guard
with getattr and fall back to a generic ToolError. General fix for all
non-BaseTool tools.
Mocked regression tests for all three.
* Fix Round 013: refresh three stale/over-constrained test_examples
The registry-wide example sweep flagged three tools whose own test_example
returned empty -- the tools work, the examples were wrong:
- DailyMed_search_spls combined four mutually-inconsistent filters
(drug_name + ndc + rxcui + setid), which the API ANDs -> no match. Keep
just drug_name.
- MGnify_list_analyses used MGYS00000001, which has zero analyses. Use
MGYS00002012 (verified to have analyses).
- HPA_generic_search searched 'machine learning optimization' (nonsense for
a protein atlas). Use the gene 'EGFR'.
All three now return data.
* Fix Round 013: actionable error for ProtVar's removed /mappings endpoint
ProtVar restructured its API and removed the batch /mappings endpoint that
ProtVar_map_variant POSTs to, so every call returned a bare 'HTTP Error 404'.
The /function and /population endpoints still work. On a 404, return an
actionable message pointing the caller at ProtVar_get_function /
ProtVar_get_population instead. Mocked test for the 404 path.
* Fix Round 014: OpenTargets drug query used removed schema fields (HTTP 400)
OpenTargets_get_drug_description_by_chemblId requested 'yearOfFirstApproval'
(removed from the Drug type) and 'maximumClinicalTrialPhase' (renamed to
'maximumClinicalStage'), so every call returned HTTP 400 'Cannot query
field ...'. Update the GraphQL query and return_schema to current fields and
add a real test_example (CHEMBL25). Now returns drug data for aspirin.
* Fix Round 014: four more OpenTargets tools used removed GraphQL fields
A schema-drift sweep (dry-running every OpenTargets query against the live
GraphQL schema) found four more tools returning HTTP 400 on removed/renamed
fields:
- get_drug_blackbox_status_by_chembl_ID: blackBoxWarning -> drugWarnings{warningType,...}
- get_approved_indications_by_drug_chemblId: approvedIndications -> indications{count,rows{...}}
- get_gene_ontology_terms_by_goID: GeneOntologyTerm.name -> label
- get_target_gene_ontology_by_ensemblID: term.name -> label
Update each query + add real test_examples (had none). Parametrized
regression test asserts no removed field remains.
* Fix Round 015: skills referenced ClinVar tools with wrong case (clinvar_* -> ClinVar_*)
18 skills told agents to call 'clinvar_search_variants',
'clinvar_get_variant_details', and 'clinvar_get_clinical_significance', but
SDK tool lookup is case-sensitive and the registered tools are 'ClinVar_*'.
An agent following these skills got 'Tool not found'. Correct the case for
the three tool names only (50 refs across 18 source skills, synced to
plugin/skills); data-field names like 'clinvar_classification' are left
untouched.
* Fix Round 015: correct 20 more case-mismatched tool refs across skills
Skills referenced real tools with the wrong case, which case-sensitive SDK
lookup rejects as 'Tool not found'. Fixed (token -> real tool), e.g.:
- 8x OpenTargets_..._by_..._ensemblId -> ...ensemblID
- gnomAD_* -> gnomad_*, KEGG_get_pathway_genes -> KEGG (and kegg_find_genes),
reactome_get_pathway -> Reactome_get_pathway, go_search_terms -> GO_search_terms,
GEO_search_datasets -> geo_search_datasets, ENA_get_entry -> ena_get_entry,
PDBe_get_entry_summary -> pdbe_get_entry_summary, enrichr_enrich -> Enrichr_enrich,
monarch_search -> Monarch_search
Only exact multi-segment tool-name tokens changed (param names like the
'ensemblId' argument and field names are untouched); synced to plugin/skills.
* docs: skill audit of tool references not found in the registry (Round 015)
112 backtick-wrapped tool-shaped names appear in skills but match no
registered tool (any case) and aren't in any data config. Documented for
per-skill human review rather than mass-edited, since the list mixes
plausible renames, genuinely-absent tools, and illustrative pseudo-names.
* Fix Round 016: correct 23 renamed/format-variant tool refs in skills
Verified subset of the skill-reference audit: tool names that are unambiguous
format variants or clear renames of a real tool (target existence + semantics
confirmed). 80 refs across 28 source skills, synced to plugin/skills. Examples:
- ChEMBL_search_compounds -> ChEMBL_search_molecules
- DailyMed_get_spl_by_set_id / _sections_by_setid -> DailyMed_get_spl_by_setid
- ClinicalTrials_search -> ClinicalTrials_search_studies
- SemanticScholar_search -> SemanticScholar_search_papers
- UniProt_get_protein_by_accession -> UniProt_get_entry_by_accession
- OpenTargets_get_disease_associated_targets -> _get_associated_targets_by_disease_efoId
- DGIdb_get_interactions -> DGIdb_get_drug_gene_interactions, Orphanet doubled-prefix, etc.
Ambiguous/illustrative/genuinely-absent names left in the audit doc.
* docs: refresh skill tool-reference audit after Round 016 fixes
* Fix Round 016: restore ProtVar_map_variant on the restructured ProtVar API
ProtVar 2.x removed the batch POST /mappings endpoint (404). Reverse-engineered
the current contract from the ProtVar web app: GET /mapping?q=<input>&assembly=
<GRCh38|GRCh37>, returning content.inputs[].derivedGenomicVariants[].genes[].
isoforms[]. Rewrote the tool to call it and parse the new shape (AlphaMissense
amScore, merged popEveScore eve/esm1v, gene-level caddScore), added an 'assembly'
param and a test_example, dropped the now-unused _post_json helper.
Verified live for protein ('P04637 R175H' -> chr17:7675088, TP53 missense, AM
0.9857 PATHOGENIC), dbSNP ('rs1799966' -> BRCA1), and genomic VCF inputs.
Mocked tests updated for the new GET contract.
* Fix Round 016: actionable error for Dfam's broken genome-annotation endpoint
Dfam's /annotations endpoint returns HTTP 405 'Invalid Input - 101 - undefined'
for every well-formed region query (param validation passes; chrom/assembly/
start/end/family/nrph are all accepted), while Dfam's family endpoints still
work. This is a server-side Dfam issue. Detect the 405/'Invalid Input' response
and return an actionable message pointing at Dfam_search_families /
Dfam_get_family and the UCSC RepeatMasker track (UCSC_get_track) for genome TE
annotations, instead of a bare 'HTTP 405'. Mocked tests for the 405 path and
the missing-region validation guard.
* Fix Round 017: resolve 50 more renamed/moved tool refs in skills
Worked through the skill-reference audit instead of leaving it as a TODO.
Verified each target exists and matches semantics (checked tool descriptions),
then fixed 50 references across the skills (synced to plugin/skills), e.g.:
- clinical_trials_search -> ClinicalTrials_search_studies
- Reactome_search_pathway -> ReactomeContent_search
- gnomAD_get_variant_* -> gnomad_get_variant; gnomAD_search_gene_variants -> gnomad_search_variants
- NCBI_Taxonomy_search/get -> NCBIDatasets_suggest_taxonomy/get_taxonomy
- UniProt_get_protein_sequence -> UniProt_get_sequence_by_accession
- DrugBank_get_drug/targets -> drugbank_get_*_by_drug_name_or_id
- OpenFDA_get_drug_recalls/enforcement -> OpenFDA_search_drug_enforcement
- OpenTargets_get_associated_diseases_by_target -> _get_diseases_phenotypes_by_target_ensembl
- ExAC_get_constraint_metrics -> gnomad_get_gene_constraints (ExAC retired)
- emdb_search/get_entry -> EMDB_search_structures/get_structure, +~35 more
Only exact tool-name tokens changed (params/field names untouched).
* docs: recategorize skill-reference audit (94 fixed; remaining are non-tool/absent)
* Add HPO phenotype->genes and phenotype->diseases tools (fills audit gap)
Skills needed to go from an observed HPO phenotype to candidate genes and a
disease differential, but TU only exposed term lookup + hierarchy (the audit
flagged HPO_get_term_genes / HPO_get_term_diseases as absent capabilities).
Add two tools on the JAX network-annotation endpoint
(ontology.jax.org/api/network/annotation/{HP-id}):
- HPO_get_genes_by_phenotype: HP term -> NCBI genes (e.g. HP:0001250 -> RELN, SCN1A)
- HPO_get_diseases_by_phenotype: HP term -> diseases (ORPHA/OMIM) + MONDO id
Both with limit param, oneOf return_schema, real test_examples, never-raise.
Point the previously-broken skill references at the new tools. Mocked tests.
* Add GtoPdb_search_diseases tool (fills audit gap)
The audit flagged GtoPdb_list_diseases / GtoPdb_get_disease as absent. GtoPdb's
/services/diseases endpoint is public and config-driven by the existing
GtoPdbRESTTool, so add a search_diseases tool config (name/query -> diseaseId,
name, synonyms, external DB links) and point the skill references at it.
Verified live: 'epilepsy' returns GtoPdb disease entries.
* docs: refresh skill audit after building HPO/GtoPdb gap-filling tools
* Round 019: add OpenTargets target-info tool, fix Ensembl stable-ID lookup, +renames
- Build OpenTargets_get_target_info_by_ensemblID (GraphQL core target info);
verified live for ENSG00000141510 (TP53).
- Fix ensembl_lookup_gene: bare stable ID (no species) failed with 'Missing path
parameter species' (build_url ran before stable-ID routing). Add species default.
- Resolve more audit refs by rename to existing equivalents (Ensembl_get_gene_info,
FDA_drug_search, FAERS_search_by_drug, OpenTargets_get_target/_associated_targets).
Mocked test for the new tool; skill refs updated and synced.
* docs: refresh skill audit after Round 019 (OpenTargets target-info + renames)
* Round 020: build UniProt features tool + resolve remaining addressable audit refs
- Build UniProt_get_features_by_accession (config-only: extract_path 'features'
on the UniProtKB entry) -- returns all sequence features (domains, sites, PTMs,
etc.). Verified live for P04637 (TP53).
- Renames to existing equivalents: ChEMBL_get_bioactivity_by_chemblid ->
ChEMBL_search_activities (filters by molecule_chembl_id), ChEMBL_get_assays ->
ChEMBL_search_assays, OpenTargets_diseases ->
OpenTargets_get_diseases_phenotypes_by_target_ensembl, UniProt_get_protein_features
-> UniProt_get_features_by_accession.
- Fix PubChem_get_drug_label_info_by_CID skill-logic error (PubChem has no FDA
labels): rewrite example snippets to the correct CID -> compound synonyms ->
FDA_get_drug_label(drug_name) chain; point tool-option tables at FDA_get_drug_label.
* Round 020: OpenTargets_pathways -> KEGG_get_gene_pathways (last clean audit rename)
The rare-disease TOOLS_REFERENCE listed OpenTargets_pathways as a gene->pathways
fallback, but no such tool exists; KEGG_get_gene_pathways is the right gene->pathway
lookup. DepMap_get_drug_response stays unresolved: GDSC/DepMap drug-sensitivity is
bulk-download data, not a queryable REST endpoint (all Sanger API sensitivity paths 404).
* docs+test: refresh audit (24 left, all non-actionable) + UniProt features tool test
* docs: remove skill-reference audit (all actionable items resolved)
Every fixable reference was resolved over Rounds 015-020 (105 renames/fixes + 5
new gap-filling tools). The only remaining entries were non-tool tokens (pipeline
function names, Enrichr gene-set library values, dev-SDK references, tutorial
illustratives) and DepMap drug sensitivity (bulk-download data, no REST API) --
none are actionable TODOs, so the tracking doc is no longer needed.
* Round 021: package a GDSC drug-sensitivity script for the no-API DepMap gap
DepMap/GDSC drug response is bulk-download data, not a REST API, so it can't be a
TU tool. Instead of leaving the gap, give the skill a real implementation:
- Add scripts/gdsc_drug_response.py to the precision-oncology skill: downloads the
public GDSC2 fitted dose-response table once (cached), and queries drug
sensitivity by drug / cell-line / target gene (LN_IC50, AUC, Z_SCORE, TCGA type).
Verified: Trametinib in SKCM -> sensitive melanoma lines; target BRAF -> Dabrafenib.
- Replace the non-existent DepMap_get_drug_response references in 3 skills with a
subprocess wrapper + tables pointing at the script, and document why (no API).
This is the 'skill provides a computational procedure when no tool can' pattern.
* Round 022: add IEDB_predict_bcell_epitopes (B-cell epitope prediction)
Skill audit (vaccine-design): TU predicted MHC-I/II epitopes but had no B-cell
(antibody) epitope prediction -- only search of known epitopes. The IEDB B-cell
API (BepiPred/Emini/etc.) is public and keyless. Add a predict_bcell endpoint to
IEDBPredictionTool + IEDB_predict_bcell_epitopes tool (collapses per-residue
assignment into contiguous epitope regions). Verified live; mocked test; wired
into the vaccine-design skill. (Audit's MHC-I/II 'gaps' were false positives.)
* Round 022: fix metabolomics-analysis code examples calling nonexistent tools
code_examples.md called hmdb_search_by_mass, kegg_find_compound, and
kegg_enrich_pathway -- none exist. Replace with the real tools:
- mass annotation -> MetabolomicsWorkbench_search_by_mz (mz_value/adduct/tolerance)
- metabolite-set pathway enrichment -> MetaboAnalyst_pathway_enrichment (takes
metabolite names directly; drops the broken 2-step KEGG-ID lookup).
* Round 023: DepMap Chronos gene-dependency script (no-API bulk-data gap)
cell-line-profiling and functional-genomics-screens both need per-cell-line
CRISPR (Chronos) dependency scores, which DepMap_get_gene_dependencies can't
return (metadata only) -- the data is a bulk CSV, not a REST API.
Add scripts/depmap_gene_dependency.py: resolves the freshest DepMap Public
download URLs via the public index (signed URLs expire), caches
CRISPRGeneEffect.csv + Model.csv, and queries dependency by gene (most-dependent
cell lines, optional lineage filter) or by cell-line (most-essential genes).
Verified: KRAS -> pancreatic lines (ASPC1 -4.46); A375 -> ribosomal essentials.
Wired into both skills (functional-genomics points at the cell-line-profiling script).
* Round 023: gwas-study-explorer meta-analysis -- real pooling, no more fabricated I2
The skill advertised inverse-variance meta-analysis but the code FABRICATED I2
(variance of -log10(p) * 10, capped at 100), set combined_beta/se=None always,
and used min(p) as the 'combined p' -- presenting a non-statistical guess as a
real heterogeneity statistic.
Fix: parse per-study effect sizes (beta, or log(OR), + SE from the 95% CI 'range')
from GWAS Catalog associations. When >=2 studies have usable effect sizes, do a
REAL inverse-variance fixed-effect pooling + Cochran's-Q I2 (+ heterogeneity p via
scipy when available). When they don't (common), return method='descriptive',
i2=None, combined_beta=None, and combined_p = smallest reported p clearly labeled
as NOT a pooled p. Dataclass + interpretation updated to never invent an I2.
Verified effect-size parsing (beta/OR/CI incl. negative ranges) and both paths.
* Round 023: bundle full Hart CEGv2/NEGv1 reference gene sets for CRISPR screens
crispr-screen-analysis used a 5-gene/3-gene placeholder stub for BAGEL Bayes-Factor
scoring and for the screen-QC check ('are known essential genes recovered?') -- too
small for either. Bundle the published reference sets (CEGv2 core-essential ~684,
NEGv1 non-essential ~928) from BAGEL + a loader (reference_gene_sets.py with
core_essential()/nonessential()/recovery_rate()). Wire into the BAGEL function and
the QC step.
* Round 024: dN/dS (Ka/Ks) script for comparative-genomics selection analysis
The skill makes dN/dS its central tool to distinguish positive (>1) vs purifying
(<<1) vs relaxed (~1) selection, but TU has no tool that computes it. Add
scripts/dnds.py: a dependency-free Nei-Gojobori (1986) estimator with Jukes-Cantor
correction (counts syn/non-syn sites, averages over mutational pathways for
multi-diff codons). dN validated to match Biopython's NG86 exactly (0.0698);
returns null dN/dS honestly when dS is 0/uncorrectable. Documented the
homology -> CDS -> align -> dnds.py workflow in SKILL.md.
* Round 024: network proximity Z-score script (Guney/Barabasi) for network-pharmacology
The Network Pharmacology Score's largest component (35 pts) is the target-disease
network proximity Z-score, which the skill could only describe as prose pseudocode
(no tool: it needs the full interactome + a degree-matched random null). Add
scripts/network_proximity.py: downloads STRING v12 high-confidence human PPI once
(cached), builds a networkx graph, computes closest-distance d_c and the
degree-matched-null Z-score + empirical p. Verified: EGFR/ERBB2/MET vs a
lung-cancer module -> Z=-2.07 (proximal). Replaced the pseudocode with the script;
kept the count-based proxy clearly labeled as NOT the Z-score.
* Round 024: antibody developability script (AGGRESCAN/pI) + honest non-computable notes
The antibody developability phase called predict_tango_score / predict_aggrescan /
predict_binding_energy_change / predict_thermal_stability / predict_expression --
all undefined. Replace with what's legitimately sequence-computable, plus honest
notes for what isn't:
- scripts/developability.py: AGGRESCAN aggregation-prone regions (real Conchillo-Sole
a3v propensity scale + windowing/hot-spots), isoelectric point (validated: polyK
11.75, polyE 3.03), Kyte-Doolittle hydrophobic patches.
- Binding ddG -> FoldX/Rosetta on a modeled complex; Tm/titer -> ML predictors:
documented as external, NOT fabricated.
* Round 025: add ENCORI_get_miRNA_targets tool (miRNA-target lookup gap)
noncoding-rna had no miRNA target-lookup tool -- skills fell back to bulk
TargetScan/miRTarBase downloads (miRTarBase Cloudflare-blocked). ENCORI (starBase)
exposes CLIP-supported + predicted miRNA-target interactions via a public REST API.
Add ENCORITool + ENCORI_get_miRNA_targets (registered in default_config):
mirna->targets or gene->miRNAs, ranked by CLIP-experiment support, predicting
programs listed. Verified miR-21-5p->CBX4 (103 CLIP); TP53->188 miRNAs. Mocked test.
* Round 025: add LDlink (LD proxies) and IUCN (conservation status) tools
Two key-gated gaps from the skill audit:
- LDlink_get_proxies (gwas-snp-interpretation): LD proxy variants for a SNP via
NIH LDlink LDproxy, population-specific, R2-filtered (free LDLINK_TOKEN).
- IUCN_get_conservation_status (ecology-biodiversity): Red List category by
scientific name (free IUCN_API_KEY, v4 API).
Both declare optional_api_keys/api_key_info, return a clear register-here error
without a token, parse the verified API response formats. Mocked tests. Wired
into both skills.
* Round 025: population-coverage script for vaccine HLA coverage (last queued gap)
Vaccine design needs HLA population-coverage, but TU has no HLA-frequency tool and
the skill just pointed at the external IEDB web tool. Package the coverage MATH
(scripts/population_coverage.py): per-locus Hardy-Weinberg coverage
1-(1-p)^2 combined across loci, with a small bundled common-HLA-A/B average
frequency table for a first-pass estimate and --freq-file to supply real
AFND/population-specific frequencies. Verified A*02:01 -> 27.1% (matches cited),
broad set -> 73.6%. Wired into Phase 3, with a clear caveat not to use the
average default for a specific ethnicity.
* Round 026: standard envelope for FDA label tools + RCSB entry_id alias
Feature-026C-1: FDALabelTool (FDA_search_drug_labels / FDA_get_drug_label /
FDA_list_drug_classes) returned bare lists/dicts on success (framework-wrapped
to {"result": [...]}), inconsistent with the project-wide
{status, data, metadata} success contract that the error paths already follow.
Added an _ok() helper; all three query types now return the standard envelope
and their return_schema oneOf success branch is updated to match.
Feature-026B-002: RCSBData_get_entry only accepted pdb_id; the RCSB-native term
is entry_id (endpoint is /core/entry/{id}), so callers reaching for it hit a
hard schema-validation failure. _query() now aliases entry_id/id -> pdb_id and
the schema accepts either (anyOf) while an empty call still returns a clear
error.
Tests: tests/unit/test_fda_label_envelope.py, test_rcsb_entry_id_alias.py (9).
* rnaseq-deseq2 skill: scan for precomputed DESeq results embedded in the data file
Supplementary RNA-seq spreadsheets frequently ship the authors' own DESeq
output (per-comparison Up/Down flags, log2FC/padj column blocks, extra sheets)
alongside the counts. Re-running DESeq2 — especially on the normalized counts
these files contain (DESeq2 needs raw integer counts) — gives a materially
different, wrong number. Added guidance to open every sheet, inspect all
columns, and count DE genes directly from the embedded flags when present;
defines DE across-all-comparisons (union) vs jointly/also-DE (intersection).
* Fix stale tool names in skill references (PR #245 review)
Review of PR #245 surfaced wrong/nonexistent tool names in skill docs:
- GtoPdb_get_target_interactions -> GtoPdb_get_interactions (does not exist;
branch had renamed the sibling on the same line but missed this one)
- PDB_search -> PDB_search_similar_structures (fallback-cell hint)
- UniProt_features -> UniProt_get_features_by_accession
- UniProt_taxonomy -> UniProtTaxonomy_search
All four targets verified present in the registry; fixed across skills/ and the
plugin/skills/ mirror (kept byte-identical).
* Round 026 review: GtoPdb diseases schema shape + FDALabel envelope test update
Third review pass of PR #245 surfaced two genuine issues:
- GtoPdb_search_diseases return_schema declared synonyms as an array of strings,
but the GtoPdb API (passthrough) returns synonyms as objects {"name": ...};
live output failed schema validation. Widened items to object-or-string so the
declared contract matches reality across epilepsy/asthma/diabetes queries.
- The standard-envelope change to FDALabelTool (commit
|
||
|
|
41b0f04c36 |
Discovery batch: 5 resource tools + 8 analysis skills (#242)
* Add BGPT structured scientific-evidence tool (issue #204) BGPT searches papers and returns full-text-derived evidence fields — methods, sample size/population, results, limitations and biases, conflicts of interest, data/code availability, quality scores, study blind spots, and a how_to_falsify statement — rather than only titles/abstracts. This is a genuinely different shape from the existing PubMed/EuropePMC/OpenAlex tools (confirmed via find_tools: no existing tool does evidence-quality appraisal) and fits claim-interrogation / hypothesis-critique / evidence-dashboard workflows. - BGPT_search_paper_evidence (BGPTPaperEvidenceTool): POST bgpt.pro/api/mcp-search. - Optional BGPT_API_KEY (free for the first 50 results, then keyed) — declared as optional_api_keys so the tool stays usable in the free tier and in CI. - return_schema has oneOf (success + error); HTTP 402 maps to an actionable 'set BGPT_API_KEY' message; timeout/connection/non-JSON handled; never raises. - Registered in default_config + lazy registry; regen .env.template + catalog. - mocked unit tests (no live calls); live-validated: 'GLP-1 alcohol craving' returns DOIs with limitations/results/how_to_falsify fields populated. License MIT, ToS permits redistribution (confirmed by the author in #204). * Add Antibody Registry tools (RRID search + resolve) Discovery-harness sweep found the Antibody Registry (antibodyregistry.org, a SciCrunch/RRID resource of 3M+ research antibodies) absent despite the existing antibody-engineering / immunology skills. Journals increasingly require antibody RRIDs for reproducibility, and no existing TU tool resolves or searches them (find_tools confirmed no overlap). - AntibodyRegistry_search: full-text search by target/name/vendor via GET /api/fts-antibodies?q=... (the bare /api/antibodies endpoint lists the whole DB unfiltered — confirmed against the published OpenAPI spec). - AntibodyRegistry_get_by_rrid: resolve 'AB_2298772' / 'RRID:AB_2298772' / bare numeric id via GET /api/antibodies/{id}. - Both return the derived RRID plus target (UniProt/Entrez ids), clonality, clone id, host/source organism, conjugate, applications, vendor, catalog number, and defining citation; ~38 raw fields trimmed to the useful subset. - Public API, no key. oneOf return_schemas; never raises; timeout/HTTP/non-JSON handled. Registered in default_config + lazy registry. Live-validated (search 'GFAP' -> 2348 hits with RRIDs; resolve AB_10000343 -> Parvalbumin mAb) + 6 mocked unit tests; sweep 2/2 passed. * Add retraction-status check tool (Crossref + Retraction Watch) Discovery-harness sweep: the careful pass found TU's resource coverage is essentially saturated (every probed gap — ACMG/InterVar, lipidomics/LipidMaps, glycomics/GlyGen, networks/OmniPath, ADMET-tox/ADMETAI, scholarly/Unpaywall+ ORCID+ROR — was already covered). The one genuine TASK gap: there is no discoverable way to check whether a paper is retracted (find_tools 'retraction' returned nothing). The raw signal sits in Crossref's 'updated-by' field but is buried in the full work record returned by Crossref_get_work. - Crossref_check_retraction (RetractionCheckTool): GET api.crossref.org/works/{doi}, read 'updated-by' (populated from Retraction Watch, source: retraction-watch), dedupe and classify notices into retraction / expression-of-concern / correction. - Returns a clear is_retracted flag plus structured notices (type, label, the notice's DOI, date) — a research-integrity guardrail for the literature ecosystem (PubMed/OpenAlex/Europe PMC/BGPT return papers without retraction status). Public API, no key; optional CROSSREF_MAILTO for the polite pool. Live-validated (Wakefield 1998 Lancet -> is_retracted=true with correction+ retraction notices; AlphaFold 2021 -> false; unknown DOI -> clean error) + 6 mocked unit tests; sweep 2/2 passed. * Add GeneBe ACMG variant-classification tool Discovery: complementary resource (different organization, not a mirror). GeneBe (genebe.net) is an independent ACMG/AMP auto-classifier alongside the existing InterVar tool — covering the same task from a different group, with different data layered in (AlphaMissense, APOGEE2, gene-specific ACMG, dbSNP, gnomAD allele frequency, ClinVar cross-reference), so the two give independent second opinions on borderline variants. - GeneBe_classify_variant (GeneBeTool): GET api.genebe.net/cloud/api-public/v1/variant with chr/pos/ref/alt + genome (hg38 default / hg19). Returns the ACMG verdict, score, and triggered criteria plus gene/transcript/HGVS, dbSNP, gnomAD AF, ClinVar class, and AlphaMissense score; the ~54-field response is trimmed to the useful subset. - chr/chrom + genome/build aliases; GRCh38->hg38 / GRCh37->hg19 normalization; 'chr' prefix stripped; 429 -> actionable rate-limit message; never raises. Public API, no key. Registered in default_config + lazy registry. Live-validated (BRAF V600E 7-140753336-A-T -> Pathogenic, score 14, criteria PS3,PM1,PM2,PM5,PP2,PP3_Moderate,PP5, AlphaMissense 0.9927; a BRCA1 variant via GRCh38+chr17 -> Likely_benign) + 6 mocked unit tests; sweep 1/1 passed. * Add SwissLipids tools (SIB lipid database) Complementary resource (different organization, not a mirror): SwissLipids (swisslipids.org, SIB Swiss Institute of Bioinformatics) sits alongside the existing LIPID MAPS tools as an independently curated lipid database, with its own class->species->subspecies hierarchy and an adduct m/z table that LIPID MAPS does not provide (useful for matching lipidomics MS peaks). - SwissLipids_search: lipid name / shorthand search via /api/index.php/search. - SwissLipids_get_lipid: full entry via /api/index.php/entity/{SLM_id} — formula, monoisotopic mass, charge, adduct m/z, classification, synonyms, and cross-references (ChEBI, HMDB, LIPID MAPS, Rhea, MetaNetX). - query/term + entity_id/slm_id aliases; auto-adds the 'SLM:' prefix; never raises. Public API, no key. Registered in default_config + lazy registry. Live-validated (search 'PC(16:0/18:1)' -> 5 hits; get SLM:000000510 -> hexadecanoate C16H31O2, mass 255.4167, adduct m/z, xrefs incl. LipidMaps) + 6 mocked unit tests; sweep 2/2 passed. * Add meta-analysis skill (evidence synthesis / effect-size pooling) Discovery task-gap (Sub-loop B): no skill covered statistical meta-analysis. The MetaAnalysis_run tool (fixed/random effects, inverse-variance, DerSimonian- Laird) existed but was only mentioned contextually in other skills; there was no workflow teaching the error-prone parts. (Survival/Cox is deliberately routed to statistical-modeling, so it is not the gap — meta-analysis is.) tooluniverse-meta-analysis encodes: - Effect-size + SE preparation table: OR/RR/HR+CI -> ln + SE, two-group means+SD -> Hedges' g, proportions -> logit, correlations -> Fisher z. - Fixed vs random-effects decision table. - Heterogeneity interpretation (I^2 bands, Q p-value, tau^2) + the borderline- I^2 / few-studies case. - A scale foot-gun warning: MetaAnalysis_run pools whatever scale it is given and returns log-scale output (incl. its interpretation string) — for ratios, pass ln() values and exp() the result back. - scripts/meta_analysis.py: reads a study CSV, auto-detects the input type, converts to (effect, SE), pools (fixed + DL random), back-transforms ratios, and prints a text forest plot. Cross-validated to agree exactly with MetaAnalysis_run (pooled OR 1.5054, I^2=3.6% on a shared example). - Routing rows added to the router (both skills/ and plugin/skills/ mirrors). Validated with an epidemiologist persona (3-study colorectal-cancer OR synthesis): correct pooled OR 1.40 [1.16-1.68], I^2=29.7%; skill scored 8/10. The persona's 3 UX findings (log-scale foot-gun under-warned, borderline-I^2 guidance thin) were folded back into Step 3/Step 4. * Add dose-response curve-fitting skill (IC50/EC50/potency) Discovery task-gap (Sub-loop B): three DoseResponse_* tools exist (fit_curve, calculate_ic50, compare_potency — 4PL Hill model) but no skill taught the workflow. The 'ic50' term appeared only inside domain skills (computational-biophysics, gpcr-structural-pharmacology), and image-analysis covers only image-DERIVED dose-response — assay concentration-response had no home. tooluniverse-dose-response encodes: - Data-prep table (the error-prone part): consistent concentration units, linear (not log) input, dropping zero-concentration rows, direction, raw->% normalization, replicate handling, and the >=4-6-points/spanning- both-plateaus coverage requirement. - Tool orchestration: calculate_ic50 / fit_curve / compare_potency. - Interpretation table for the four 4PL parameters (IC50/EC50, Hill slope, Emax/efficacy + partial agonism, r^2) with sanity checks. - Quality gotchas: incomplete/no-plateau curves, too few transition points, biphasic data, IC50!=Ki (Cheng-Prusoff), units. - scripts/fit_dose_response.py: scipy 4PL fit from a CSV with optional normalization + replicate averaging; reports plateaus by position and flags out-of-range IC50 / low r^2 / implausibly steep slopes. Cross- validated to match DoseResponse_calculate_ic50 exactly (IC50 0.7624, Hill 0.736, r^2 0.9998). - Routing rows added to the router (both skills/ and plugin/skills/ mirrors), scoped to concentration-response/IC50/potency so it doesn't collide with image-analysis's image-derived dose-response. * Add pharmacokinetics (NCA) skill Discovery task-gap (Sub-loop B): a dedicated NCA tool family exists (NCA_compute_parameters, NCA_fit_one_compartment, NCA_calculate_bioavailability) but no skill. PK was only mentioned in domain skills (admet-prediction is structure-based PREDICTION, not measured-data NCA). tooluniverse-pharmacokinetics encodes: - Concentration-time data prep (unit consistency, IV vs oral route -> true vs apparent CL/Vd, BLQ handling: leading->0 / terminal->drop, terminal-phase sampling requirement, single vs steady-state). - NCA_compute_parameters orchestration + bioavailability (NCA_calculate_bioavailability) + one-compartment fitting. - Interpretation table (Cmax/Tmax, AUC0-t/0-inf, lambda_z/t_half, CL, Vd, MRT) with the AUC-extrapolation>20% and terminal-fit-r^2 quality gates. - Gotchas: extrapolation, weak terminal fit, oral CL/Vd are apparent (CL/F), unit-driven scaling, flip-flop kinetics. - scripts/nca_from_csv.py: NCA from a CSV with BLQ handling, FDA/EMA linear-up/ log-down trapezoidal AUC + terminal regression. Cross-validated vs the tool (Cmax 6.1, AUC0-last 42.1, CL 2.3 match). - Routing rows added (both skills/ and plugin/skills/ mirrors). * Add enzyme-kinetics (Michaelis-Menten) skill Discovery task-gap (Sub-loop B): EnzymeKinetics_calculate exists (MM Km/Vmax via nonlinear fit + Lineweaver-Burk, catalytic efficiency, and inhibition-mode analysis) but no skill — enzyme kinetics was only mentioned in domain skills. tooluniverse-enzyme-kinetics encodes: - Data prep: initial velocities (not endpoints), substrate range must span Km, units, and the [E] requirement for kcat. - michaelis_menten + inhibition operations. - 'Prefer nonlinear over Lineweaver-Burk' guidance (LB distorts error — for visualization only, never final values). - Interpretation table (Km affinity, Vmax, kcat turnover, kcat/Km specificity constant incl. the diffusion limit, R^2/residuals). - Inhibition table (competitive Km up/Vmax same, uncompetitive both down, non-competitive Vmax down; Ki). - Gotchas: substrate inhibition (biphasic), Km out of range, no kcat without [E]. - scripts/fit_michaelis_menten.py: nonlinear MM fit from CSV + Vmax->kcat-> kcat/Km with --enzyme-conc; flags out-of-range Km / curved residuals. Cross-checked vs the tool (Vmax/Km within optimizer variance, R^2~0.998). - Routing rows added (both skills/ and plugin/skills/ mirrors). * Add primer-design skill (PCR/qPCR primers + oligo QC) Discovery task-gap (Sub-loop B): four primer/oligo tools exist (DNA_primer_design, NEB_Tm_calculate, IDT_analyze_oligo, DNA_calculate_gc_content) but no skill — a very common molecular-biology task with no workflow. tooluniverse-primer-design encodes: - DNA_primer_design orchestration + the non-obvious target/product-size constraint (product_size_min <= target span <= product <= product_size_max), with the two common errors and their fixes. - NEB_Tm_calculate (Tm + polymerase-specific Ta) and IDT_analyze_oligo (GC, MW, hairpin/dimer) usage, plus the 'don't mix Tm methods/conditions' rule. - Primer design-rule table (length, Tm 58-62, dTm<3, GC 40-60, GC clamp, 3'-end, runs, qPCR amplicon size). - Explicit specificity warning: the tools do NOT check genome specificity — always BLAST/Primer-BLAST before ordering. - scripts/primer_qc.py: dependency-free QC of a primer pair (length, GC%, GC clamp, 3' self/cross-dimer, runs, rough Tm + Tm match) with PASS/WARN. Verified on a designed pair (all PASS) and a bad pair (flags runs/GC/Tm). - Routing rows added (both skills/ and plugin/skills/ mirrors). * Add diagnostic-test-evaluation skill (2x2 + ROC/AUC + Bayes) Discovery task-gap (Sub-loop B): Epidemiology_diagnostic (2x2 sens/spec/PPV/NPV/ LR) and Epidemiology_bayesian (post-test probability) exist, but no skill, and ROC-curve analysis for a continuous biomarker (AUC, Youden cutoff) had no home. tooluniverse-diagnostic-test-evaluation encodes: - Case routing: 2x2 table -> Epidemiology_diagnostic; continuous score -> ROC; pre-test prob -> Epidemiology_bayesian. - 2x2 metrics table + the central PPV/NPV-prevalence-dependence trap (report sens/spec/LR as prevalence-independent; get real-world PPV from Bayes). - ROC interpretation (AUC bands), Youden optimal cutoff, and the cost-asymmetry caveat. - Gotchas: AUC ignores the operating point, class imbalance -> PR curve, spectrum bias, threshold chosen+evaluated on the same data. - scripts/roc_analysis.py: sklearn ROC AUC + bootstrap 95% CI + Youden cutoff (sens/spec) + text ROC curve. Verified (AUC 0.878 [0.825,0.927], cutoff found). - Routing rows added (both skills/ and plugin/skills/ mirrors). * Add drug-synergy skill (Bliss/Loewe/HSA/ZIP/Combination Index) Discovery task-gap (Sub-loop B): five DrugSynergy_calculate_* tools exist (Bliss, HSA, Loewe, ZIP, Chou-Talalay CI) but no skill — only the SYNERGxDB *database* lookup is referenced (cell-line-profiling), not the compute-from- your-own-data tools. tooluniverse-drug-synergy encodes: - Model-selection table keyed by the DATA you measured (single effects -> Bliss; effect arrays -> HSA; dose-response curves + combo -> Loewe/CI; full dose x dose viability matrix -> ZIP) with each tool's exact (and inconsistent) param names. - What each model's additivity null means + when to use it. - The fractional-inhibition vs %-viability scale rule (most common error: inhibition = 1 - viability/100). - Interpretation table incl. the Combination Index's opposite direction (CI<1 = synergy), dose-dependence, and ceiling effects. - Gotchas: scale mismatch, ZIP/Loewe need >=3 non-zero dose points, model disagreement is normal (pre-specify + report the model), synergy != efficacy. - scripts/synergy_reference.py: Bliss/HSA/additive expectations side-by-side from one dose pair; matches the tool (Bliss expected 0.580, score 0.120). - Routing rows added (both skills/ and plugin/skills/ mirrors). * Add molecular-cloning skill (Gibson + Golden Gate assembly design) Discovery task-gap (Sub-loop B): DNA_gibson_design and DNA_golden_gate_design exist but no skill — molecular cloning assembly design had no workflow (complements the new primer-design skill). tooluniverse-molecular-cloning encodes: - Gibson-vs-Golden-Gate method-selection table. - Gibson: overlap design rules (15-40 bp, Tm balance, fragment order, unique junctions, avoid repeats); DNA_gibson_design returns the homology arms to append to primers. - Golden Gate: Type IIS (BsaI/BbsI) with the mandatory domestication rule (no internal enzyme sites) + unique non-palindromic 4-bp overhangs. - QC + gotchas: internal Type IIS sites (the #1 failure), non-unique overlaps, junction repeats, overlap-Tm imbalance; fragments still need primers (-> tooluniverse-primer-design) + specificity BLAST. - scripts/cloning_qc.py: screens parts for internal BsaI/BbsI sites, overhang uniqueness/palindromes, and Gibson overlap length/GC. Verified (catches an internal BsaI site, a palindromic overhang, passes clean overlaps). - Routing rows added (both skills/ and plugin/skills/ mirrors). * Move BGPT tool tests to tests/tools/ (convention) tests/tools/ is the convention for tool-specific tests (per devtu-create-tool); the file was placed in tests/unit/ by mistake. No content change. * Move test_antibody_registry_tool tests to tests/tools/ (convention) tests/tools/ is the convention for tool-specific tests (per devtu-create-tool). No content change. * Move test_retraction_tool tests to tests/tools/ (convention) tests/tools/ is the convention for tool-specific tests (per devtu-create-tool). No content change. * Move test_genebe_tool tests to tests/tools/ (convention) tests/tools/ is the convention for tool-specific tests (per devtu-create-tool). No content change. * Move test_swisslipids_tool tests to tests/tools/ (convention) tests/tools/ is the convention for tool-specific tests (per devtu-create-tool). No content change. * Add meta-analysis skill tests (effect-size conversions + pooling) 7 pytest tests covering OR/CI->log+SE, Hedges' g, Fisher z conversions and the fixed/random-effects pooling math. In skills/ only (sync-skills.sh filters test_*.py out of plugin/skills/). * Add dose-response skill tests (4PL Hill fit) 3 pytest tests: 4PL endpoints/midpoint + IC50 recovery from a known curve. * Add pharmacokinetics skill tests (NCA AUC + terminal + BLQ) 4 pytest tests: linear/log-down trapezoid AUC, terminal lambda_z recovery, BLQ cleaning (leading->0, terminal->drop). * Add enzyme-kinetics skill tests (Michaelis-Menten fit) 3 pytest tests: v=Vmax/2 at [S]=Km, saturation to Vmax, Km/Vmax recovery. * Add primer-design skill tests (primer QC) 5 pytest tests: GC%, longest run, Tm GC-dependence + range, 3'-complementarity dimer detection. * Add diagnostic-test-evaluation skill tests (ROC bootstrap CI) 3 pytest tests: perfect-separation AUC=1, bootstrap CI brackets the point AUC, degenerate-resample handling (no NaN/crash). * Add drug-synergy skill tests (Bliss/HSA reference) 3 pytest tests: _call() synergy/additive/antagonism thresholds, Bliss expected 0.580/score 0.120 matches the tool, out-of-range fraction rejected. * Add molecular-cloning skill tests (assembly QC) 5 pytest tests: revcomp (incl. BsaI site), GC%, internal-BsaI-site flagging + domestication note, palindromic-overhang detection, clean-overlap pass. * Move test_bgpt_tool back to tests/unit/ (runs in PR CI) These are MOCKED unit tests. The repo CI runs tests/unit/ on every PR but tests/tools/ only on manual workflow_dispatch (network-gated integration suite). Mocked tool tests belong in tests/unit/ to actually run. * Move test_antibody_registry_tool back to tests/unit/ (runs in PR CI) These are MOCKED unit tests. The repo CI runs tests/unit/ on every PR but tests/tools/ only on manual workflow_dispatch (network-gated integration suite). Mocked tool tests belong in tests/unit/ to actually run. * Move test_retraction_tool back to tests/unit/ (runs in PR CI) These are MOCKED unit tests. The repo CI runs tests/unit/ on every PR but tests/tools/ only on manual workflow_dispatch (network-gated integration suite). Mocked tool tests belong in tests/unit/ to actually run. * Move test_genebe_tool back to tests/unit/ (runs in PR CI) These are MOCKED unit tests. The repo CI runs tests/unit/ on every PR but tests/tools/ only on manual workflow_dispatch (network-gated integration suite). Mocked tool tests belong in tests/unit/ to actually run. * Move test_swisslipids_tool back to tests/unit/ (runs in PR CI) These are MOCKED unit tests. The repo CI runs tests/unit/ on every PR but tests/tools/ only on manual workflow_dispatch (network-gated integration suite). Mocked tool tests belong in tests/unit/ to actually run. * primer-design: clarify the example template must be long enough (target_end is clamped) * SwissLipids: actionable not-found for non-existent ids (was raw HTTP 500) The entity endpoint returns HTTP 500 for non-existent/malformed SLM ids; map any HTTP error on get_lipid to a clear 'no entry, use SwissLipids_search' message (optimize-skills actionable-error standard). +1 test (7 total). |
||
|
|
3ae1554a04 | Release v1.2.5 | ||
|
|
4e34f5d7d6 |
install skill: add uv-tool-shadow gotcha, agent-runnable self-repair, fix release notes (#232)
- Troubleshooting: add the non-obvious 'global uv tool install shadows the
plugin's uvx -> stuck on an old version' case + its fix (uv tool uninstall).
- New 'Automated diagnosis & repair' section: a safe, idempotent 7-step bash
sequence an agent can run end-to-end ('read this skill and fix my install')
covering missing uvx, resolve/run check, the uv-tool shadow, stale cache,
Python version, interfering global skills, and plugin-enabled state.
- Fix stale maintainer release notes: reflect the auto-release.yml [release:X]
marker flow + max(manifest, tag) baseline, and that pip (pyproject/PyPI) is a
separate release line from the plugin.
|
||
|
|
b060f106d6 |
Add uv preflight check + document uv as a prerequisite (#230)
The MCP server launches via 'uvx tooluniverse'. If a user has Claude Code but not uv installed, the server silently fails to start and the tools never appear, with no guidance. Add a SessionStart hook that prints a one-line install hint ONLY when uvx is missing (failure-silent, exits 0, no output and no network when uv is present), and call out uv as a prerequisite in the README with the install command. |
||
|
|
30829d734c |
Release v1.2.4 (unified: pip + MCP registry + plugin) (#220)
Align all release channels at 1.2.4 and ship the backlog accumulated on main since 1.2.3: - pyproject.toml 1.2.3 -> 1.2.4 → triggers publish-pypi (ships #215 #217 #219 #213 #222 #223 src/ changes that never reached PyPI) + publish-mcp-registry - server.json 1.2.3 -> 1.2.4 (top-level + packages[]) for the MCP registry - plugin.json 1.2.1 -> 1.2.4 and marketplace.json 1.2.0 -> 1.2.4 → plugin auto-update (literature-search multi-source rewrite #216 etc.) Push tag v1.2.4 after merge for the GitHub Release. |
||
|
|
9d60718d39 |
statistical-modeling: report spline peak in fitted x-variable units (#221)
For 'at what ratio/frequency is the maximum' questions where the spline is fit on a frequency/proportion column (0-1), report that fraction (e.g. 0.909), not the colon-ratio label it was derived from (e.g. 10:1). Convert a:b to a/(a+b) when the question expects a 0-1 value. General convention; no dataset-specific values. |
||
|
|
657a6dcc69 |
Add Mendelian randomization skill + harden EpiGraphDB MR/genetic-cor tools (#213)
* Add Mendelian randomization skill + harden EpiGraphDB MR/genetic-cor tools Discovery-harness sweep found Mendelian randomization (MR) had no orchestrating skill despite working EpiGraphDB MR-EvE tools — a clean net-new causal-inference gap in genetic epidemiology. New skill tooluniverse-mendelian-randomization: - Resolve trait labels (OpenGWAS) -> MR estimate -> interpret -> triangulate - Encodes the three IV assumptions, MOE/IVW/Egger interpretation tables, bidirectional-MR reverse-causation check, and honest limitations (pre-computed pairs only, pleiotropy, European-ancestry instruments). Tool hardening (EpiGraphDB), driven by a persona validation smoke test: - get_mendelian_randomization: retry sentence-case trait labels and emit metadata.note instead of silently returning empty on a case mismatch (e.g. 'coronary heart disease' previously returned nothing). - get_genetic_correlations: same empty-result note; document that the /genetic-cor graph is sparse (|rg| > 0.8 only) and that pval_threshold is not applied upstream; fix the misleading description and replace non-returning test examples (Body mass index/Depression) with working ones (Waist circumference/Hip circumference). - search_opengwas: declare the undeclared meta_node field in return_schema. Validation: genetic-epidemiologist persona answered 'is LDL->CHD causal?' (OR~1.5, MOE 1.0) and 'is BMI->T2D causal?' (OR~2.2-3.2) end-to-end via the skill; the four issues it surfaced are fixed here. Adds tests/unit/test_epigraphdb_mr_casing.py (5 tests). * MR skill: skill-creator pass — pushier trigger description + evals.json Ran the skill against the skill-creator quality bar: - Rewrote the description to combat undertriggering: more trigger phrasings (causal risk factor, instrumental variables, 'is X causal for Y', observational-vs-causal) and explicit negative boundaries (not plain GWAS lookups; not custom-instrument two-sample MR). - Added evals/evals.json with three realistic test prompts and verifiable assertions (LDL->CHD, BMI->T2D, and a CRP->stroke label-resolution/empty-result-handling case). Structure already met the bar (118 lines < 500, explains why, examples, progressive disclosure). * Add OpenGWAS_get_mr_instruments tool for custom two-sample MR Fills the gap the MR skill documented: EpiGraphDB MR-EvE only covers pre-computed trait pairs, so there was no way to run MR on an arbitrary exposure/outcome GWAS pair. New tool assembles the instruments: - POST /tophits -> the exposure's genome-wide significant, LD-clumped SNPs - POST /associations -> those SNPs' effects in the outcome GWAS - harmonizes both onto a common effect allele (aligned / flipped / drop incompatible), returning MR-ready rows for an IVW/MR-Egger estimator Follows the create-tool contract: oneOf return_schema, real test IDs (ieu-a-2 BMI -> ieu-a-7 CHD), run() never raises, 60s timeout. Needs a free OPENGWAS_JWT token (required_api_keys + api_key_info + synced key catalog); clear registration hint when absent. Registered in default_config.py + _lazy_registry_static.py. Wires the tool into the mendelian-randomization skill + adds 5 unit tests. * plugin: don't ship skill evals/ scaffolding to the user-facing plugin evals/evals.json is skill-creator dev/test scaffolding (like test_*.py), not something plugin users need. sync-skills.sh only excluded test_*.py, so eval dirs leaked into plugin/skills/. Add evals/ to the rsync excludes and re-sync — removes the mendelian-randomization evals (this PR) and the pre-existing data-wrangling leak. The evals stay in the skills/ source tree for future skill-creator iteration. |
||
|
|
b6802d0cf7 | Fix and expand literature-search skills with adaptive multi-source coverage (#216) | ||
|
|
577f7c7638 |
fix(plugin): correct install skill — skill count, .mcp.json args, version pin (#206)
* fix(plugin): correct skill count, .mcp.json args, and version-pin in install skill * fix(plugin): use existing PyPI version (1.2.2) in install skill version-pin example |
||
|
|
ac41c28fe8 |
Add /tooluniverse:setup-keys command for API key configuration (#194)
* feat: add API key metadata catalog source * feat: add API key catalog generator * feat: generate API key catalog and regenerate .env.template * ci: sync API key catalog from tool configs * feat: load global ~/.tooluniverse/.env from any workspace * feat: add .env read/mask/merge helper for key setup * feat: add graphical API key setup server * feat: add /tooluniverse:setup-keys command and package it * feat: redesign setup-keys page with domain grouping and per-key context - Group keys by research domain (Genomics & Variants, Drugs & Chemistry, Proteins & Structure, Literature & Patents, Clinical & Safety, Models & Infrastructure) instead of Required/Optional/Endpoints. - Each key shows its purpose (what it unlocks) and a without-it note (blocked / demo mode / lower limits / leave blank unless self-hosting). - Light product UI: white cards in responsive grid, indigo accent, Hanken Grotesk + IBM Plex Mono, live progress meter, show/hide toggles. - Enrich api_key_metadata.json with domain, purpose, without fields; generator emits them in api_keys_catalog.json; .env.template regrouped by domain with per-key Without notes. * fix: exclude __pycache__ from plugin scripts dir during build cp -r was shipping stray .pyc files / __pycache__ dirs into the built plugin if a developer had run the scripts locally before building. Switch to rsync with explicit excludes, and add a regression test that plants a canary __pycache__ before the build and asserts dist/ stays clean. * fix: split one-line try/except in pycache regression test (ruff E701) * refactor: declare API key metadata inline in tool configs (api_key_info) Instead of a separate api_key_metadata.json, each tool config now carries an api_key_info block next to its required/optional_api_keys. The generator scans configs for both the key names and their info, so adding a new key is a one-file edit. Deletes api_key_metadata.json; runtime code is untouched (required_api_keys stays a list of strings). Catalog + .env.template output is byte-identical. * chore: sync API key catalog [skip ci] * docs: document api_key_info convention in custom-tool skill The JSON-tool authoring guide now explains declaring required/optional_api_keys and the inline api_key_info block (domain/purpose/without/register_url/type) that flows into the /tooluniverse:setup-keys UI and .env.template, plus the generator + CI auto-sync step. * fix: add UMLS, ICD-11, Gemini, OpenRouter keys to the catalog These keys are read by umls_tool / icd_tool / llm_clients via os.getenv but were never declared in any tool config, so the generated catalog and .env.template omitted them (UMLS_API_KEY + ICD_CLIENT_ID/SECRET regressed vs main). Declare them as optional_api_keys with api_key_info on the relevant tools so they appear in /tooluniverse:setup-keys and .env.template. Runtime load behavior unchanged (optional, not required). --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
28ba4589ca |
Fix: sync SAE/DMS skills to source /skills/ directory (#200)
PR #192 and #193 wrote skill changes only to plugin/skills/, which is the auto-generated copy produced by plugin/sync-skills.sh from /skills/. The source-of-truth /skills/ directory was never updated, so the next sync would have wiped out: - 5 entirely new skills only present in plugin/skills/: - tooluniverse-protein-lof-mechanism - tooluniverse-protein-sae-variant-interpretation - tooluniverse-protein-structural-annotation-pdb - tooluniverse-residue-functional-mechanism-interpretation - tooluniverse-variant-predictor-dms-validation (replacing -dms-benchmarking) - SAE-integration paragraphs added to 21 existing skills (ACMG variant classification, rare-disease-diagnosis, variant-interpretation, etc.) This commit: 1. Adds the 5 missing skill directories to /skills/ 2. Backports the SAE-integration edits from plugin/skills/ into /skills/ 3. Renames plugin/skills/...-dms-benchmarking → ...-dms-validation (subsumes the still-open PR #197) Verified plugin/sync-skills.sh is now idempotent — running it produces no further changes. |
||
|
|
aa8060225a |
fix: 16 skills with broken/stale tool references (#193)
* Fix broken tool references across 16+ skills
Cross-skill audit (all 119 tooluniverse-* skills) found broken/stale
tool references. Two root causes:
(1) Direct renames — tools were renamed (case/suffix change) at some
point but skills still used old names. Verified each replacement
against current tu.all_tool_dict registry:
GWAS_search_associations -> gwas_search_associations
KEGG_get_pathway -> kegg_get_pathway_info
NCBI_search_gene -> NCBIGene_search
ArrayExpress_search -> arrayexpress_search_experiments
GEO_search_datasets -> geo_search_datasets
SRA_search_runs -> SRA_search_experiments
RCSB_get_entry_info -> RCSBData_get_entry
RCSB_search_structures -> RCSBAdvSearch_search_structures
IEDB_get_epitope -> iedb_get_epitope_mhc
IEDB_search_epitopes -> iedb_search_epitopes
MGnify_search_studies_detail -> MGnify_get_study_detail
OpenTargets_multi_entity_search -> OpenTargets_multi_entity_search_by_query_string
OpenTargets_search_target -> OpenTargets_multi_entity_search_by_query_string
RNAcentral_get_rna -> RNAcentral_get_by_accession
Rfam_search -> Rfam_search_sequence
HCA_search_projects -> hca_search_projects
ChEMBL_get_assays_by_chemblid -> ChEMBL_get_assay_activities
ChEMBL_get_bioactivity_by_chemblid -> ChEMBL_get_compound_record_activities
ChEMBL_get_target_by_chemblid -> ChEMBL_get_molecule_targets
ADMETAI_predict_metabolism -> ADMETAI_predict_toxicity
NvidiaNIM_alphafold2 -> alphafold_get_prediction
WormBase_search -> WormBase_get_gene
CellxGene_search_datasets -> CELLxGENE_get_cell_metadata
BRENDA_search -> BRENDA_get_enzyme_info
(2) Tools that ARE in TU but are API-key-gated and were thus
silently filtered out at load time. The skills called them by
correct name; the tool registry just doesn't surface them when
the credentials are missing. Annotated each one with the required
env var so skill authors / agents see why and how to enable:
NvidiaNIM_esm2_650m, _proteinmpnn, _rfdiffusion, _boltz2, _genmol
*(requires NVIDIA_API_KEY env var; free key at build.nvidia.com)*
BRENDA_get_enzyme_info
*(requires BRENDA_EMAIL + BRENDA_PASSWORD env vars; free academic
registration at brenda-enzymes.org)*
This is the same pattern as ESM_API_KEY gating the ESM SAE tools —
tool is implemented + registered, just hidden until creds are set.
16 skills touched, 42 line changes initial + corrections. No tool-code
or skill-logic changes — all edits are name substitutions or inline
annotations in the skill markdown.
* Fix semantic mismatches in tool name renames
Five skills had broken tool references where prior renames mapped to
existing tools with different semantics or non-existent tools:
- protein-therapeutic-design + binder-discovery + infectious-disease:
alphafold_get_prediction (fetches precomputed structure from EBI by
UniProt accession) was used as if it were AlphaFold2 inference from
sequence. Reverted to NvidiaNIM_alphafold2 (with API-key requirement
note) where the call passes a sequence.
- drug-target-validation: 'Metabolism' row referenced nonexistent
ADMETAI_predict_toxicity '(if available)'. Replaced with
ADMETAI_predict_CYP_interactions.
- stem-cell-organoid: nonexistent CellxGene_search_datasets replaced
with CELLxGENE_get_census_versions; misleading description of
CELLxGENE_get_cell_metadata corrected to point at the census
discovery tool first.
* Fix variant-interpretation Phase 4 alphafold_get_prediction duplicate
The
|
||
|
|
44997b5853 |
feat: ESM-C SAE variant interpretation + DMS analysis suite (#192)
* Phase A: ESM SAE variant interpretation (tool + skill)
Adds the first piece of full ada-f/esmc_sae integration into ToolUniverse:
a new tool that runs a protein sequence through the ESMC-6B Sparse
Autoencoder (SAE) via the EvolutionaryScale Forge API, plus a skill that
guides agents through SAE-based missense variant interpretation.
Changes:
src/tooluniverse/esm_tool.py:
+ _get_esmc_client() helper — separate ESMCForgeInferenceClient (the
existing _get_client returns ESM3ForgeInferenceClient which does not
expose SAE outputs)
+ ESMTool._get_sae_features() — new operation 'get_sae_features'
Calls client.logits(..., config=LogitsConfig(sae_config=SAEConfig(...))),
parses the torch.sparse_coo_tensor (L+2, 16384), strips BOS/EOS,
optionally filters to position +/- window, returns top-K features
per residue as JSON-friendly COO triples.
+ Defensive ordering: input validation BEFORE SDK import check, so a
caller with bad args sees the input error (not 'install esm').
+ License notice in output metadata (Cambrian non-commercial).
src/tooluniverse/data/esm_tools.json:
+ ESM_get_sae_features entry with full parameter schema, oneOf return
schema, real test example using TP53 R175H N-terminal fragment.
tests/tools/test_esm_sae_tool.py (new, 8 tests):
- Mocked SDK; all input + output paths covered including error cases.
plugin/skills/tooluniverse-protein-sae-variant-interpretation/SKILL.md:
+ New skill (212 lines). 5-step workflow:
1. gene -> UniProt accession
2. Fetch canonical sequence
3. Validate ref residue + build mutant
4. Call ESM_get_sae_features for ref + mut
5. Compute per-feature deltas, rank gained/lost, interpret
+ 6-category interpretation table
+ Cross-validation pattern
+ Provenance attribution to ada-f/esmc_sae
Verification:
- Real end-to-end SAE call on TP53 (209-AA fragment, R175H at position
175, +/- 8 window) returned 17 residues x 64 features = 1088 sparse
activations.
- 8 unit tests pass (mock SDK).
- 13 total tests pass (8 SAE + 5 Gemini), no regressions.
Phase A scope only — Phase B (label tool + composite) and Phase C
(ThermoMPNN + LoF synthesis skill) pending.
* Phase B (1/2): ESM_score_variant_sae_disruption composite tool
Convenience layer over ESM_get_sae_features that runs ref + mutant SAE
inference in one call and returns ranked top-K features lost/gained at
the mutation site. This is the standard entry point for SAE-based
variant interpretation.
Tool: ESM_score_variant_sae_disruption
Inputs: sequence, position (1-indexed), ref_aa, alt_aa,
window=8 (default), top_k_features=10 (default)
Output: ranked top_features_lost + top_features_gained with delta +
ref_activation_sum + mut_activation_sum per entry
Cost: 2 Forge credits. Latency ~3-6s.
Defensive validation:
- sequence + position + ref_aa + alt_aa all required
- sequence[position-1] must equal ref_aa (catches isoform mismatch
before consuming Forge credits)
Tests (4 new, all mocked SDK):
- happy path with correct shape + variant string
- ref_aa mismatch returns clear error with actual residue
- missing position / missing ref_aa each return distinct errors
- metadata reports forge_calls_made=2 + non-commercial license
Integration test (live Forge, TP53 R175H):
- 10.1s, 204 unique features touched, 2 Forge calls
- feature 13270 went 0.19 -> 0 (fully deactivated by R175H)
- feature 6404 weakened 0.83 -> 0.67
- ref_aa='K' instead of 'R' returned correct validation error
Skill: tooluniverse-protein-sae-variant-interpretation/SKILL.md
+ Added 'Quick path (recommended)' section pointing at the composite
tool; long path (manual ESM_get_sae_features + delta) preserved
for cases needing raw per-residue features.
Total SAE tests: 12 pass (8 from Tool 1 + 4 for Tool 3).
* Phase B (2/2): ESM_describe_sae_feature on-demand labeling tool
Closes the SAE interpretability gap: agents can look up what a feature_id
MEANS biologically without depending on Ada Fang's pre-computed feature
label parquet. Runs SAE inference across a curated 10-protein panel,
cross-references SAE-activating residues with UniProt feature
annotations, infers a dominant high-level category.
Tool: ESM_describe_sae_feature
Input: feature_id (0-16383), optional n_proteins / top_residues / use_cache
Output: category (catalytic | ligand-binding | ptm | domain | motif |
structural-stability | secondary-structure | transmembrane |
signal-peptide | propeptide | uncategorized),
confidence (0-1), category_vote_counts, supporting_evidence
Cost: 10 Forge credits first call, FREE on cache hit.
Cache: ~/.cache/tooluniverse/sae_labels/{sae_model}/feature_{id}.json
Latency: ~30-60s first call, <1s cache hit.
Pipeline: per protein -> fetch UniProt JSON (urllib, no new dep) ->
SAE inference -> find top-K activating residues for target feature_id
-> check overlapping UniProt features -> map types to high-level
categories. Aggregate votes across panel.
Curated panel (10 diverse well-annotated human proteins): TP53, EGFR,
KRAS, thrombin, CYP3A4, insulin, hemoglobin-beta, tPA, serum albumin,
AKT1. Covers DNA-binding, kinase, GTPase, serine protease, P450 enzyme,
signal-peptide hormone, oxygen carrier, fibrinolytic protease,
transport protein.
Tests (5 new, mocked SDK + UniProt, 17 SAE tests total all pass):
- Bad feature_id (out-of-range, wrong type, missing) errors clearly
- Bad n_proteins (0, > panel size) errors clearly
- 'uncategorized' when no UniProt overlap (honest 'don't know')
- Cache hit avoids new UniProt fetches; from_cache=true
- Voting: panel proteins with Modified residue at activating position
-> category='ptm', confidence=1.0
Integration test (live Forge, feature 13270 with n_proteins=3):
- 33.2s first call, 0.000s cache hit
- Activated only at EGFR C-terminal tail; no informative UniProt
annotations at those positions -> correctly returns 'uncategorized'
Phase B complete (Tools 2 + 3). Phase C remaining: ThermoMPNN tool +
tooluniverse-protein-lof-mechanism synthesis skill.
* Phase C: tooluniverse-protein-lof-mechanism skill (DynaMut2-based)
Final piece of the ada-f/esmc_sae TU integration: a synthesis skill that
combines 5 independent signals to propose a LoF mechanism for missense
variants and distinguishes 'structural stability LoF' (folding broken)
from 'direct functional disruption' (catalytic / binding / PTM site
damage).
5 signals:
1. AlphaMissense pathogenicity score
2. AlphaFold pLDDT (structural context at mutation position)
3. ESMC ΔlogP (evolutionary plausibility of substitution)
4. SAE feature disruption (which biological feature breaks) — from
ESM_score_variant_sae_disruption + ESM_describe_sae_feature
5. DynaMut2 ΔΔG (thermodynamic stability change)
Decision rule from ada's variant_lof_mechanism workflow:
ddG > +1 AND ΔlogP < 0 → structural stability LoF
ddG ≈ 0 AND SAE catalytic → direct catalytic LoF
ddG ≈ 0 AND SAE binding → binding LoF
ddG ≈ 0 AND SAE PTM → PTM LoF
ddG ≈ 0 AND SAE domain → interface / domain LoF
pathogenic but no SAE → generic damaging, mechanism unclear
Why DynaMut2 instead of ThermoMPNN (which ada repo uses):
- ThermoMPNN requires GPU + git clone + CUDA + conda; TU's design is
pip-install + single env var
- DynaMut2 is already in TU via hosted academic API (BioSig, UQ); no
extra setup for the user
- Both predict the same ΔΔG signal; for the binary 'ddG > +1 vs ≈ 0'
decision driving this skill, either model gives the same answer
- For users who need ThermoMPNN specifically (better accuracy near
threshold, double-mutant support, indel support), the skill's
'Optional: ThermoMPNN' section documents 1 local install path
(MIT license, Kuhlman Lab) + 5 hosted SaaS alternatives (Tamarind,
Neurosnap, BioLM, ProteinIQ, Levitate)
Provenance: Workflow adapted from variant_lof_mechanism.md in
ada-f/esmc_sae (Ada Fang, Marinka Zitnik lab). The 5-signal synthesis
+ mechanism decision rule + the structural-stability-vs-direct-
functional-disruption framework are from that work.
Cross-refs verified: all 9 tools referenced (UniProt_search,
UniProt_get_sequence_by_accession, UniProt_get_entry_by_accession,
AlphaMissense_get_variant_score, alphafold_get_prediction,
ESM_score_sequence, ESM_score_variant_sae_disruption,
ESM_describe_sae_feature, DynaMut2_predict_stability) exist in TU's
loaded tool registry.
No tests added — this is a methodology document; the tools it dispatches
have their own unit + integration tests in Phase A/B.
* Pre-push polish: skill stale wording fix + sequence length cap
Two issues found by pre-push audit:
1. plugin/skills/tooluniverse-protein-sae-variant-interpretation/SKILL.md
line 171 said 'Look up via ESM_describe_sae_feature (Phase B tool) —
once available' — but Phase B is now done. Updated to describe the
tool's actual cost / cache behaviour.
2. ESM_get_sae_features had no upper sequence-length cap. ESMC-6B Forge
handles ~2,700 AA in practice (per ada repo + EvolutionaryScale docs);
longer sequences fail with opaque server errors. Added explicit
pre-flight check that returns a clear actionable error before
wasting Forge credits.
Tests: +1 unit test (rejects overlong sequence with clear error).
Total 18 SAE tests, all pass.
* Phase D (1/2): Structure_annotate_per_residue tool
Per-residue structural annotation from a PDB structure:
- binding interface: scHA distance to partner chain(s) < cutoff
- ligand pocket: scHA distance to ligand heavy atoms < cutoff
- core vs surface: relative SASA (freesasa, isolated target chain)
- optional secondary structure from PDBe REST
Methodology adapted from ada-f/esmc_sae (Ada Fang, Marinka Zitnik lab) -
dms_analysis/scripts/04_compute_structural_annotations.py.
Verification: reproduces ada's kras_anno.csv with 168/168 region matches on
6VJJ (KRAS-RAF1 + GNP/MG). KRAS G12/G13/Q61/D119 oncogenic hotspots all
correctly classified as GTP-pocket residues; interface residues 21-41/67/71
match the known KRAS switch I/II region contacting RAF1-RBD.
New optional deps in [bioinformatics] extra: freesasa>=2.2.0.
Tests: 10 unit tests with synthetic PDB fixture, all pass. Real-PDB test
covered by the test_example in the JSON config (CLI smoke-test runnable).
* Phase D (2/2): port 6 DMS analysis skills from ada-f/esmc_sae
Adapts ada's dms_analysis/skills/ to the tooluniverse plugin layout:
1. tooluniverse-protein-structural-annotation-pdb
- Wraps Structure_annotate_per_residue (added in previous commit)
- Cross-refs verified: PDBeSIFTS_get_best_structures,
PDBeSIFTS_get_all_structures, RCSBAdvSearch_search_structures,
UniProt_get_sequence_by_accession, pdbe_get_entry_secondary_structure
2. tooluniverse-mavedb-dms-retrieval
- Orchestrates existing MaveDB_* tools (search, get_score_set,
get_variant_scores, search_experiments)
- Adds HGVS parsing, single-mutant filtering, canonical-numbering
verification step
3. tooluniverse-sae-mutant-tensor-build
- Library-scale orchestration of ESM_get_sae_features
- (20 x n_positions x 16384) tensor + WT-diagonal NaN convention
- Aggressive caching pattern documented
4. tooluniverse-sae-dms-global-validation
- Statistical workflow: Mann-Whitney U on topK_drop, neutral vs
disruptive group, robustness sweep over K/neutral-band/quantile
5. tooluniverse-sae-dms-hotspot-features
- Per-cluster permutation test (mean of max_drop, BH-FDR within
cluster) + descriptive top-5 ranking
- Calls ESM_describe_sae_feature for labels
6. tooluniverse-annotated-dms-heatmap
- Visualization: heatmap + sequence + structural annotation track
- Optional per-hotspot SAE feature callouts above heatmap
All skills:
- disable-model-invocation: true (explicit invocation, like skills 5 + 6)
- Provenance attribution to ada-f/esmc_sae throughout
- Honest-limitations section + cross-references to other TU skills/tools
- Tool names in code snippets verified against tu.all_tool_dict
Together with the SAE tools from Phase A+B+C and the
Structure_annotate_per_residue tool from Phase D (1/2), this PR delivers
the full TU implementation of ada-f/esmc_sae:
Ada's plan TU implementation
-----------------------------------------------------
Tool 1 (SAE extraction) ESM_get_sae_features
Tool 2 (SAE interpretation) ESM_describe_sae_feature
Tool 3 (structural annotation) Structure_annotate_per_residue
Tool 4 (DMS heatmap plotting) skill, no atomic tool needed
Skill: structural annotations tooluniverse-protein-structural-annotation-pdb
Skill: build per-mutant SAE tensor tooluniverse-sae-mutant-tensor-build
Skill: SAE global drop vs DMS tooluniverse-sae-dms-global-validation
Skill: SAE hotspot enrichment tooluniverse-sae-dms-hotspot-features
Skill: retrieve DMS from MaveDB tooluniverse-mavedb-dms-retrieval
Skill: annotated DMS heatmap tooluniverse-annotated-dms-heatmap
* Test+fix: execute every Python snippet in the 6 DMS skills
Process correction: shipped 6 DMS-analysis SKILL.md files with 37 Python
code blocks WITHOUT executing the load-bearing logic against synthetic data
first. This violated the harness rule I had just codified (devtu-create-tool
'Top 7 Mistakes #3: Fake test_examples — tests fail').
Built tests/tools/test_dms_skill_snippets.py:
17 tests covering the verbatim Python logic from 5 of the 6 DMS skills:
- skill 8 (MaveDB retrieval): HGVS parser, matrix reshape, landmark check
- skill 9 (SAE tensor build): pooled features shape, WT-diagonal NaN, cache function
- skill 10 (global validation): drop computation, topk_drop, categorize, MWU
- skill 11 (hotspot features): max_drop, cluster chaining, permutation_pvalues, descriptive top-5
- skill 12 (annotated heatmap): landmark alignment, full matplotlib render to PNG
- skill 7 (structural annotation): field-extraction pattern (caught real bug)
Real bugs found and fixed:
1. Skill 7 read 'annotations[X-1]["region"]' — assumes positions are
1-indexed and contiguous, which PDB chains often violate. Changed to
dict-by-position pattern: 'by_pos = {a["position"]: a for a in
annotations}; by_pos[X]["region"]'. Tested both in the skill (verbatim)
and the snippet test.
Also added live-PDBe test for Structure_annotate_per_residue's
include_secondary_structure=True code path — was previously untested.
Verifies KRAS β1 strand (residues 2-9) and α3 helix (residues 88-104) come
back labeled correctly. Skipped automatically if PDBe REST is unreachable.
Tests now: 28 (was 28) + 18 new (17 snippet + 1 live SS) = 46 total, all pass.
* Docs: bump skill count 115->121 in plugin.json + CHANGELOG 1.2.1 entry
- plugin/.claude-plugin/plugin.json: skill count 115 -> 121 (added 2 in
Phase A+C: protein-sae-variant-interpretation + protein-lof-mechanism;
added 6 in Phase D: protein-structural-annotation-pdb, mavedb-dms-retrieval,
sae-mutant-tensor-build, sae-dms-global-validation, sae-dms-hotspot-features,
annotated-dms-heatmap), version 1.2.0 -> 1.2.1 to match the actual release
pyproject.toml ships.
- plugin/CHANGELOG.md: new [1.2.1] section documenting the 4 new tools +
8 new skills, verification status (46 unit tests, 168/168 KRAS region
match, TP53 R175H reproduction, PDBe SS live path), prerequisites, and
the Cambrian Inference Clickthrough License attribution.
* E2E integration test: KRAS DMS pipeline end-to-end (real Forge API)
Validates the full 6-skill DMS analysis chain on real data, not synthetic:
Step 1: UniProt_get_sequence_by_accession (P01116) → KRAS 189 AA
Step 2: MaveDB_get_variant_scores (urn:mavedb:00000115-a-7)
folding ΔΔG abundancePCA → 2191 variants
Step 3: HGVS parse + filter to positions 10-25 → 303 single missense
numbering check passes (MaveDB pos 10 ref=G == UniProt KRAS[9])
Step 4: ESM_get_sae_features per WT + per mutant → 319 Forge calls
Step 5: SAE drop + Mann-Whitney U: disruptive vs neutral
ΔΔG range -0.49 to 2.92, neutral=173, disruptive=91
disruptive median drop = 0.057 vs neutral = 0.041
→ p = 0.00706 (SAE drop correlates with folding disruption)
Step 6: G12/G13 cluster hotspot → top 5 features labeled via
ESM_describe_sae_feature:
feature 6255 → ligand-binding (conf 0.50)
feature 271 → secondary-structure
feature 11930 → secondary-structure
feature 10721 → ligand-binding (conf 0.50)
feature 6615 → secondary-structure
Biology check passes: KRAS G12/G13 sit in the P-loop / Walker-A motif that
binds the β-phosphate of GTP. The top-ranked feature at this hotspot is
labeled 'ligand-binding' — the expected mechanism.
Integration bugs found: 0. All 6 skills (mavedb-dms-retrieval,
sae-mutant-tensor-build, sae-dms-global-validation, sae-dms-hotspot-features,
... ) chain together with consistent shapes / dtypes / orderings on real
data; no shape mismatches; HGVS parser handles MaveDB's actual hgvs_pro
format; numbering pins correctly to UniProt canonical.
Test is skipped by default (set RUN_DMS_E2E=1 to opt in). Reference output
from the successful run is committed at
tests/integration/dms_pipeline_e2e_kras_output.txt as evidence.
* Fix: MaveDB_get_variant_scores returns ALL variants by default
The /scores endpoint downloads the full CSV in one HTTP request; limit
was a client-side truncation that defaulted to 50 and was hard-capped at
500. For whole-protein DMS workflows (KRAS folding ΔΔG = 3553 variants,
TP53 = thousands) this silently truncated the data, forcing agents to do
per-position hgvs_pro substring pagination — which I had to write into the
DMS skill as a workaround.
Fix:
- default limit changed to 0 (return all)
- limit=null / 0 / negative all mean 'no truncation'
- response now reports total_variants_in_set as a plain integer plus
truncated:bool and limit_applied:int|null so callers can detect
silent truncation explicitly
- removed the 500 cap (the API streams it all anyway, capping client-side
discarded data we already paid for downloading)
Files:
src/tooluniverse/mavedb_tool.py:
_get_variant_scores - limit logic + new response fields
src/tooluniverse/data/mavedb_tools.json:
schema: limit is now [integer, null], default 0; description rewritten;
return_schema has new truncated + limit_applied fields
tests/tools/test_mavedb_tool.py (NEW, 9 tests):
default -> all 3000, limit=0 -> all, limit=100 -> truncated, limit>data -> all,
limit=-5 -> all, hgvs_filter, missing urn, 404, empty CSV
plugin/skills/tooluniverse-mavedb-dms-retrieval/SKILL.md:
Step 3 simplified - single call returns everything, no pagination workaround
tests/integration/test_dms_pipeline_e2e_kras.py:
Step 2 simplified from 17 paginated calls to 1
tests/integration/dms_pipeline_e2e_kras_output.txt:
regenerated reference output (now shows 3553 variants from a single call)
Verification:
9/9 new unit tests pass
E2E pipeline still finds the G12/G13 hotspot top feature labeled
'ligand-binding' (the expected KRAS P-loop / Walker-A biology)
* Reframe 3 DMS skills around user goals, not methodology
The original sae-* prefixed skills described what the methodology DID
(validate SAE method, find SAE features at hotspots). Users actually need
answers to broader questions:
- 'Does my variant-effect predictor work on this DMS?' (not 'is SAE good?')
- 'Why is this hotspot critical?' (not 'what SAE features dropped?')
Refactor (3 skills became 2, more general purpose):
DELETED:
tooluniverse-sae-mutant-tensor-build (was just a pipeline step, not a goal;
content folded into the two below)
tooluniverse-sae-dms-global-validation (replaced by predictor-benchmarking)
tooluniverse-sae-dms-hotspot-features (replaced by hotspot-mechanism)
NEW:
tooluniverse-variant-predictor-dms-benchmarking
Goal: validate ANY per-variant predictor against DMS data.
Works for: AlphaMissense, ESM-C SAE drops, ESM logits, EVE,
conservation, DynaMut2, any custom score.
Same statistical methodology (MWU + robustness sweep); SAE is shown
as the worked example. Reviewer-quality benchmark output.
tooluniverse-dms-hotspot-mechanism-interpretation
Goal: explain WHY a DMS hotspot is functionally critical.
Synthesizes 3-4 evidence layers:
- Structural context (Structure_annotate_per_residue: interface /
pocket / core / secondary structure)
- UniProt features (active site / binding site / PTM / disulfide)
- SAE feature drops (optional 4th layer, more rigorous via
permutation test or fast via descriptive ranking)
Returns a mechanism call: catalytic / ligand-binding / interface /
structural-core / PTM / regulatory / mixed / unknown — synthesized
from the evidence stack, with the reasoning shown.
Updates:
plugin/skills/tooluniverse-mavedb-dms-retrieval/SKILL.md: cross-refs
now point to the 2 reframed skills (predictor-benchmarking + hotspot-
mechanism), not the 3 deleted SAE skills.
plugin/skills/tooluniverse-annotated-dms-heatmap/SKILL.md: callouts
cross-ref updated to hotspot-mechanism-interpretation.
plugin/CHANGELOG.md: 1.2.1 entry rewritten to describe the new
user-goal-oriented skills.
plugin/.claude-plugin/plugin.json: skill count 121 -> 120.
tests/tools/test_dms_skill_snippets.py: header + section comments
updated; statistical logic tests unchanged and all 17 still pass.
Verification: 55/55 unit tests pass (17 snippet + 9 MaveDB + 11 Structure
+ 18 ESM SAE).
* Merge annotated-dms-heatmap into dms-hotspot-mechanism-interpretation
By the user-goal principle: visualization is a deliverable of hotspot
interpretation, not a user goal on its own. Nobody comes to ToolUniverse
saying 'I want to draw a DMS heatmap' — they come saying 'I want to
understand these hotspots, ideally with a publication figure I can use.'
Changes:
DELETED:
plugin/skills/tooluniverse-annotated-dms-heatmap/
Content folded into Step 7 of hotspot-mechanism-interpretation.
EXPANDED:
plugin/skills/tooluniverse-dms-hotspot-mechanism-interpretation/SKILL.md
New Step 7: 'Visualize — annotated DMS heatmap with hotspot callouts'
covering landmark alignment, heatmap + sequence + annotation track,
WT-vs-not-measured cell distinction, callout overlay, long-protein
panel splitting. The matplotlib code reads the structural annotation
and mechanism call data already computed in Steps 2/5.
CROSS-REFS UPDATED:
mavedb-dms-retrieval, protein-structural-annotation-pdb,
variant-predictor-dms-benchmarking: visualization line now points to
'Step 7 of dms-hotspot-mechanism-interpretation' instead of the
deleted standalone skill.
Docs:
plugin/.claude-plugin/plugin.json: skill count 120 -> 119.
plugin/CHANGELOG.md: 1.2.1 entry updated to reflect the merge.
tests/tools/test_dms_skill_snippets.py: header + section comments
updated; matplotlib snippet tests unchanged (now exercise Step 7
content of hotspot-mechanism instead of standalone heatmap skill).
Verification: 55/55 unit tests pass.
* Skill iter-2 improvements from eval feedback
Iteration-2 quality eval (4 user-question test prompts, 2 configs each, 8
subagent runs) exposed 3 actionable gaps in the refactored DMS skills.
All 3 fixed; iter-2 with_skill pass rate 100% (vs iter-1 91.7%).
variant-predictor-dms-benchmarking/SKILL.md:
1. NEW Step 3.5 (mandatory NaN sanity gate)
- Pre-MWU check that both groups have >=5 finite predictor scores;
raises ValueError instead of silently running MWU on all-NaN inputs.
- Sign-convention double-check via Spearman vs AlphaMissense.
Motivation: iter-1 eval-1 had SAE matrix all-NaN, agent ran MWU,
reported 'AM wins by default' — exactly the failure this gate prevents.
2. Step 4 commentary: rank correlation + AUROC as valid complements to MWU
Motivation: iter-1 eval-0 without_skill used Spearman+AUROC and gave a
richer answer than the skill's MWU-only path.
3. Predictor option B (AlphaMissense): rewrote with actual TU schema
- Real call is AlphaMissense_get_variant_score(uniprot_id, variant).
- Documents the hegelab proxy's categorical-bin response shape and
provides bin-midpoint parsing code (BIN_MIDPOINTS dict).
- Adds AlphaMissense_get_residue_scores + AlphaMissense_get_protein_scores
as cheaper alternatives for saturation/full-protein analyses.
- Documents the DeepMind bulk CSV as the higher-resolution alternative
when true per-substitution numerics are needed.
Motivation: iter-2 eval-0 with_skill agent flagged that the skill's
call signature was wrong (used non-existent uniprot_accession +
position + reference_amino_acid + alternate_amino_acid kwargs).
dms-hotspot-mechanism-interpretation/SKILL.md:
4. NEW Step 0 (mandatory premise check)
- When user names specific positions as a hotspot, verify they actually
rank high in this DMS before proceeding.
- Concrete decision rule: top 25% confirmed, top 50% noted, below 50%
MUST be flagged as mismatch at the top of the answer.
- Worked example: KRAS G12/G13 rank 105/187 + 124/187 by max ΔΔG in
the AbundancePCA assay — famously oncogenic but NOT folding hotspots.
Motivation: iter-1 eval-2 with_skill caught this mismatch by accident;
iter-2 with_skill agent followed the new explicit Step 0 and surfaced
it as the very first line of its report.
test_dms_skill_snippets.py:
3 new tests for the new skill code blocks (per harness rule):
- test_variant_predictor_alphamissense_bin_parsing: validates the
categorical-bin -> bin-midpoint parsing logic (empty, single, full
saturation cases)
- test_variant_predictor_nan_sanity_gate: validates the ValueError
raise on all-NaN predictor input, and the pass-through on sparse-
but-adequate input
- test_hotspot_premise_check_rank_logic: validates the ranking math
that decides 'top 25% / top 50% / below 50%' bucket for the user-
named positions
20/20 snippet tests now pass.
* Generalize skill 6: residue-functional-mechanism-interpretation
Skill 6 rename + scope generalization: the previous name
'tooluniverse-dms-hotspot-mechanism-interpretation' framed the entry as
'DMS hotspot' specifically, which excluded valid user goals where the
residues come from other sources.
The core methodology (multi-evidence synthesis: structural context +
UniProt features + optional SAE) is already source-agnostic. Only the
entry was DMS-locked. This commit generalizes the entry.
Rename:
tooluniverse-dms-hotspot-mechanism-interpretation
-> tooluniverse-residue-functional-mechanism-interpretation
Two entry paths:
Path A — user_provided_positions (NEW)
Skips Step 1 hotspot detection entirely. Covers:
- ClinVar recurrent variants (pull positions, pass directly)
- Literature hot regions (paste positions from paper Fig 1)
- Evolutionary conserved residues (filter by conservation, pass top-N)
- Druggable-site residues (from a binding-site predictor)
- Clinician's single-residue question ('why does R175 matter?')
Path B — DMS hotspot detection (ORIGINAL use case, preserved)
Same as before. DMS workflow runs unchanged.
Path A vs B evidence-layer contract documented:
Structural + UniProt + DMS-effect-size: ✓ both paths
SAE permutation test (needs DMS baseline): Path B only
SAE descriptive labels: both paths (if SAE tensor supplied)
Cross-refs updated in:
- plugin/skills/tooluniverse-mavedb-dms-retrieval/SKILL.md
- plugin/skills/tooluniverse-protein-structural-annotation-pdb/SKILL.md
- plugin/skills/tooluniverse-variant-predictor-dms-benchmarking/SKILL.md
- plugin/CHANGELOG.md (1.2.1 entry rewritten for the broader scope)
- tests/tools/test_dms_skill_snippets.py (header + section comments)
Snippet test added: test_residue_mechanism_path_a_user_provided_positions
verifies the new Path A entry: deduplication, ordering, gap<=2 clustering
on a mixed-source residue list (KRAS G12/G13 + TP53 R175 + R273).
Verification: 21/21 snippet tests pass (was 20). Skill count unchanged
(rename, not delete) at 119 tooluniverse-* skills.
Why this matters: the eval-driven design principle ('user goals not
methodology') still had one specific entry framing left. Surfaced by
honest self-audit after iter-2 100% pass rate.
* Promote MaveDB DMS retrieval from skill to tool
New tool MaveDB_get_effect_matrix(urn, uniprot_id?, score_field?) returns
ready-to-analyze (20 AAs x n_positions) effect matrix in one call. Internally:
fetch all variants (limit=0), parse HGVS, filter to single missense, auto-
detect score field, optionally verify position numbering against UniProt
canonical, reshape to standard layout. Returns full audit metadata.
Tests: 7 new unit tests in test_mavedb_tool.py (HGVS filter, score-field
auto-detection + override, no-usable-variants error, missing urn error,
numbering match + offset-detected paths). 37/37 across mavedb + snippet
test files.
Deleted plugin/skills/tooluniverse-mavedb-dms-retrieval/. The boilerplate
that lived there (HGVS parser + numbering check + matrix reshape) is now
hidden inside the tool. Consumers updated:
- tooluniverse-variant-predictor-dms-benchmarking/SKILL.md
Step 1 now calls the tool with one line instead of 30 lines of inline
parsing.
- tooluniverse-residue-functional-mechanism-interpretation/SKILL.md
Cross-refs updated.
Docs: plugin.json skill count 119 -> 118. CHANGELOG 1.2.1 updated for
5 new tools (was 4) + 5 new skills (was 6). The end state has every skill
mapping to a real user question and every tool mapping to a self-contained
action; no more 'orchestration step' skills in the SAE/DMS bundle.
* Fix AlphaMissense_get_protein_scores: actually return all residues
Iter-3 eval surfaced that AlphaMissense_get_protein_scores returned only
a 'sample' (1 residue) despite name + description claiming whole-protein
coverage. Skill 5 (variant-predictor-dms-benchmarking) iterates
data['scores'] keyed by 'resi', so the tool's actual output was a
load-bearing contract violation.
Fix: tool now actually fetches every residue:
1. Look up protein_length from UniProt FASTA
2. Concurrent per-residue hegelab API calls (ThreadPoolExecutor, 20 workers)
3. Aggregate into a 'scores' list, one record per successful position
New parameter: max_residues (default 0 = no cap). Set to a positive integer
to fetch only the first N residues for fast diagnostic calls on long proteins.
New response shape:
data.scores: List[{position, uid, aa, resi, benign, ambiguous, pathogenic,
benign_all, ambiguous_all, pathogenic_all, mean, mean_all}]
data.protein_length: int
data.n_positions_returned + n_positions_attempted + max_residues_cap
Live verification:
KRAS (max=20): 19/20 in 2.3s
KRAS (full 189): 188/189 in 15.9s (pos 1 = Met start, no missense data)
Tests:
- test_get_protein_scores_returns_per_residue_list pins the new contract
- test_get_protein_scores_max_residues_cap verifies the cap parameter
- existing test_get_protein_scores adjusted to use max_residues=5
14/14 pass.
Skill 5 unchanged - it was already written against the post-fix contract;
the bug was the tool not honoring it.
* Scrub personal path from e2e reference output before merge
Pre-merge audit found one occurrence of '/Users/shgao/.../ToolUniverse-auto/'
in tests/integration/dms_pipeline_e2e_kras_output.txt — it came from the
TU 'Auto-loaded workspace profile.yaml' banner printed at startup. Replaced
with <REPO_ROOT> placeholder.
No secrets (ESM_API_KEY, ClawInstitute admin key, AWS/OpenAI keys) and no
other personal paths in any of the 24 PR-touched files.
* Remove upstream-collaboration attributions across SAE/DMS suite
Scrub explicit references to Ada Fang / ada-f/esmc_sae / Marinka Zitnik
lab from all PR-touched skills, tools, JSON descriptions, CHANGELOG, and
test assertions. Reference paths (dms_analysis/scripts, kras_anno.csv)
also removed in favor of generic wording ('an upstream research workflow',
'the original script', 'reference annotation CSV').
Files updated:
plugin/CHANGELOG.md
plugin/skills/tooluniverse-protein-lof-mechanism/SKILL.md
plugin/skills/tooluniverse-protein-sae-variant-interpretation/SKILL.md
plugin/skills/tooluniverse-protein-structural-annotation-pdb/SKILL.md
plugin/skills/tooluniverse-residue-functional-mechanism-interpretation/SKILL.md
plugin/skills/tooluniverse-variant-predictor-dms-benchmarking/SKILL.md
src/tooluniverse/data/structure_annotation_tools.json
src/tooluniverse/structure_annotation_tool.py
tests/tools/test_structure_annotation_tool.py
Provenance language now uses neutral 'an upstream research workflow'.
The test_provenance_attribution_present assertion was updated to check
for the new phrasing.
Verification: 80/80 unit tests pass; 0 residual references across all
24 PR-touched files (Ada Fang, ada-f, Marinka Zitnik, esmc_sae, Mzitnik,
dms_analysis, kras_anno — all scrubbed).
* Strip residual attribution + reference-book language from skills
Second pass after initial scrub: skills are operational guidance, not
bibliographies. Removed:
- all remaining 'ada' / 'Ada's' / 'ada repo' phrases
- 'Provenance' paragraphs (5 skills) — they explain origin, not how to do anything
- empty '## References' sections (5 skills)
- script-path references ('05_validation1_global.py' etc.) leftover from scrub
- 'from upstream research' attribution clauses
- eval-debrief language ('The without-skill agent in the original eval missed this')
- one stray comment in esm_tool.py ('per ada repo + EvolutionaryScale docs')
Final exhaustive sweep: 0 residual matches for any attribution pattern
(Ada Fang, ada-f, Marinka Zitnik, esmc_sae, dms_analysis, kras_anno,
Provenance, original eval, from upstream research, etc.).
Verification: 80/80 unit tests pass. Skills now read as pure operational
guidance — 'how to do X', not 'this rule is from Y'.
* Add 3 composite SAE tools: batch variant scoring, region aggregation, mechanism explanation
The first 3 SAE tools (get_sae_features, score_variant_sae_disruption,
describe_sae_feature) expose the raw capability but leave common
workflows on the caller. These 3 additions cover the most-frequent
caller-side composites:
- ESM_score_variant_sae_batch: score N variants against one reference
with N+1 Forge calls instead of 2N (reference SAE computed once and
reused). Enables saturation mutagenesis (all 19 alts at a position),
DMS-style sweeps, and clinical variant panels at half the API cost.
Capped at 100 variants per call to keep cost predictable.
- ESM_get_region_sae_features: aggregate SAE features over a contiguous
residue range (a domain, epitope, binding pocket) and return features
ranked by total |activation| with per-residue hit pattern. Feeds into
ESM_describe_sae_feature on the top-K for category labels.
- ESM_explain_variant_mechanism: composite of disruption +
describe_sae_feature on each top affected feature + 1-line category
summary (e.g. "Disrupted feature categories (lost): catalytic=2,
ligand-binding=1"). Set include_descriptions=false to skip labeling
for cheaper calls. Used by variant-interpretation skills to get
mechanism in one call rather than orchestrating 2 + N calls.
All 3 reuse existing _get_sae_features / _score_variant_sae_disruption /
_describe_sae_feature internally to keep validation and error handling
consistent. License note (EvolutionaryScale Cambrian Inference License,
non-commercial only) propagated to all 3.
8 new tests added to test_esm_sae_tool.py covering happy path, Forge
call counting, input validation, and category aggregation. Full suite
27 passed (was 19).
* Integrate ESMC-6B SAE mechanism tools into variant-interpretation + rare-disease-diagnosis skills
Both skills already use AlphaMissense to score variant pathogenicity but
had no mechanistic interpretation layer. Adds the new SAE composite
tools (ESM_explain_variant_mechanism, ESM_score_variant_sae_batch,
ESM_get_region_sae_features) as the "how does the variant disrupt
function" complement to "is the variant pathogenic":
- variant-interpretation/SKILL.md: new Phase 4.2 "Mechanism of Effect"
between structural analysis and expression context. Recommends
ESM_explain_variant_mechanism as the one-call entry point, with
pointers to ESM_score_variant_sae_batch (saturation) and
ESM_get_region_sae_features (domain-level) for advanced workflows.
- variant-interpretation/TOOLS_REFERENCE.md: new "ESMC-6B SAE" section
with all 5 SAE tools, usage examples, and a table mapping SAE
categories (catalytic/ligand-binding/ptm/etc.) to ACMG criteria.
- rare-disease-diagnosis/DIAGNOSTIC_WORKFLOW.md: new step "2b" added to
the variant-interpretation pipeline that calls
ESM_explain_variant_mechanism alongside AlphaMissense when WT
sequence and aa-position are available.
- rare-disease-diagnosis/TOOLS_REFERENCE.md: new SAE section + table
entry alongside the existing AlphaMissense entry.
All integrations explicitly note the ESM_API_KEY + esm-feature-branch
install requirements and the EvolutionaryScale Cambrian Inference
License (non-commercial only) so users aren't surprised at runtime.
No tool code changed; this is documentation/workflow integration of the
3 composite SAE tools added in the prior commit.
* Integrate composite SAE tools into 4 mechanism-focused skills
After the prior commit added the 3 composite SAE tools
(score_variant_sae_batch, get_region_sae_features,
explain_variant_mechanism), update the 4 skills whose workflow they
directly improve:
- protein-lof-mechanism: recommend ESM_explain_variant_mechanism as the
one-call alternative to the existing ESM_score_variant_sae_disruption +
ESM_describe_sae_feature chain in Step 4 (the unique signal). Lower-
level pattern kept for callers that need raw feature_ids before
labeling (e.g. filter by category first).
- variant-predictor-dms-benchmarking: replace the per-variant ESM_get_
sae_features loop with ESM_score_variant_sae_batch as the preferred
saturation-scoring path (1 + N Forge calls vs 2N). Loop pattern kept
for callers that need the full per-residue x per-feature tensor for
downstream PCA / clustering.
- protein-sae-variant-interpretation: promote ESM_explain_variant_
mechanism as the fullest one-call entry point in the "Quick path"
section; add a saturation example using ESM_score_variant_sae_batch.
- residue-functional-mechanism-interpretation: Step 4 cluster-feature
ranking now has Path A (ESM_get_region_sae_features, 1 Forge call)
for the common contiguous-cluster case and Path B (precomputed DMS
tensor) for callers who already have one. Required-inputs table and
cross-reference table updated accordingly.
No tool code changed. Total +88/-31 across 4 SKILL.md files.
* Light-touch SAE mechanism notes in ACMG + cancer-variant skills
Both skills already use computational pathogenicity predictors
(AlphaMissense, REVEL, CADD). Add a brief mention of
ESM_explain_variant_mechanism as the mechanism complement that turns
"PP3 satisfied" into "PP3 satisfied + here's why" — without changing
the underlying classification logic.
- acmg-variant-classification: Phase 2 (Computational Predictions /
PP3, BP4) — note that PP3/BP4 is a vote count, not a mechanism.
For VUS-resolution narratives, ESM_explain_variant_mechanism adds
the lost/gained SAE feature categories (catalytic, ligand-binding,
ptm, etc.) as a mechanistic explanation alongside the PP3 verdict.
Does not strengthen PP3 above the predictor score alone.
- cancer-variant-interpretation: Driver vs Passenger Reasoning — for
unique (non-hotspot) missense in known driver genes, recommend
comparing the variant's SAE feature disruption to known hotspots in
the same gene. A novel missense that disrupts the same SAE category
as a known driver is more likely a driver than one disrupting
unrelated features.
Both notes are 1-3 sentences each, no new phases or sections added.
* Fix dead-code bug in rare-disease-diagnosis SAE step: actually parse variant + fetch WT sequence
The prior commit added a Step 2b that called ESM_explain_variant_
mechanism but gated it on variant_info["wt_sequence"], ["aa_position"],
["aa_ref"], ["aa_alt"] — none of which the surrounding workflow ever
populates (it uses ["aa_change"] like "V600E" and ["uniprot_id"] only,
visible in Step 2 AlphaMissense). Result: the SAE step never ran in
practice.
Fix: parse aa_change with a regex (handles both "V600E" and "p.V600E"
forms), fetch the WT sequence via UniProt_get_entry_by_accession, and
gate on a WT-residue-matches-ref-aa check so isoform mismatches are
silently skipped rather than surfacing as the tool's ref_aa-mismatch
error inside a diagnostic report.
Verified UniProt_get_entry_by_accession exists in the registry with
the expected (accession) signature.
* Fix ESM tools' multi-op routing: bind operation via fields, not parameter
Pre-existing bug across all 10 ESM tool configs: operation was declared
as a required parameter with a JSON-schema default, but TU does not
honor JSON defaults during validation. Result: any caller using
tu.tools.X(...) or tu.run_one_function without explicitly passing
operation=<name> hit "Parameter validation failed: operation is a
required property" before reaching the tool's run() dispatch. Every
SAE skill code example in the repo (existing + my new ones) was
silently broken.
Root cause: ESM tools used the parameter.operation pattern. Other
multi-op tools (AlphaMissense, MaveDB, Orphanet, OMIM, ClinGen, etc.)
use the fields.operation pattern: the operation is bound to the tool
instance in __init__ from tool_config["fields"]["operation"], and
run() reads it from self.operation instead of from arguments. With
that pattern, operation isn't part of the user-facing schema at all.
Changes:
- ESMTool.__init__: read self.operation from fields.operation
- ESMTool.run: prefer self.operation, fall back to arguments["operation"]
for backward compatibility with tests that pass it inline
- esm_tools.json (all 10 tools): add "fields": {"operation": "..."},
remove operation from parameter.properties + required, drop the
now-redundant operation field from test_examples
All 27 tests still pass (backward compatible). Verified via
tu.run_one_function on all 3 new composite tools: they now reach
their input-validation logic without the user passing operation.
* Fix ruff F841 in test_dms_skill_snippets — remove unused Path A locals
CI ruff check failed on:
tests/tools/test_dms_skill_snippets.py:599:5: F841 Local variable
`dms_matrix` is assigned to but never used
tests/tools/test_dms_skill_snippets.py:600:5: F841 Local variable
`disruptive_tail` is assigned to but never used
The Path A test (user_provided_positions, no DMS matrix) sets both
locals to None as documentation that they're irrelevant to this code
path, but then never references them. Remove the dead assignments —
the path-switch and the test comment above the if already document
that DMS-derived inputs aren't required here.
Verified locally:
ruff check . → All checks passed
pytest tests/tools/test_esm_sae_tool.py tests/tools/test_dms_skill_snippets.py → 48 passed
* Unwrap 3 malformed test_examples in ESM SAE tool configs
Three original SAE tool configs (ESM_get_sae_features,
ESM_score_variant_sae_disruption, ESM_describe_sae_feature) had
test_examples wrapped in {description, arguments} dicts instead of
flat parameter dicts. TU's test_example runner expects flat dicts
(matching every other tool in the repo, e.g. AlphaMissense, MaveDB),
so these examples would have failed schema validation on first call.
Unwrapped each {description, arguments} → arguments dict and dropped
the now-redundant operation field (routing handled by fields.operation
since the previous commit). Pre-existing bug exposed by my schema audit
after the operation-routing refactor.
|
||
|
|
16af425c05 |
Claude Code plugin: self-contained layout, skill-based routing, ML demo readiness (#161)
⏺ Introduces the Claude Code plugin for ToolUniverse as the recommended Claude Code
integration. Replaces the previous MCP-only setup with a one-command install
(`claude plugin install tooluniverse@tooluniverse`) that auto-configures the MCP
server, slash commands, sub-agent, hooks, and 115 specialized research skills.
Plugin structure
- 115 skills routed via a single visible router (router auto-matches by question
keywords + file extensions; sub-skills load on demand via Skill('name')).
- 5 slash commands (research, compare, cross-validate, literature-sweep,
translate-id) — each enforces a discipline the default agent doesn't apply.
- 1 sub-agent (researcher) — same investigation as /tooluniverse:research but
delegates to a forked-context subagent and returns one summary.
- SessionStart hook (idempotent) that cleans up legacy globally-installed
ToolUniverse skills so they don't shadow the plugin.
Skill changes (general, not benchmark-specific)
- Router gets a single percentage-vs-proportion units rule.
- Sub-skills get top-of-mind discipline banners: long-format methylation
ROWS-vs-unique-positions counting; Trimmomatic "reads completely discarded"
= F + R + 2*D; DEG-count default reads the padj-only line; ClinVar benign-
proportion 3-tier reporting; ortholog amino-acid single-representative sum;
raw + log10 sensitivity for count-vs-length Pearson.
Versions aligned at 1.2.0
- Plugin manifest + marketplace.json + Python package (`pyproject.toml`) all at
1.2.0. After merge, tagging v1.2.0 triggers the plugin-release workflow.
Docs
- New install + usage page (docs/guide/building_ai_scientists/claude_code.rst)
covering the two-command install, version pinning, API-key setup, plugin
troubleshooting, and the manual-MCP fallback.
Quality measurement
- BixBench closed-book official protocol (no reference notebook, hardened
harness with strict leak audit and isolated workspace): 67.3% (LLM-graded).
Compare to BixBench paper baseline 17% (Claude 3.5 Sonnet).
|