tooluniverse 0.1.4__py3-none-any.whl → 1.0.0__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.

Files changed (187) hide show
  1. tooluniverse/__init__.py +340 -4
  2. tooluniverse/admetai_tool.py +84 -0
  3. tooluniverse/agentic_tool.py +563 -0
  4. tooluniverse/alphafold_tool.py +96 -0
  5. tooluniverse/base_tool.py +129 -6
  6. tooluniverse/boltz_tool.py +207 -0
  7. tooluniverse/chem_tool.py +192 -0
  8. tooluniverse/compose_scripts/__init__.py +1 -0
  9. tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
  10. tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
  11. tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
  12. tooluniverse/compose_scripts/literature_tool.py +34 -0
  13. tooluniverse/compose_scripts/output_summarizer.py +279 -0
  14. tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
  15. tooluniverse/compose_scripts/tool_discover.py +705 -0
  16. tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
  17. tooluniverse/compose_tool.py +371 -0
  18. tooluniverse/ctg_tool.py +1002 -0
  19. tooluniverse/custom_tool.py +81 -0
  20. tooluniverse/dailymed_tool.py +108 -0
  21. tooluniverse/data/admetai_tools.json +155 -0
  22. tooluniverse/data/agentic_tools.json +1156 -0
  23. tooluniverse/data/alphafold_tools.json +87 -0
  24. tooluniverse/data/boltz_tools.json +9 -0
  25. tooluniverse/data/chembl_tools.json +16 -0
  26. tooluniverse/data/clait_tools.json +108 -0
  27. tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
  28. tooluniverse/data/compose_tools.json +202 -0
  29. tooluniverse/data/dailymed_tools.json +70 -0
  30. tooluniverse/data/dataset_tools.json +646 -0
  31. tooluniverse/data/disease_target_score_tools.json +712 -0
  32. tooluniverse/data/efo_tools.json +17 -0
  33. tooluniverse/data/embedding_tools.json +319 -0
  34. tooluniverse/data/enrichr_tools.json +31 -0
  35. tooluniverse/data/europe_pmc_tools.json +22 -0
  36. tooluniverse/data/expert_feedback_tools.json +10 -0
  37. tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
  38. tooluniverse/data/fda_drug_labeling_tools.json +544 -168
  39. tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
  40. tooluniverse/data/finder_tools.json +209 -0
  41. tooluniverse/data/gene_ontology_tools.json +113 -0
  42. tooluniverse/data/gwas_tools.json +1082 -0
  43. tooluniverse/data/hpa_tools.json +333 -0
  44. tooluniverse/data/humanbase_tools.json +47 -0
  45. tooluniverse/data/idmap_tools.json +74 -0
  46. tooluniverse/data/mcp_client_tools_example.json +113 -0
  47. tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
  48. tooluniverse/data/medlineplus_tools.json +141 -0
  49. tooluniverse/data/monarch_tools.json +1 -1
  50. tooluniverse/data/openalex_tools.json +36 -0
  51. tooluniverse/data/opentarget_tools.json +82 -58
  52. tooluniverse/data/output_summarization_tools.json +101 -0
  53. tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
  54. tooluniverse/data/packages/categorized_tools.txt +206 -0
  55. tooluniverse/data/packages/cheminformatics_tools.json +347 -0
  56. tooluniverse/data/packages/earth_sciences_tools.json +74 -0
  57. tooluniverse/data/packages/genomics_tools.json +776 -0
  58. tooluniverse/data/packages/image_processing_tools.json +38 -0
  59. tooluniverse/data/packages/machine_learning_tools.json +789 -0
  60. tooluniverse/data/packages/neuroscience_tools.json +62 -0
  61. tooluniverse/data/packages/original_tools.txt +0 -0
  62. tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
  63. tooluniverse/data/packages/scientific_computing_tools.json +560 -0
  64. tooluniverse/data/packages/single_cell_tools.json +453 -0
  65. tooluniverse/data/packages/software_tools.json +4954 -0
  66. tooluniverse/data/packages/structural_biology_tools.json +396 -0
  67. tooluniverse/data/packages/visualization_tools.json +399 -0
  68. tooluniverse/data/pubchem_tools.json +215 -0
  69. tooluniverse/data/pubtator_tools.json +68 -0
  70. tooluniverse/data/rcsb_pdb_tools.json +1332 -0
  71. tooluniverse/data/reactome_tools.json +19 -0
  72. tooluniverse/data/semantic_scholar_tools.json +26 -0
  73. tooluniverse/data/special_tools.json +2 -25
  74. tooluniverse/data/tool_composition_tools.json +88 -0
  75. tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
  76. tooluniverse/data/txagent_client_tools.json +9 -0
  77. tooluniverse/data/uniprot_tools.json +211 -0
  78. tooluniverse/data/url_fetch_tools.json +94 -0
  79. tooluniverse/data/uspto_downloader_tools.json +9 -0
  80. tooluniverse/data/uspto_tools.json +811 -0
  81. tooluniverse/data/xml_tools.json +3275 -0
  82. tooluniverse/dataset_tool.py +296 -0
  83. tooluniverse/default_config.py +165 -0
  84. tooluniverse/efo_tool.py +42 -0
  85. tooluniverse/embedding_database.py +630 -0
  86. tooluniverse/embedding_sync.py +396 -0
  87. tooluniverse/enrichr_tool.py +266 -0
  88. tooluniverse/europe_pmc_tool.py +52 -0
  89. tooluniverse/execute_function.py +1775 -95
  90. tooluniverse/extended_hooks.py +444 -0
  91. tooluniverse/gene_ontology_tool.py +194 -0
  92. tooluniverse/graphql_tool.py +158 -36
  93. tooluniverse/gwas_tool.py +358 -0
  94. tooluniverse/hpa_tool.py +1645 -0
  95. tooluniverse/humanbase_tool.py +389 -0
  96. tooluniverse/logging_config.py +254 -0
  97. tooluniverse/mcp_client_tool.py +764 -0
  98. tooluniverse/mcp_integration.py +413 -0
  99. tooluniverse/mcp_tool_registry.py +925 -0
  100. tooluniverse/medlineplus_tool.py +337 -0
  101. tooluniverse/openalex_tool.py +228 -0
  102. tooluniverse/openfda_adv_tool.py +283 -0
  103. tooluniverse/openfda_tool.py +393 -160
  104. tooluniverse/output_hook.py +1122 -0
  105. tooluniverse/package_tool.py +195 -0
  106. tooluniverse/pubchem_tool.py +158 -0
  107. tooluniverse/pubtator_tool.py +168 -0
  108. tooluniverse/rcsb_pdb_tool.py +38 -0
  109. tooluniverse/reactome_tool.py +108 -0
  110. tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
  111. tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
  112. tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
  113. tooluniverse/remote/expert_feedback/simple_test.py +23 -0
  114. tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
  115. tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
  116. tooluniverse/remote/expert_feedback_mcp/human_expert_mcp_server.py +1611 -0
  117. tooluniverse/remote/expert_feedback_mcp/simple_test.py +34 -0
  118. tooluniverse/remote/expert_feedback_mcp/start_web_interface.py +91 -0
  119. tooluniverse/remote/immune_compass/compass_tool.py +327 -0
  120. tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
  121. tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
  122. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
  123. tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
  124. tooluniverse/remote_tool.py +99 -0
  125. tooluniverse/restful_tool.py +53 -30
  126. tooluniverse/scripts/generate_tool_graph.py +408 -0
  127. tooluniverse/scripts/visualize_tool_graph.py +829 -0
  128. tooluniverse/semantic_scholar_tool.py +62 -0
  129. tooluniverse/smcp.py +2452 -0
  130. tooluniverse/smcp_server.py +975 -0
  131. tooluniverse/test/mcp_server_test.py +0 -0
  132. tooluniverse/test/test_admetai_tool.py +370 -0
  133. tooluniverse/test/test_agentic_tool.py +129 -0
  134. tooluniverse/test/test_alphafold_tool.py +71 -0
  135. tooluniverse/test/test_chem_tool.py +37 -0
  136. tooluniverse/test/test_compose_lieraturereview.py +63 -0
  137. tooluniverse/test/test_compose_tool.py +448 -0
  138. tooluniverse/test/test_dailymed.py +69 -0
  139. tooluniverse/test/test_dataset_tool.py +200 -0
  140. tooluniverse/test/test_disease_target_score.py +56 -0
  141. tooluniverse/test/test_drugbank_filter_examples.py +179 -0
  142. tooluniverse/test/test_efo.py +31 -0
  143. tooluniverse/test/test_enrichr_tool.py +21 -0
  144. tooluniverse/test/test_europe_pmc_tool.py +20 -0
  145. tooluniverse/test/test_fda_adv.py +95 -0
  146. tooluniverse/test/test_fda_drug_labeling.py +91 -0
  147. tooluniverse/test/test_gene_ontology_tools.py +66 -0
  148. tooluniverse/test/test_gwas_tool.py +139 -0
  149. tooluniverse/test/test_hpa.py +625 -0
  150. tooluniverse/test/test_humanbase_tool.py +20 -0
  151. tooluniverse/test/test_idmap_tools.py +61 -0
  152. tooluniverse/test/test_mcp_server.py +211 -0
  153. tooluniverse/test/test_mcp_tool.py +247 -0
  154. tooluniverse/test/test_medlineplus.py +220 -0
  155. tooluniverse/test/test_openalex_tool.py +32 -0
  156. tooluniverse/test/test_opentargets.py +28 -0
  157. tooluniverse/test/test_pubchem_tool.py +116 -0
  158. tooluniverse/test/test_pubtator_tool.py +37 -0
  159. tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
  160. tooluniverse/test/test_reactome.py +54 -0
  161. tooluniverse/test/test_semantic_scholar_tool.py +24 -0
  162. tooluniverse/test/test_software_tools.py +147 -0
  163. tooluniverse/test/test_tool_description_optimizer.py +49 -0
  164. tooluniverse/test/test_tool_finder.py +26 -0
  165. tooluniverse/test/test_tool_finder_llm.py +252 -0
  166. tooluniverse/test/test_tools_find.py +195 -0
  167. tooluniverse/test/test_uniprot_tools.py +74 -0
  168. tooluniverse/test/test_uspto_tool.py +72 -0
  169. tooluniverse/test/test_xml_tool.py +113 -0
  170. tooluniverse/tool_finder_embedding.py +267 -0
  171. tooluniverse/tool_finder_keyword.py +693 -0
  172. tooluniverse/tool_finder_llm.py +699 -0
  173. tooluniverse/tool_graph_web_ui.py +955 -0
  174. tooluniverse/tool_registry.py +416 -0
  175. tooluniverse/uniprot_tool.py +155 -0
  176. tooluniverse/url_tool.py +253 -0
  177. tooluniverse/uspto_tool.py +240 -0
  178. tooluniverse/utils.py +369 -41
  179. tooluniverse/xml_tool.py +369 -0
  180. tooluniverse-1.0.0.dist-info/METADATA +377 -0
  181. tooluniverse-1.0.0.dist-info/RECORD +186 -0
  182. {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.dist-info}/WHEEL +1 -1
  183. tooluniverse-1.0.0.dist-info/entry_points.txt +9 -0
  184. tooluniverse-0.1.4.dist-info/METADATA +0 -141
  185. tooluniverse-0.1.4.dist-info/RECORD +0 -18
  186. {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.dist-info}/licenses/LICENSE +0 -0
  187. {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.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}"