mirror of
https://github.com/mims-harvard/ToolUniverse.git
synced 2026-09-19 07:31:47 +08:00
e2d9673fdd
* Fix Round 40: DailyMed deleted the division from antidote dose equations
SPL has no fraction element, so a dosing equation is drawn as a stacked
fraction: an underlined <content> numerator on its own line, a <br/>, then
the denominator, with the underline acting as the division bar. Every text
path in dailymed_tool flattened <br/> to a space or to nothing, which
deleted the division outright.
DigiFab (digoxin immune fab, setid c05ee6a5-c98b-45f4-83fd-40781639d653)
encodes its dosing equations this way, so DailyMed_parse_dosing returned
Dose (in vials) = (Serum digoxin ng/mL)(weight in kg) 100
which reads as a multiplication by 100 rather than a division by it -- a
10,000-fold error in an antidote dose, at the bedside, with nothing in the
response indicating anything had been dropped.
The <br/>-aware walker previously served table cells only. Fixing just that
path left the same label rendering the same equation two different ways in
one response, since section 2.1 repeats the equations in <paragraph>s. <br/>
is a property of SPL markup, not of tables, so _cell_text becomes _flow_text
and every flatten site (table cell, paragraph, list item, contraindications)
goes through it.
The bar is recognised only on the exact stacked-fraction shape: an underlined
<content> that follows an "=" and is followed by a <br/>. Requiring the "="
keeps an underlined heading before a line break -- this same label has two,
"Risk Summary" in Pregnancy and Lactation -- from becoming a division. That
false positive is pinned by its own test.
* Fix Round 40: FDA_get_drug_label returned no warnings for pre-PLR labels
_extract_label mapped its warnings_and_precautions response key from the PLR
section name only. openFDA carries a label in whichever format it was
submitted in and the two formats share no section names, so for every pre-PLR
label the key came back null -- next to truncated: false, an explicit
"nothing was cut".
Partitioning the live drug/label corpus of 261,646 records:
has warnings_and_cautions (PLR) 46,930
has warnings only, no warnings_and_cautions 204,636
has neither 10,080
So 204,636 of the 251,566 labels that carry a warnings section at all -- 81%
of them -- could not return it. This follows the label's vintage, not the
drug: flumazenil is pre-PLR while naloxone and pralidoxime are PLR.
Flumazenil is the benzodiazepine antidote and its WARNINGS section ("Risk of
Seizures ... not recommended in cases of serious cyclic antidepressant
poisoning") is the reason the drug is dangerous. It was retrievable by field
query the whole time; this extraction just never asked for it.
warnings_and_precautions falls back to the pre-PLR warnings section, and the
legacy PRECAUTIONS section is returned under its own name rather than folded
into the PLR keys, so provenance stays exact. general_precautions is the same
section under an alternate name on 25,729 labels, 725 of which have no
precautions at all.
search=_exists_:warnings_and_precautions returns NOT_FOUND -- that name is the
printed heading, never an openFDA field -- so the original first lookup was
dead code and is gone.
The sibling openfda_tool family deliberately keeps the opposite policy
(annotate, never substitute) because there the response keys ARE openFDA field
names; here the key is a format-neutral printed heading, so mapping either
source section onto it is correct. Both are noted where they differ.
* Fix Round 40: DrugSafetyAnalyzer required the filter it documented as optional
patient_sex was listed in `required` while its own description ended
"(optional)" and the composition function already handled its absence
(`arguments.get("patient_sex")`, then `if patient_sex:` before forwarding it
to FAERS). So the schema rejected the call the description invited:
$ python -m tooluniverse.cli run DrugSafetyAnalyzer \
'{"drug_name":"dexmedetomidine"}'
Error: Parameter validation failed for 'root':
'patient_sex' is a required property
There is no sex-unstratified drug safety review available while that holds,
and filtering by sex roughly halves the FAERS denominator.
An earlier sweep took serious_events_only out of `required` here, and
sample_type out of BiomarkerDiscoveryWorkflow's, on exactly this reasoning --
patient_sex was read the same way but left in. The guard that pinned the old
value is updated with the evidence rather than deleted.
The description also now states the denominator effect and that the enum is
Male/Female rather than the FAERS-native numeric encoding, which was the
first thing callers tried.
* Fix Round 40: europepmc_disease_target_score documented pageSize as a result limit
The parameter was described as "Number of results per page (default: 100,
max: 100)", but it is the upstream fetch batch size: the tool pages through
every one of the disease's associated targets regardless of it, bounded only
by a 25-second scan budget. Measured live, pageSize: 10 on MONDO_0005011
(Crohn disease) still returns 390 scored targets.
Behaviour is unchanged; the description now says what the tool actually does
and what the parameter actually controls, and the tool description states
that the result is exhaustive-and-time-bounded rather than paged.
* Fix Round 40: docs and examples called tools that do not exist
An earlier round removed BioRxiv_search_preprints and MedRxiv_search_preprints
(the bioRxiv/medRxiv APIs have no keyword-search endpoint at all) and cleaned
up the tool configs, but the runnable examples that told users to call them
were left behind:
$ python -m tooluniverse.cli run BioRxiv_search_preprints '{"query":"x"}'
Error: Tool 'BioRxiv_search_preprints' not found even after loading tools
$ python -c "from tooluniverse.tools import BioRxiv_search_preprints"
ImportError: cannot import name 'BioRxiv_search_preprints'
The second is not a stale doc, it is a shipped example that cannot be
imported. A sweep for the whole class found it was not two names but many:
docs/guide/tools.rst alone carried 95 non-existent tool names against 14 real
ones, and examples/oncogenomics/icgc called ICGCARGO_query, a tool that has
never existed in any config.
Every replacement name was checked with `cli info`, and most were executed
with the snippet's own arguments so the arguments are valid too, not just the
name. Where a snippet was obsolete with no modern equivalent it is deleted
rather than patched; where a name was a deliberate placeholder it is now
written as one (`<tool_name>`) so it cannot be mistaken for a real call.
test_documented_tool_names_exist guards the class, as the companion to
test_compose_tool_dependencies: that module checks names a config declares as
a dependency, this one checks names a human-facing runnable example tells a
user to call. Both failures are a name that resolves to nothing; the config
sweep cannot see this one because docs and examples are not configs.
Generated trees (docs/tools/, docs/locale/, src/tooluniverse/tools/) are
excluded as sources to fix -- a stale name there means the generator has not
been re-run, and a hand-edit would be reverted -- though tools/__init__'s
__all__ is used as ground truth for the import check.
* Fix Round 40: match the generated wrapper's docstring to the generator
The hand-edited DrugSafetyAnalyzer wrapper labelled the two now-optional
parameters `Optional[str]` / `Optional[bool]` in its numpydoc block. The
generator wraps `Optional[...]` around the SIGNATURE type only; the docstring
label is the raw JSON type (generate_tools.py:263 emits `py_type` unchanged,
and neither property is a nullable `["x","null"]` type). DrugSafetyAnalyzer.py
was the only one of 2738 files in src/tooluniverse/tools/ with that pattern,
which is what gave it away. Left as-is it would have silently reverted on the
next SDK regeneration.
The signature itself was already correct and is unchanged.
Also applies ruff format to the new sweep test, which the pre-commit
ruff-format hook would otherwise rewrite.
89 lines
3.3 KiB
Python
89 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Paper Search Tools Example
|
|
|
|
This example demonstrates how to use the optimized paper search tools
|
|
in ToolUniverse to search for academic papers across multiple databases.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
|
|
from tooluniverse import ToolUniverse
|
|
|
|
def main():
|
|
"""Main example function"""
|
|
|
|
# Initialize ToolUniverse
|
|
tu = ToolUniverse()
|
|
|
|
# Load paper search tools
|
|
print("Loading paper search tools...")
|
|
tu.load_tools(tool_type=[
|
|
"semantic_scholar", "EuropePMC", "OpenAlex", "arxiv", "pubmed", "crossref",
|
|
"biorxiv", "medrxiv", "hal", "doaj", "dblp", "pmc"
|
|
])
|
|
|
|
print(f"Loaded {len(tu.all_tools)} tools")
|
|
|
|
# Search query
|
|
query = "machine learning"
|
|
print(f"\nSearching for: '{query}'")
|
|
print("=" * 60)
|
|
|
|
# Define tools to test
|
|
tools_to_test = [
|
|
("ArXiv_search_papers", {"query": query, "limit": 2}, "ArXiv"),
|
|
("EuropePMC_search_articles", {"query": query, "limit": 2}, "Europe PMC"),
|
|
("openalex_literature_search", {"search_keywords": query, "max_results": 2}, "OpenAlex"),
|
|
("Crossref_search_works", {"query": query, "limit": 2}, "Crossref"),
|
|
# bioRxiv/medRxiv have no keyword-search endpoint -- their APIs are
|
|
# DOI/date retrieval only. Europe PMC (above) indexes both; fetch full
|
|
# preprint metadata by DOI with BioRxiv_get_preprint / MedRxiv_get_preprint.
|
|
]
|
|
|
|
# Test each tool
|
|
for tool_name, args, display_name in tools_to_test:
|
|
print(f"\n--- {display_name} ---")
|
|
|
|
try:
|
|
function_call = {"name": tool_name, "arguments": args}
|
|
result = tu.run_one_function(function_call)
|
|
|
|
if isinstance(result, list) and len(result) > 0:
|
|
paper = result[0]
|
|
print(f"Found {len(result)} papers")
|
|
print(f"Title: {paper.get('title', 'N/A')[:60]}...")
|
|
|
|
# Show data quality
|
|
if 'data_quality' in paper:
|
|
quality = paper['data_quality']
|
|
available_fields = [k for k, v in quality.items() if v]
|
|
print(f"Available fields: {', '.join(available_fields)}")
|
|
|
|
# Show key information
|
|
if paper.get('authors'):
|
|
print(f"Authors: {paper['authors'][:2]}..." if len(paper['authors']) > 2 else f"Authors: {paper['authors']}")
|
|
if paper.get('year'):
|
|
print(f"Year: {paper['year']}")
|
|
if paper.get('journal') or paper.get('venue'):
|
|
print(f"Journal: {paper.get('journal') or paper.get('venue')}")
|
|
if paper.get('citations') or paper.get('citation_count'):
|
|
citations = paper.get('citations') or paper.get('citation_count')
|
|
print(f"Citations: {citations}")
|
|
|
|
elif isinstance(result, dict) and 'error' in result:
|
|
print(f"Error: {result['error']}")
|
|
else:
|
|
print("No results found")
|
|
|
|
except Exception as e:
|
|
print(f"Error: {e}")
|
|
|
|
print("\n" + "=" * 60)
|
|
print("Example completed!")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|