tooluniverse 0.2.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tooluniverse might be problematic. Click here for more details.
- tooluniverse/__init__.py +340 -4
- tooluniverse/admetai_tool.py +84 -0
- tooluniverse/agentic_tool.py +563 -0
- tooluniverse/alphafold_tool.py +96 -0
- tooluniverse/base_tool.py +129 -6
- tooluniverse/boltz_tool.py +207 -0
- tooluniverse/chem_tool.py +192 -0
- tooluniverse/compose_scripts/__init__.py +1 -0
- tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
- tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
- tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
- tooluniverse/compose_scripts/literature_tool.py +34 -0
- tooluniverse/compose_scripts/output_summarizer.py +279 -0
- tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
- tooluniverse/compose_scripts/tool_discover.py +705 -0
- tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
- tooluniverse/compose_tool.py +371 -0
- tooluniverse/ctg_tool.py +1002 -0
- tooluniverse/custom_tool.py +81 -0
- tooluniverse/dailymed_tool.py +108 -0
- tooluniverse/data/admetai_tools.json +155 -0
- tooluniverse/data/adverse_event_tools.json +108 -0
- tooluniverse/data/agentic_tools.json +1156 -0
- tooluniverse/data/alphafold_tools.json +87 -0
- tooluniverse/data/boltz_tools.json +9 -0
- tooluniverse/data/chembl_tools.json +16 -0
- tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
- tooluniverse/data/compose_tools.json +202 -0
- tooluniverse/data/dailymed_tools.json +70 -0
- tooluniverse/data/dataset_tools.json +646 -0
- tooluniverse/data/disease_target_score_tools.json +712 -0
- tooluniverse/data/efo_tools.json +17 -0
- tooluniverse/data/embedding_tools.json +319 -0
- tooluniverse/data/enrichr_tools.json +31 -0
- tooluniverse/data/europe_pmc_tools.json +22 -0
- tooluniverse/data/expert_feedback_tools.json +10 -0
- tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
- tooluniverse/data/fda_drug_labeling_tools.json +1 -1
- tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
- tooluniverse/data/finder_tools.json +209 -0
- tooluniverse/data/gene_ontology_tools.json +113 -0
- tooluniverse/data/gwas_tools.json +1082 -0
- tooluniverse/data/hpa_tools.json +333 -0
- tooluniverse/data/humanbase_tools.json +47 -0
- tooluniverse/data/idmap_tools.json +74 -0
- tooluniverse/data/mcp_client_tools_example.json +113 -0
- tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
- tooluniverse/data/medlineplus_tools.json +141 -0
- tooluniverse/data/monarch_tools.json +1 -1
- tooluniverse/data/openalex_tools.json +36 -0
- tooluniverse/data/opentarget_tools.json +1 -1
- tooluniverse/data/output_summarization_tools.json +101 -0
- tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
- tooluniverse/data/packages/categorized_tools.txt +206 -0
- tooluniverse/data/packages/cheminformatics_tools.json +347 -0
- tooluniverse/data/packages/earth_sciences_tools.json +74 -0
- tooluniverse/data/packages/genomics_tools.json +776 -0
- tooluniverse/data/packages/image_processing_tools.json +38 -0
- tooluniverse/data/packages/machine_learning_tools.json +789 -0
- tooluniverse/data/packages/neuroscience_tools.json +62 -0
- tooluniverse/data/packages/original_tools.txt +0 -0
- tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
- tooluniverse/data/packages/scientific_computing_tools.json +560 -0
- tooluniverse/data/packages/single_cell_tools.json +453 -0
- tooluniverse/data/packages/structural_biology_tools.json +396 -0
- tooluniverse/data/packages/visualization_tools.json +399 -0
- tooluniverse/data/pubchem_tools.json +215 -0
- tooluniverse/data/pubtator_tools.json +68 -0
- tooluniverse/data/rcsb_pdb_tools.json +1332 -0
- tooluniverse/data/reactome_tools.json +19 -0
- tooluniverse/data/semantic_scholar_tools.json +26 -0
- tooluniverse/data/special_tools.json +2 -25
- tooluniverse/data/tool_composition_tools.json +88 -0
- tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
- tooluniverse/data/txagent_client_tools.json +9 -0
- tooluniverse/data/uniprot_tools.json +211 -0
- tooluniverse/data/url_fetch_tools.json +94 -0
- tooluniverse/data/uspto_downloader_tools.json +9 -0
- tooluniverse/data/uspto_tools.json +811 -0
- tooluniverse/data/xml_tools.json +3275 -0
- tooluniverse/dataset_tool.py +296 -0
- tooluniverse/default_config.py +165 -0
- tooluniverse/efo_tool.py +42 -0
- tooluniverse/embedding_database.py +630 -0
- tooluniverse/embedding_sync.py +396 -0
- tooluniverse/enrichr_tool.py +266 -0
- tooluniverse/europe_pmc_tool.py +52 -0
- tooluniverse/execute_function.py +1775 -95
- tooluniverse/extended_hooks.py +444 -0
- tooluniverse/gene_ontology_tool.py +194 -0
- tooluniverse/graphql_tool.py +158 -36
- tooluniverse/gwas_tool.py +358 -0
- tooluniverse/hpa_tool.py +1645 -0
- tooluniverse/humanbase_tool.py +389 -0
- tooluniverse/logging_config.py +254 -0
- tooluniverse/mcp_client_tool.py +764 -0
- tooluniverse/mcp_integration.py +413 -0
- tooluniverse/mcp_tool_registry.py +925 -0
- tooluniverse/medlineplus_tool.py +337 -0
- tooluniverse/openalex_tool.py +228 -0
- tooluniverse/openfda_adv_tool.py +283 -0
- tooluniverse/openfda_tool.py +393 -160
- tooluniverse/output_hook.py +1122 -0
- tooluniverse/package_tool.py +195 -0
- tooluniverse/pubchem_tool.py +158 -0
- tooluniverse/pubtator_tool.py +168 -0
- tooluniverse/rcsb_pdb_tool.py +38 -0
- tooluniverse/reactome_tool.py +108 -0
- tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
- tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
- tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
- tooluniverse/remote/expert_feedback/simple_test.py +23 -0
- tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
- tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
- tooluniverse/remote/immune_compass/compass_tool.py +327 -0
- tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
- tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
- tooluniverse/remote_tool.py +99 -0
- tooluniverse/restful_tool.py +53 -30
- tooluniverse/scripts/generate_tool_graph.py +408 -0
- tooluniverse/scripts/visualize_tool_graph.py +829 -0
- tooluniverse/semantic_scholar_tool.py +62 -0
- tooluniverse/smcp.py +2452 -0
- tooluniverse/smcp_server.py +975 -0
- tooluniverse/test/mcp_server_test.py +0 -0
- tooluniverse/test/test_admetai_tool.py +370 -0
- tooluniverse/test/test_agentic_tool.py +129 -0
- tooluniverse/test/test_alphafold_tool.py +71 -0
- tooluniverse/test/test_chem_tool.py +37 -0
- tooluniverse/test/test_compose_lieraturereview.py +63 -0
- tooluniverse/test/test_compose_tool.py +448 -0
- tooluniverse/test/test_dailymed.py +69 -0
- tooluniverse/test/test_dataset_tool.py +200 -0
- tooluniverse/test/test_disease_target_score.py +56 -0
- tooluniverse/test/test_drugbank_filter_examples.py +179 -0
- tooluniverse/test/test_efo.py +31 -0
- tooluniverse/test/test_enrichr_tool.py +21 -0
- tooluniverse/test/test_europe_pmc_tool.py +20 -0
- tooluniverse/test/test_fda_adv.py +95 -0
- tooluniverse/test/test_fda_drug_labeling.py +91 -0
- tooluniverse/test/test_gene_ontology_tools.py +66 -0
- tooluniverse/test/test_gwas_tool.py +139 -0
- tooluniverse/test/test_hpa.py +625 -0
- tooluniverse/test/test_humanbase_tool.py +20 -0
- tooluniverse/test/test_idmap_tools.py +61 -0
- tooluniverse/test/test_mcp_server.py +211 -0
- tooluniverse/test/test_mcp_tool.py +247 -0
- tooluniverse/test/test_medlineplus.py +220 -0
- tooluniverse/test/test_openalex_tool.py +32 -0
- tooluniverse/test/test_opentargets.py +28 -0
- tooluniverse/test/test_pubchem_tool.py +116 -0
- tooluniverse/test/test_pubtator_tool.py +37 -0
- tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
- tooluniverse/test/test_reactome.py +54 -0
- tooluniverse/test/test_semantic_scholar_tool.py +24 -0
- tooluniverse/test/test_software_tools.py +147 -0
- tooluniverse/test/test_tool_description_optimizer.py +49 -0
- tooluniverse/test/test_tool_finder.py +26 -0
- tooluniverse/test/test_tool_finder_llm.py +252 -0
- tooluniverse/test/test_tools_find.py +195 -0
- tooluniverse/test/test_uniprot_tools.py +74 -0
- tooluniverse/test/test_uspto_tool.py +72 -0
- tooluniverse/test/test_xml_tool.py +113 -0
- tooluniverse/tool_finder_embedding.py +267 -0
- tooluniverse/tool_finder_keyword.py +693 -0
- tooluniverse/tool_finder_llm.py +699 -0
- tooluniverse/tool_graph_web_ui.py +955 -0
- tooluniverse/tool_registry.py +416 -0
- tooluniverse/uniprot_tool.py +155 -0
- tooluniverse/url_tool.py +253 -0
- tooluniverse/uspto_tool.py +240 -0
- tooluniverse/utils.py +369 -41
- tooluniverse/xml_tool.py +369 -0
- tooluniverse-1.0.1.dist-info/METADATA +387 -0
- tooluniverse-1.0.1.dist-info/RECORD +182 -0
- tooluniverse-1.0.1.dist-info/entry_points.txt +9 -0
- tooluniverse/generate_mcp_tools.py +0 -113
- tooluniverse/mcp_server.py +0 -3340
- tooluniverse-0.2.0.dist-info/METADATA +0 -139
- tooluniverse-0.2.0.dist-info/RECORD +0 -21
- tooluniverse-0.2.0.dist-info/entry_points.txt +0 -4
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/WHEEL +0 -0
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# medlineplus_tool.py
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
import xmltodict
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
6
|
+
import re
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from .base_tool import BaseTool
|
|
10
|
+
from .tool_registry import register_tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@register_tool("MedlinePlusRESTTool")
|
|
14
|
+
class MedlinePlusRESTTool(BaseTool):
|
|
15
|
+
"""
|
|
16
|
+
MedlinePlus REST API tool class.
|
|
17
|
+
Supports health topic search, code lookup, genetics information retrieval, etc.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, tool_config):
|
|
21
|
+
super().__init__(tool_config)
|
|
22
|
+
self.timeout = 10
|
|
23
|
+
self.endpoint_template = tool_config["fields"]["endpoint"]
|
|
24
|
+
self.param_schema = tool_config["parameter"]["properties"]
|
|
25
|
+
|
|
26
|
+
def _build_url(self, arguments: dict) -> str:
|
|
27
|
+
"""Build complete URL"""
|
|
28
|
+
url_path = self.endpoint_template
|
|
29
|
+
placeholders = re.findall(r"\{([^{}]+)\}", url_path)
|
|
30
|
+
|
|
31
|
+
for ph in placeholders:
|
|
32
|
+
if ph not in arguments:
|
|
33
|
+
return {"error": f"Missing required parameter '{ph}'"}
|
|
34
|
+
url_path = url_path.replace(f"{{{ph}}}", str(arguments[ph]))
|
|
35
|
+
|
|
36
|
+
return url_path
|
|
37
|
+
|
|
38
|
+
def _extract_text_content(self, text_item: dict) -> str:
|
|
39
|
+
"""Extract content from text item"""
|
|
40
|
+
if not isinstance(text_item, dict):
|
|
41
|
+
return ""
|
|
42
|
+
|
|
43
|
+
text = text_item.get("text", {})
|
|
44
|
+
if not isinstance(text, dict):
|
|
45
|
+
return ""
|
|
46
|
+
|
|
47
|
+
html = text.get("html", "")
|
|
48
|
+
if isinstance(html, dict) and "html:p" in html:
|
|
49
|
+
paragraphs = html["html:p"]
|
|
50
|
+
if isinstance(paragraphs, list):
|
|
51
|
+
return "\n".join(
|
|
52
|
+
[
|
|
53
|
+
p.get("#text", "")
|
|
54
|
+
for p in paragraphs
|
|
55
|
+
if isinstance(p, dict) and "#text" in p
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
return html.replace("<p>", "").replace("</p>", "\n")
|
|
59
|
+
|
|
60
|
+
def _format_response(self, response: Any, tool_name: str) -> Dict[str, Any]:
|
|
61
|
+
"""Format response content"""
|
|
62
|
+
if not isinstance(response, dict):
|
|
63
|
+
return {"raw_response": response}
|
|
64
|
+
|
|
65
|
+
# Extract text content
|
|
66
|
+
def get_text_content(data, role):
|
|
67
|
+
text_list = data.get("text-list", [])
|
|
68
|
+
if isinstance(text_list, dict):
|
|
69
|
+
text_list = [text_list]
|
|
70
|
+
for item in text_list:
|
|
71
|
+
if isinstance(item, dict) and "text" in item:
|
|
72
|
+
text = item["text"]
|
|
73
|
+
if text.get("text-role") == role:
|
|
74
|
+
return self._extract_text_content(item)
|
|
75
|
+
return ""
|
|
76
|
+
|
|
77
|
+
# Extract list items
|
|
78
|
+
def get_list_items(
|
|
79
|
+
data, list_key, item_key, name_key="name", url_key="ghr-page"
|
|
80
|
+
):
|
|
81
|
+
items = []
|
|
82
|
+
list_data = data.get(list_key, {})
|
|
83
|
+
if isinstance(list_data, dict):
|
|
84
|
+
items = list_data.get(item_key, [])
|
|
85
|
+
if not isinstance(items, list):
|
|
86
|
+
items = [items]
|
|
87
|
+
for item in items:
|
|
88
|
+
if isinstance(item, dict):
|
|
89
|
+
name = item.get(name_key, "")
|
|
90
|
+
url = item.get(url_key, "")
|
|
91
|
+
items.append(f"{name} ({url})" if url else name)
|
|
92
|
+
return items
|
|
93
|
+
|
|
94
|
+
# Format response based on tool type
|
|
95
|
+
if tool_name == "MedlinePlus_search_topics_by_keyword":
|
|
96
|
+
# First print raw response for debugging
|
|
97
|
+
print("\n🔍 Raw response structure:")
|
|
98
|
+
print(
|
|
99
|
+
json.dumps(response, indent=2, ensure_ascii=False)[:2000] + "..."
|
|
100
|
+
if len(json.dumps(response, indent=2, ensure_ascii=False)) > 2000
|
|
101
|
+
else json.dumps(response, indent=2, ensure_ascii=False)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Extract topic information from XML structure
|
|
105
|
+
nlm_result = response.get("nlmSearchResult", {})
|
|
106
|
+
if not nlm_result:
|
|
107
|
+
return {"error": "nlmSearchResult node not found"}
|
|
108
|
+
|
|
109
|
+
# Get document list
|
|
110
|
+
document_list = nlm_result.get("list", {}).get("document", [])
|
|
111
|
+
if not document_list:
|
|
112
|
+
return {"error": "document list not found"}
|
|
113
|
+
|
|
114
|
+
# Ensure document_list is a list
|
|
115
|
+
if isinstance(document_list, dict):
|
|
116
|
+
document_list = [document_list]
|
|
117
|
+
|
|
118
|
+
formatted_topics = []
|
|
119
|
+
for doc in document_list:
|
|
120
|
+
# Get document basic info
|
|
121
|
+
doc_url = doc.get("@url", "")
|
|
122
|
+
doc_rank = doc.get("@rank", "")
|
|
123
|
+
|
|
124
|
+
# Get content node
|
|
125
|
+
content = doc.get("content", {})
|
|
126
|
+
if isinstance(content, dict):
|
|
127
|
+
health_topic = content.get("health-topic", {})
|
|
128
|
+
if health_topic:
|
|
129
|
+
# Extract health topic information
|
|
130
|
+
title = health_topic.get("@title", "")
|
|
131
|
+
meta_desc = health_topic.get("@meta-desc", "")
|
|
132
|
+
topic_url = health_topic.get("@url", doc_url)
|
|
133
|
+
language = health_topic.get("@language", "")
|
|
134
|
+
|
|
135
|
+
# Extract aliases
|
|
136
|
+
also_called = health_topic.get("also-called", [])
|
|
137
|
+
if isinstance(also_called, str):
|
|
138
|
+
also_called = [also_called]
|
|
139
|
+
elif isinstance(also_called, dict):
|
|
140
|
+
also_called = [also_called.get("#text", str(also_called))]
|
|
141
|
+
elif not isinstance(also_called, list):
|
|
142
|
+
also_called = []
|
|
143
|
+
|
|
144
|
+
# Extract summary
|
|
145
|
+
full_summary = health_topic.get("full-summary", "")
|
|
146
|
+
if isinstance(full_summary, dict):
|
|
147
|
+
full_summary = str(full_summary)
|
|
148
|
+
|
|
149
|
+
# Extract group information
|
|
150
|
+
groups = health_topic.get("group", [])
|
|
151
|
+
if isinstance(groups, str):
|
|
152
|
+
groups = [groups]
|
|
153
|
+
elif isinstance(groups, dict):
|
|
154
|
+
groups = [groups.get("#text", str(groups))]
|
|
155
|
+
elif not isinstance(groups, list):
|
|
156
|
+
groups = []
|
|
157
|
+
|
|
158
|
+
formatted_topics.append(
|
|
159
|
+
{
|
|
160
|
+
"title": title,
|
|
161
|
+
"meta_desc": meta_desc,
|
|
162
|
+
"url": topic_url,
|
|
163
|
+
"language": language,
|
|
164
|
+
"rank": doc_rank,
|
|
165
|
+
"also_called": also_called,
|
|
166
|
+
"summary": (
|
|
167
|
+
full_summary[:500] + "..."
|
|
168
|
+
if len(str(full_summary)) > 500
|
|
169
|
+
else full_summary
|
|
170
|
+
),
|
|
171
|
+
"groups": groups,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
{"topics": formatted_topics}
|
|
177
|
+
if formatted_topics
|
|
178
|
+
else {"error": "Failed to parse health topic information"}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
elif tool_name == "MedlinePlus_get_genetics_condition_by_name":
|
|
182
|
+
return {
|
|
183
|
+
"name": response.get("name", ""),
|
|
184
|
+
"description": get_text_content(response, "description"),
|
|
185
|
+
"genes": get_list_items(
|
|
186
|
+
response, "related-gene-list", "related-gene", "gene-symbol"
|
|
187
|
+
),
|
|
188
|
+
"synonyms": [
|
|
189
|
+
s.get("synonym", "") for s in response.get("synonym-list", [])
|
|
190
|
+
],
|
|
191
|
+
"ghr_page": response.get("ghr_page", ""),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
elif tool_name == "MedlinePlus_get_genetics_gene_by_name":
|
|
195
|
+
gene_summary = response.get("gene-summary", {})
|
|
196
|
+
return {
|
|
197
|
+
"name": gene_summary.get("name", ""),
|
|
198
|
+
"function": get_text_content(gene_summary, "function"),
|
|
199
|
+
"health_conditions": get_list_items(
|
|
200
|
+
gene_summary,
|
|
201
|
+
"related-health-condition-list",
|
|
202
|
+
"related-health-condition",
|
|
203
|
+
),
|
|
204
|
+
"synonyms": gene_summary.get("synonym-list", {}).get("synonym", []),
|
|
205
|
+
"ghr_page": gene_summary.get("ghr-page", ""),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
elif tool_name == "MedlinePlus_connect_lookup_by_code":
|
|
209
|
+
responses = response.get("knowledgeResponse", [])
|
|
210
|
+
return (
|
|
211
|
+
{
|
|
212
|
+
"responses": [
|
|
213
|
+
{
|
|
214
|
+
"title": r.get("title", ""),
|
|
215
|
+
"summary": r.get("summary", ""),
|
|
216
|
+
"url": r.get("url", ""),
|
|
217
|
+
}
|
|
218
|
+
for r in responses
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
if responses
|
|
222
|
+
else {"error": "No matching code information found"}
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
elif tool_name == "MedlinePlus_get_genetics_index":
|
|
226
|
+
topics = response.get("genetics_home_reference_topic_list", {}).get(
|
|
227
|
+
"topic", []
|
|
228
|
+
)
|
|
229
|
+
return (
|
|
230
|
+
{
|
|
231
|
+
"topics": [
|
|
232
|
+
{"name": t.get("name", ""), "url": t.get("url", "")}
|
|
233
|
+
for t in topics
|
|
234
|
+
]
|
|
235
|
+
}
|
|
236
|
+
if topics
|
|
237
|
+
else {"error": "No genetics topics found"}
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return {"raw_response": response}
|
|
241
|
+
|
|
242
|
+
def run(self, arguments: dict):
|
|
243
|
+
"""Execute tool call"""
|
|
244
|
+
# Validate required parameters
|
|
245
|
+
for key, prop in self.param_schema.items():
|
|
246
|
+
if prop.get("required", False) and key not in arguments:
|
|
247
|
+
return {"error": f"Parameter '{key}' is required."}
|
|
248
|
+
|
|
249
|
+
# Build URL
|
|
250
|
+
url = self._build_url(arguments)
|
|
251
|
+
if isinstance(url, dict) and "error" in url:
|
|
252
|
+
return url
|
|
253
|
+
|
|
254
|
+
# Print complete URL
|
|
255
|
+
print(f"\n🔗 Request URL: {url}")
|
|
256
|
+
|
|
257
|
+
# Make request
|
|
258
|
+
try:
|
|
259
|
+
resp = requests.get(url, timeout=self.timeout)
|
|
260
|
+
if resp.status_code != 200:
|
|
261
|
+
return {
|
|
262
|
+
"error": f"MedlinePlus returned non-200 status code: {resp.status_code}",
|
|
263
|
+
"detail": resp.text,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
print(f"\n📊 Response status: {resp.status_code}")
|
|
267
|
+
print(f"📏 Response length: {len(resp.text)} characters")
|
|
268
|
+
print(f"🔤 First 500 characters of response: {resp.text[:500]}...")
|
|
269
|
+
|
|
270
|
+
# Improved parsing logic
|
|
271
|
+
tool_name = self.tool_config["name"]
|
|
272
|
+
response_text = resp.text.strip()
|
|
273
|
+
|
|
274
|
+
# Decide parsing method based on tool type and content format
|
|
275
|
+
if url.endswith(".json") or (arguments.get("format") == "json"):
|
|
276
|
+
# JSON format
|
|
277
|
+
response = resp.json()
|
|
278
|
+
print("📋 Parsed as: JSON")
|
|
279
|
+
elif (
|
|
280
|
+
url.endswith(".xml")
|
|
281
|
+
or response_text.startswith("<?xml")
|
|
282
|
+
or (arguments.get("format") == "xml")
|
|
283
|
+
):
|
|
284
|
+
# XML format
|
|
285
|
+
response = xmltodict.parse(resp.text)
|
|
286
|
+
print("📋 Parsed as: XML -> Dictionary")
|
|
287
|
+
elif tool_name == "MedlinePlus_search_topics_by_keyword":
|
|
288
|
+
# Search tool defaults to XML
|
|
289
|
+
response = xmltodict.parse(resp.text)
|
|
290
|
+
print("📋 Parsed as: XML -> Dictionary (Search tool)")
|
|
291
|
+
elif tool_name == "MedlinePlus_get_genetics_index":
|
|
292
|
+
# Genetics index defaults to XML
|
|
293
|
+
response = xmltodict.parse(resp.text)
|
|
294
|
+
print("📋 Parsed as: XML -> Dictionary (Genetics index)")
|
|
295
|
+
else:
|
|
296
|
+
# Other cases keep original text
|
|
297
|
+
response = resp.text
|
|
298
|
+
print("📋 Parsed as: Plain text")
|
|
299
|
+
|
|
300
|
+
print(f"🔍 Parsed data type: {type(response)}")
|
|
301
|
+
if isinstance(response, dict):
|
|
302
|
+
print(f"🗝️ Top-level dictionary keys: {list(response.keys())}")
|
|
303
|
+
|
|
304
|
+
return self._format_response(response, tool_name)
|
|
305
|
+
|
|
306
|
+
except requests.RequestException as e:
|
|
307
|
+
return {"error": f"Failed to request MedlinePlus: {str(e)}"}
|
|
308
|
+
|
|
309
|
+
# Tool methods
|
|
310
|
+
def search_topics_by_keyword(
|
|
311
|
+
self, term: str, db: str, rettype: str = "brief"
|
|
312
|
+
) -> Dict[str, Any]:
|
|
313
|
+
return self.run({"term": term, "db": db, "rettype": rettype})
|
|
314
|
+
|
|
315
|
+
def connect_lookup_by_code(
|
|
316
|
+
self,
|
|
317
|
+
cs: str,
|
|
318
|
+
c: str,
|
|
319
|
+
dn: Optional[str] = None,
|
|
320
|
+
language: str = "en",
|
|
321
|
+
format: str = "json",
|
|
322
|
+
) -> Any:
|
|
323
|
+
args = {"cs": cs, "c": c, "language": language, "format": format}
|
|
324
|
+
if dn:
|
|
325
|
+
args["dn"] = dn
|
|
326
|
+
return self.run(args)
|
|
327
|
+
|
|
328
|
+
def get_genetics_condition_by_name(
|
|
329
|
+
self, condition: str, format: str = "json"
|
|
330
|
+
) -> Any:
|
|
331
|
+
return self.run({"condition": condition, "format": format})
|
|
332
|
+
|
|
333
|
+
def get_genetics_gene_by_name(self, gene: str, format: str = "json") -> Any:
|
|
334
|
+
return self.run({"gene": gene, "format": "json"})
|
|
335
|
+
|
|
336
|
+
def get_genetics_index(self) -> Any:
|
|
337
|
+
return self.run({})
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import urllib.parse
|
|
3
|
+
from .base_tool import BaseTool
|
|
4
|
+
from .tool_registry import register_tool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register_tool("OpenAlexTool")
|
|
8
|
+
class OpenAlexTool(BaseTool):
|
|
9
|
+
"""
|
|
10
|
+
Tool to retrieve literature from OpenAlex based on search keywords.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, tool_config):
|
|
14
|
+
super().__init__(tool_config)
|
|
15
|
+
self.base_url = "https://api.openalex.org/works"
|
|
16
|
+
|
|
17
|
+
def run(self, arguments):
|
|
18
|
+
"""Main entry point for the tool."""
|
|
19
|
+
search_keywords = arguments.get("search_keywords")
|
|
20
|
+
max_results = arguments.get("max_results", 10)
|
|
21
|
+
year_from = arguments.get("year_from", None)
|
|
22
|
+
year_to = arguments.get("year_to", None)
|
|
23
|
+
open_access = arguments.get("open_access", None)
|
|
24
|
+
|
|
25
|
+
return self.search_literature(
|
|
26
|
+
search_keywords, max_results, year_from, year_to, open_access
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def search_literature(
|
|
30
|
+
self,
|
|
31
|
+
search_keywords,
|
|
32
|
+
max_results=10,
|
|
33
|
+
year_from=None,
|
|
34
|
+
year_to=None,
|
|
35
|
+
open_access=None,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Search for literature using OpenAlex API.
|
|
39
|
+
|
|
40
|
+
Parameters:
|
|
41
|
+
search_keywords (str): Keywords to search for in title, abstract, and content.
|
|
42
|
+
max_results (int): Maximum number of results to return (default: 10).
|
|
43
|
+
year_from (int): Start year for publication date filter (optional).
|
|
44
|
+
year_to (int): End year for publication date filter (optional).
|
|
45
|
+
open_access (bool): Filter for open access papers only (optional).
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
list: List of dictionaries containing paper information.
|
|
49
|
+
"""
|
|
50
|
+
# Encode search keywords for URL
|
|
51
|
+
encoded_keywords = urllib.parse.quote(search_keywords)
|
|
52
|
+
|
|
53
|
+
# Build query parameters
|
|
54
|
+
params = {
|
|
55
|
+
"search": encoded_keywords,
|
|
56
|
+
"per-page": min(max_results, 200), # OpenAlex allows max 200 per page
|
|
57
|
+
"sort": "cited_by_count:desc", # Sort by citation count (most cited first)
|
|
58
|
+
"mailto": "support@openalex.org", # Polite pool access
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Add year filters if provided
|
|
62
|
+
filters = []
|
|
63
|
+
if year_from is not None and year_to is not None:
|
|
64
|
+
filters.append(f"publication_year:{year_from}-{year_to}")
|
|
65
|
+
elif year_from is not None:
|
|
66
|
+
filters.append(f"from_publication_date:{year_from}-01-01")
|
|
67
|
+
elif year_to is not None:
|
|
68
|
+
filters.append(f"to_publication_date:{year_to}-12-31")
|
|
69
|
+
|
|
70
|
+
# Add open access filter if specified
|
|
71
|
+
if open_access is True:
|
|
72
|
+
filters.append("is_oa:true")
|
|
73
|
+
elif open_access is False:
|
|
74
|
+
filters.append("is_oa:false")
|
|
75
|
+
|
|
76
|
+
if filters:
|
|
77
|
+
params["filter"] = ",".join(filters)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
response = requests.get(self.base_url, params=params)
|
|
81
|
+
response.raise_for_status()
|
|
82
|
+
data = response.json()
|
|
83
|
+
|
|
84
|
+
papers = []
|
|
85
|
+
for work in data.get("results", []):
|
|
86
|
+
paper_info = self._extract_paper_info(work)
|
|
87
|
+
papers.append(paper_info)
|
|
88
|
+
|
|
89
|
+
print(
|
|
90
|
+
f"[OpenAlex] Retrieved {len(papers)} papers for keywords: '{search_keywords}'"
|
|
91
|
+
)
|
|
92
|
+
return papers
|
|
93
|
+
|
|
94
|
+
except requests.exceptions.RequestException as e:
|
|
95
|
+
return f"Error retrieving data from OpenAlex: {e}"
|
|
96
|
+
|
|
97
|
+
def _extract_paper_info(self, work):
|
|
98
|
+
"""
|
|
99
|
+
Extract relevant information from a work object returned by OpenAlex API.
|
|
100
|
+
|
|
101
|
+
Parameters:
|
|
102
|
+
work (dict): Work object from OpenAlex API response.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
dict: Formatted paper information.
|
|
106
|
+
"""
|
|
107
|
+
# Extract title
|
|
108
|
+
title = work.get("title", "No title available")
|
|
109
|
+
|
|
110
|
+
# Extract abstract (display_name from abstract_inverted_index if available)
|
|
111
|
+
abstract = None
|
|
112
|
+
if work.get("abstract_inverted_index"):
|
|
113
|
+
# Reconstruct abstract from inverted index
|
|
114
|
+
abstract_dict = work["abstract_inverted_index"]
|
|
115
|
+
abstract_words = [""] * 500 # Assume max 500 words
|
|
116
|
+
for word, positions in abstract_dict.items():
|
|
117
|
+
for pos in positions:
|
|
118
|
+
if pos < len(abstract_words):
|
|
119
|
+
abstract_words[pos] = word
|
|
120
|
+
abstract = " ".join([word for word in abstract_words if word]).strip()
|
|
121
|
+
|
|
122
|
+
if not abstract:
|
|
123
|
+
abstract = "Abstract not available"
|
|
124
|
+
|
|
125
|
+
# Extract authors
|
|
126
|
+
authors = []
|
|
127
|
+
for authorship in work.get("authorships", []):
|
|
128
|
+
author = authorship.get("author", {})
|
|
129
|
+
author_name = author.get("display_name", "Unknown Author")
|
|
130
|
+
authors.append(author_name)
|
|
131
|
+
|
|
132
|
+
# Extract publication year
|
|
133
|
+
publication_year = work.get("publication_year", "Year not available")
|
|
134
|
+
|
|
135
|
+
# Extract organizations/affiliations
|
|
136
|
+
organizations = set()
|
|
137
|
+
for authorship in work.get("authorships", []):
|
|
138
|
+
for institution in authorship.get("institutions", []):
|
|
139
|
+
org_name = institution.get("display_name")
|
|
140
|
+
if org_name:
|
|
141
|
+
organizations.add(org_name)
|
|
142
|
+
|
|
143
|
+
# Extract additional useful information
|
|
144
|
+
venue = (
|
|
145
|
+
work.get("primary_location", {})
|
|
146
|
+
.get("source", {})
|
|
147
|
+
.get("display_name", "Unknown venue")
|
|
148
|
+
)
|
|
149
|
+
doi = work.get("doi", "No DOI")
|
|
150
|
+
citation_count = work.get("cited_by_count", 0)
|
|
151
|
+
open_access = work.get("open_access", {}).get("is_oa", False)
|
|
152
|
+
pdf_url = work.get("open_access", {}).get("oa_url")
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
"title": title,
|
|
156
|
+
"abstract": abstract,
|
|
157
|
+
"authors": authors,
|
|
158
|
+
"year": publication_year,
|
|
159
|
+
"organizations": list(organizations),
|
|
160
|
+
"venue": venue,
|
|
161
|
+
"doi": doi,
|
|
162
|
+
"citation_count": citation_count,
|
|
163
|
+
"open_access": open_access,
|
|
164
|
+
"pdf_url": pdf_url,
|
|
165
|
+
"openalex_id": work.get("id", ""),
|
|
166
|
+
"url": work.get("doi") if work.get("doi") else work.get("id", ""),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
def get_paper_by_doi(self, doi):
|
|
170
|
+
"""
|
|
171
|
+
Retrieve a specific paper by its DOI.
|
|
172
|
+
|
|
173
|
+
Parameters:
|
|
174
|
+
doi (str): DOI of the paper to retrieve.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
dict: Paper information or None if not found.
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
# OpenAlex supports DOI lookup directly
|
|
181
|
+
url = f"https://api.openalex.org/works/https://doi.org/{doi}"
|
|
182
|
+
params = {"mailto": "support@openalex.org"}
|
|
183
|
+
|
|
184
|
+
response = requests.get(url, params=params)
|
|
185
|
+
response.raise_for_status()
|
|
186
|
+
work = response.json()
|
|
187
|
+
|
|
188
|
+
return self._extract_paper_info(work)
|
|
189
|
+
|
|
190
|
+
except requests.exceptions.RequestException as e:
|
|
191
|
+
print(f"Error retrieving paper by DOI {doi}: {e}")
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
def get_papers_by_author(self, author_name, max_results=10):
|
|
195
|
+
"""
|
|
196
|
+
Retrieve papers by a specific author.
|
|
197
|
+
|
|
198
|
+
Parameters:
|
|
199
|
+
author_name (str): Name of the author to search for.
|
|
200
|
+
max_results (int): Maximum number of results to return.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
list: List of papers by the author.
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
params = {
|
|
207
|
+
"filter": f"author.display_name.search:{author_name}",
|
|
208
|
+
"per-page": min(max_results, 200),
|
|
209
|
+
"sort": "cited_by_count:desc",
|
|
210
|
+
"mailto": "support@openalex.org",
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
response = requests.get(self.base_url, params=params)
|
|
214
|
+
response.raise_for_status()
|
|
215
|
+
data = response.json()
|
|
216
|
+
|
|
217
|
+
papers = []
|
|
218
|
+
for work in data.get("results", []):
|
|
219
|
+
paper_info = self._extract_paper_info(work)
|
|
220
|
+
papers.append(paper_info)
|
|
221
|
+
|
|
222
|
+
print(
|
|
223
|
+
f"[OpenAlex] Retrieved {len(papers)} papers by author: '{author_name}'"
|
|
224
|
+
)
|
|
225
|
+
return papers
|
|
226
|
+
|
|
227
|
+
except requests.exceptions.RequestException as e:
|
|
228
|
+
return f"Error retrieving papers by author {author_name}: {e}"
|