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).
This commit is contained in:
Shanghua
2026-06-05 20:11:47 -07:00
parent 7e40bdaaaf
commit 89ffda71e7
6 changed files with 456 additions and 204 deletions
+269 -194
View File
@@ -47,94 +47,119 @@
"return_schema": {
"oneOf": [
{
"type": "array",
"description": "List of FDA drug label summaries with clinical sections",
"items": {
"type": "object",
"properties": {
"brand_name": {
"type": [
"string",
"null"
]
},
"generic_name": {
"type": [
"string",
"null"
]
},
"manufacturer": {
"type": [
"string",
"null"
]
},
"route": {
"type": [
"string",
"null"
]
},
"pharm_class": {
"type": [
"string",
"null"
]
},
"boxed_warning": {
"type": [
"string",
"null"
]
},
"indications_and_usage": {
"type": [
"string",
"null"
]
},
"dosage_and_administration": {
"type": [
"string",
"null"
]
},
"contraindications": {
"type": [
"string",
"null"
]
},
"warnings_and_precautions": {
"type": [
"string",
"null"
]
},
"drug_interactions": {
"type": [
"string",
"null"
]
},
"spl_id": {
"type": [
"string",
"null"
]
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"success"
]
},
"data": {
"type": "array",
"description": "List of FDA drug label summaries with clinical sections",
"items": {
"type": "object",
"properties": {
"brand_name": {
"type": [
"string",
"null"
]
},
"generic_name": {
"type": [
"string",
"null"
]
},
"manufacturer": {
"type": [
"string",
"null"
]
},
"route": {
"type": [
"string",
"null"
]
},
"pharm_class": {
"type": [
"string",
"null"
]
},
"boxed_warning": {
"type": [
"string",
"null"
]
},
"indications_and_usage": {
"type": [
"string",
"null"
]
},
"dosage_and_administration": {
"type": [
"string",
"null"
]
},
"contraindications": {
"type": [
"string",
"null"
]
},
"warnings_and_precautions": {
"type": [
"string",
"null"
]
},
"drug_interactions": {
"type": [
"string",
"null"
]
},
"spl_id": {
"type": [
"string",
"null"
]
}
}
}
},
"metadata": {
"type": "object"
}
}
},
"required": [
"status",
"data"
]
},
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
]
},
"error": {
"type": "string"
}
},
"required": [
"status",
"error"
]
}
@@ -176,126 +201,151 @@
"oneOf": [
{
"type": "object",
"description": "Complete FDA drug label with all clinical sections",
"properties": {
"brand_name": {
"type": [
"string",
"null"
"status": {
"type": "string",
"enum": [
"success"
]
},
"generic_name": {
"type": [
"string",
"null"
]
"data": {
"type": "object",
"description": "Complete FDA drug label with all clinical sections",
"properties": {
"brand_name": {
"type": [
"string",
"null"
]
},
"generic_name": {
"type": [
"string",
"null"
]
},
"manufacturer": {
"type": [
"string",
"null"
]
},
"route": {
"type": [
"string",
"null"
]
},
"pharm_class": {
"type": [
"string",
"null"
]
},
"rxcui": {
"type": [
"array",
"null"
]
},
"boxed_warning": {
"type": [
"string",
"null"
]
},
"indications_and_usage": {
"type": [
"string",
"null"
]
},
"dosage_and_administration": {
"type": [
"string",
"null"
]
},
"dosage_forms_and_strengths": {
"type": [
"string",
"null"
]
},
"contraindications": {
"type": [
"string",
"null"
]
},
"warnings_and_precautions": {
"type": [
"string",
"null"
]
},
"adverse_reactions": {
"type": [
"string",
"null"
]
},
"drug_interactions": {
"type": [
"string",
"null"
]
},
"use_in_specific_populations": {
"type": [
"string",
"null"
]
},
"clinical_pharmacology": {
"type": [
"string",
"null"
]
},
"mechanism_of_action": {
"type": [
"string",
"null"
]
},
"spl_id": {
"type": [
"string",
"null"
]
}
}
},
"manufacturer": {
"type": [
"string",
"null"
]
},
"route": {
"type": [
"string",
"null"
]
},
"pharm_class": {
"type": [
"string",
"null"
]
},
"rxcui": {
"type": [
"array",
"null"
]
},
"boxed_warning": {
"type": [
"string",
"null"
]
},
"indications_and_usage": {
"type": [
"string",
"null"
]
},
"dosage_and_administration": {
"type": [
"string",
"null"
]
},
"dosage_forms_and_strengths": {
"type": [
"string",
"null"
]
},
"contraindications": {
"type": [
"string",
"null"
]
},
"warnings_and_precautions": {
"type": [
"string",
"null"
]
},
"adverse_reactions": {
"type": [
"string",
"null"
]
},
"drug_interactions": {
"type": [
"string",
"null"
]
},
"use_in_specific_populations": {
"type": [
"string",
"null"
]
},
"clinical_pharmacology": {
"type": [
"string",
"null"
]
},
"mechanism_of_action": {
"type": [
"string",
"null"
]
},
"spl_id": {
"type": [
"string",
"null"
]
"metadata": {
"type": "object"
}
}
},
"required": [
"status",
"data"
]
},
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
]
},
"error": {
"type": "string"
}
},
"required": [
"status",
"error"
]
}
@@ -334,28 +384,53 @@
"return_schema": {
"oneOf": [
{
"type": "array",
"description": "List of FDA pharmacological drug classes with product counts",
"items": {
"type": "object",
"properties": {
"drug_class": {
"type": "string"
},
"count": {
"type": "integer"
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"success"
]
},
"data": {
"type": "array",
"description": "List of FDA pharmacological drug classes with product counts",
"items": {
"type": "object",
"properties": {
"drug_class": {
"type": "string"
},
"count": {
"type": "integer"
}
}
}
},
"metadata": {
"type": "object"
}
}
},
"required": [
"status",
"data"
]
},
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
]
},
"error": {
"type": "string"
}
},
"required": [
"status",
"error"
]
}
+15 -2
View File
@@ -8,10 +8,23 @@
"pdb_id": {
"type": "string",
"description": "PDB entry ID (4 characters). Examples: '4HHB' (hemoglobin), '1TUP' (p53-DNA complex), '1M17' (EGFR kinase), '6LU7' (SARS-CoV-2 main protease)."
},
"entry_id": {
"type": "string",
"description": "Alias for pdb_id (the RCSB-native term; the REST endpoint is /core/entry/{id}). Provide either pdb_id or entry_id."
}
},
"required": [
"pdb_id"
"anyOf": [
{
"required": [
"pdb_id"
]
},
{
"required": [
"entry_id"
]
}
]
},
"fields": {
+22 -8
View File
@@ -19,6 +19,18 @@ from .tool_registry import register_tool
FDA_LABEL_URL = "https://api.fda.gov/drug/label.json"
def _ok(data: Any, **metadata: Any) -> dict:
"""Wrap a successful result in the standard ToolUniverse envelope.
Error paths already return {status: error, ...}; this keeps the success
path consistent with the project-wide {status, data, metadata} contract.
"""
metadata.setdefault("source", "openFDA drug label")
if isinstance(data, list):
metadata.setdefault("count", len(data))
return {"status": "success", "data": data, "metadata": metadata}
def _extract_label(result: dict) -> dict:
"""Extract key clinical sections from a raw openFDA label record."""
openfda = result.get("openfda", {})
@@ -121,7 +133,8 @@ class FDALabelTool(BaseTool):
return {"status": "error", "error": "Provide drug_name or indication"}
if drug_name:
return self._query_drug_fields(drug_name, limit) or []
labels = self._query_drug_fields(drug_name, limit) or []
return _ok(labels, query=drug_name, query_field="drug_name")
q = f'indications_and_usage:"{indication}"'
resp = requests.get(
@@ -129,11 +142,11 @@ class FDALabelTool(BaseTool):
params={"search": q, "limit": limit},
timeout=20,
)
if resp.status_code == 404:
return []
resp.raise_for_status()
results = resp.json().get("results", [])
return [_extract_label(r) for r in results]
labels = []
if resp.status_code != 404:
resp.raise_for_status()
labels = [_extract_label(r) for r in resp.json().get("results", [])]
return _ok(labels, query=indication, query_field="indication")
def _get_label(self, arguments: dict) -> Any:
drug_name = arguments.get("drug_name", "")
@@ -154,7 +167,7 @@ class FDALabelTool(BaseTool):
return len(brand) or 999
results.sort(key=_match_score)
return results[0]
return _ok(results[0], query=drug_name)
def _list_classes(self, arguments: dict) -> Any:
limit = min(int(arguments.get("limit", 20)), 100)
@@ -169,4 +182,5 @@ class FDALabelTool(BaseTool):
resp.raise_for_status()
data = resp.json()
results = data.get("results", [])
return [{"drug_class": r["term"], "count": r["count"]} for r in results]
classes = [{"drug_class": r["term"], "count": r["count"]} for r in results]
return _ok(classes)
+6
View File
@@ -62,6 +62,12 @@ class RCSBDataTool(BaseTool):
def _query(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Route to appropriate endpoint."""
# Accept the RCSB-native term 'entry_id' (and plain 'id') as aliases for
# 'pdb_id' — the REST endpoint is /core/entry/{id}, so users reach for it.
if not arguments.get("pdb_id"):
alias = arguments.get("entry_id") or arguments.get("id")
if alias:
arguments = {**arguments, "pdb_id": alias}
if self.endpoint == "entry":
return self._get_entry(arguments)
elif self.endpoint == "assembly":
+77
View File
@@ -0,0 +1,77 @@
"""Round 026: FDALabelTool returns the standard {status, data, metadata} envelope.
Regression for Feature-026C-1: FDA_search_drug_labels previously returned a bare
list (framework-wrapped to {"result": [...]}), inconsistent with the project-wide
success envelope. All three FDALabelTool query types now return {status, data,
metadata} on success and {status: error, ...} on failure.
"""
from unittest.mock import patch
from tooluniverse.fda_label_tool import FDALabelTool, _ok
def _cfg(query_type):
return {
"name": f"FDA_{query_type}",
"type": "FDALabelTool",
"fields": {"endpoint": "https://api.fda.gov/drug/label.json", "query_type": query_type},
"parameter": {"type": "object", "properties": {}},
}
class _Resp:
status_code = 200
def __init__(self, payload):
self._payload = payload
def raise_for_status(self):
pass
def json(self):
return self._payload
def test_ok_helper_wraps_list_with_count():
out = _ok([1, 2, 3], query="x")
assert out["status"] == "success"
assert out["data"] == [1, 2, 3]
assert out["metadata"]["count"] == 3
assert out["metadata"]["source"] == "openFDA drug label"
assert out["metadata"]["query"] == "x"
def test_ok_helper_does_not_count_dict():
out = _ok({"brand_name": "X"})
assert out["status"] == "success"
assert "count" not in out["metadata"]
def test_search_returns_standard_envelope():
raw = {"results": [{"openfda": {"brand_name": ["AVYCAZ"]}, "indications_and_usage": ["use"]}]}
tool = FDALabelTool(_cfg("search"))
with patch("tooluniverse.fda_label_tool.requests.get", return_value=_Resp(raw)):
out = tool.run({"drug_name": "Avycaz", "limit": 1})
assert set(out) >= {"status", "data", "metadata"}
assert out["status"] == "success"
assert isinstance(out["data"], list)
assert out["data"][0]["brand_name"] == "AVYCAZ"
assert out["metadata"]["query"] == "Avycaz"
def test_search_missing_args_is_error_envelope():
tool = FDALabelTool(_cfg("search"))
out = tool.run({})
assert out["status"] == "error"
assert "drug_name" in out["error"]
def test_list_classes_returns_standard_envelope():
raw = {"results": [{"term": "Cephalosporin", "count": 12}]}
tool = FDALabelTool(_cfg("list_classes"))
with patch("tooluniverse.fda_label_tool.requests.get", return_value=_Resp(raw)):
out = tool.run({"limit": 1})
assert out["status"] == "success"
assert out["data"] == [{"drug_class": "Cephalosporin", "count": 12}]
assert out["metadata"]["count"] == 1
+67
View File
@@ -0,0 +1,67 @@
"""Round 026: RCSBData_get_entry accepts the RCSB-native 'entry_id' alias.
Regression for Feature-026B-002: callers reaching for the RCSB-native term
'entry_id' (the endpoint is /core/entry/{id}) previously hit a hard schema
validation failure because only 'pdb_id' was accepted. The tool now aliases
entry_id/id -> pdb_id in _query() and the schema accepts either.
"""
from unittest.mock import patch
from tooluniverse.rcsb_data_tool import RCSBDataTool
def _cfg():
return {
"name": "RCSBData_get_entry",
"type": "RCSBDataTool",
"fields": {"endpoint": "entry"},
"parameter": {"type": "object", "properties": {}},
}
def test_entry_id_is_aliased_to_pdb_id():
captured = {}
def fake_get_entry(args):
captured.update(args)
return {"status": "success", "data": {"pdb_id": args.get("pdb_id")}}
tool = RCSBDataTool(_cfg())
with patch.object(tool, "_get_entry", side_effect=fake_get_entry):
out = tool.run({"entry_id": "8fmi"})
assert out["status"] == "success"
assert captured["pdb_id"] == "8fmi"
def test_explicit_pdb_id_not_clobbered_by_alias():
captured = {}
def fake_get_entry(args):
captured.update(args)
return {"status": "success", "data": {}}
tool = RCSBDataTool(_cfg())
with patch.object(tool, "_get_entry", side_effect=fake_get_entry):
tool.run({"pdb_id": "4HHB", "entry_id": "9ZZZ"})
assert captured["pdb_id"] == "4HHB"
def test_plain_id_alias_also_accepted():
captured = {}
def fake_get_entry(args):
captured.update(args)
return {"status": "success", "data": {}}
tool = RCSBDataTool(_cfg())
with patch.object(tool, "_get_entry", side_effect=fake_get_entry):
tool.run({"id": "6LU7"})
assert captured["pdb_id"] == "6LU7"
def test_no_identifier_returns_clear_error():
tool = RCSBDataTool(_cfg())
out = tool.run({})
assert out["status"] == "error"
assert "pdb_id" in out["error"]