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,416 @@
1
+ """Simplified tool registry for automatic tool discovery and registration."""
2
+
3
+ import importlib
4
+ import pkgutil
5
+ import os
6
+ import json
7
+ import glob
8
+ import logging
9
+ from typing import Dict
10
+
11
+ # Initialize logger for this module
12
+ logger = logging.getLogger("ToolRegistry")
13
+
14
+ # Global registries
15
+ _tool_registry = {}
16
+ _config_registry = {}
17
+ _lazy_registry: Dict[str, str] = {} # Maps tool names to module names
18
+ _discovery_completed = False
19
+ _lazy_cache = {}
20
+
21
+
22
+ def register_tool(tool_type_name=None, config=None):
23
+ """
24
+ Decorator to automatically register tool classes and their configs.
25
+
26
+ Usage:
27
+ @register_tool('CustomToolName', config={...})
28
+ class MyTool:
29
+ pass
30
+ """
31
+
32
+ def decorator(cls):
33
+ name = tool_type_name or cls.__name__
34
+ _tool_registry[name] = cls
35
+
36
+ if config:
37
+ _config_registry[name] = config
38
+ logger.info(f"Registered tool with config: {name}")
39
+ else:
40
+ logger.debug(f"Registered tool: {name} -> {cls.__name__}")
41
+
42
+ return cls
43
+
44
+ return decorator
45
+
46
+
47
+ def register_external_tool(tool_name, tool_class):
48
+ """Allow external registration of tool classes."""
49
+ _tool_registry[tool_name] = tool_class
50
+ logger.info(f"Externally registered tool: {tool_name}")
51
+
52
+
53
+ def register_config(tool_type_name, config):
54
+ """Register a config for a tool type."""
55
+ _config_registry[tool_type_name] = config
56
+ logger.info(f"Registered config for: {tool_type_name}")
57
+
58
+
59
+ def get_tool_registry():
60
+ """Get a copy of the current tool registry."""
61
+ return _tool_registry.copy()
62
+
63
+
64
+ def get_config_registry():
65
+ """Get a copy of the current config registry."""
66
+ return _config_registry.copy()
67
+
68
+
69
+ def lazy_import_tool(tool_name):
70
+ """
71
+ Lazily import a tool by name without importing all tool modules.
72
+ Only imports the specific module containing the requested tool.
73
+ """
74
+ global _tool_registry, _lazy_registry, _lazy_cache
75
+
76
+ # If tool is already in registry, return it
77
+ if tool_name in _tool_registry:
78
+ return _tool_registry[tool_name]
79
+
80
+ # If we have a lazy mapping for this tool, import its module
81
+ if tool_name in _lazy_registry:
82
+ module_name = _lazy_registry[tool_name]
83
+
84
+ # Ensure we have the full module path
85
+ if not module_name.startswith("tooluniverse."):
86
+ full_module_name = f"tooluniverse.{module_name}"
87
+ else:
88
+ full_module_name = module_name
89
+
90
+ # Only import if we haven't cached this module yet
91
+ if full_module_name not in _lazy_cache:
92
+ try:
93
+ logger.debug(
94
+ f"Lazy importing module: {full_module_name} for tool: {tool_name}"
95
+ )
96
+ module = importlib.import_module(full_module_name)
97
+ _lazy_cache[full_module_name] = module
98
+ logger.debug(f"Successfully imported module: {full_module_name}")
99
+
100
+ # Check if the tool is now in the registry
101
+ if tool_name in _tool_registry:
102
+ logger.debug(f"Successfully lazy-loaded tool: {tool_name}")
103
+ return _tool_registry[tool_name]
104
+ else:
105
+ logger.warning(
106
+ f"Tool {tool_name} not found in module {full_module_name} after import"
107
+ )
108
+
109
+ except ImportError as e:
110
+ logger.warning(f"Failed to lazy import {full_module_name}: {e}")
111
+ # Remove this bad mapping so we don't try again
112
+ del _lazy_registry[tool_name]
113
+ else:
114
+ # Module was already imported, check if tool is now available
115
+ if tool_name in _tool_registry:
116
+ return _tool_registry[tool_name]
117
+ else:
118
+ logger.warning(
119
+ f"Tool {tool_name} not found in already imported module {full_module_name}"
120
+ )
121
+
122
+ # If still not found after lazy loading attempt, return None
123
+ # Don't fall back to full discovery as that defeats the purpose of lazy loading
124
+ logger.debug(f"Tool {tool_name} not found in lazy registry")
125
+ return None
126
+
127
+
128
+ def build_lazy_registry(package_name=None):
129
+ """
130
+ Build a mapping of tool names to module names using config files and naming patterns.
131
+ This is truly lazy - it doesn't import any modules, just creates the mapping.
132
+ """
133
+ global _lazy_registry
134
+
135
+ if package_name is None:
136
+ package_name = "tooluniverse"
137
+
138
+ try:
139
+ package = importlib.import_module(package_name)
140
+ package_path = package.__path__
141
+ except (ImportError, AttributeError):
142
+ logger.warning(f"Could not import package {package_name}")
143
+ return {}
144
+
145
+ logger.debug(f"Building lazy registry for package: {package_name}")
146
+
147
+ # Strategy 1: Parse config files for accurate mappings WITHOUT importing modules
148
+ config_mappings = _discover_from_configs()
149
+ config_count = 0
150
+
151
+ for module_name, tool_classes in config_mappings.items():
152
+ # Don't verify module exists by importing - just trust the mapping
153
+ # The actual import will happen when the tool is first requested
154
+ for tool_class in tool_classes:
155
+ if tool_class not in _lazy_registry:
156
+ _lazy_registry[tool_class] = module_name
157
+ config_count += len(tool_classes)
158
+
159
+ # Strategy 2: Pattern-based fallback for modules without configs
160
+ pattern_count = 0
161
+ for _importer, modname, _ispkg in pkgutil.iter_modules(package_path):
162
+ if "_tool" in modname and modname not in [m for m in config_mappings.keys()]:
163
+ # Simple pattern: module_tool -> ModuleTool, ModuleRESTTool
164
+ base_name = modname.replace("_tool", "").replace("_", "")
165
+ potential_names = [
166
+ f"{base_name.title()}Tool",
167
+ f"{base_name.title()}RESTTool",
168
+ f"{base_name.upper()}Tool",
169
+ ]
170
+
171
+ for tool_name in potential_names:
172
+ if tool_name not in _lazy_registry:
173
+ _lazy_registry[tool_name] = modname
174
+ pattern_count += 1
175
+
176
+ logger.info(
177
+ f"Built lazy registry: {config_count} from configs, {pattern_count} from patterns (no modules imported)"
178
+ )
179
+ return _lazy_registry.copy()
180
+
181
+
182
+ def _discover_from_configs():
183
+ """
184
+ Fully dynamic config file discovery - no hardcoded mappings.
185
+ Automatically discovers config-to-module mappings by:
186
+ 1. Finding all JSON config files
187
+ 2. Finding all Python tool modules
188
+ 3. Smart matching between config names and module names
189
+ """
190
+ # Get the data directory path relative to tooluniverse module
191
+ try:
192
+ import tooluniverse
193
+
194
+ package_dir = os.path.dirname(tooluniverse.__file__)
195
+ data_dir = os.path.join(package_dir, "data")
196
+ except ImportError:
197
+ # Fallback: assume we're in the right directory structure
198
+ current_dir = os.path.dirname(os.path.abspath(__file__))
199
+ data_dir = os.path.join(current_dir, "data")
200
+
201
+ if not os.path.exists(data_dir):
202
+ logger.warning(f"Data directory not found: {data_dir}")
203
+ return {}
204
+
205
+ # Step 1: Get all available tool modules
206
+ available_modules = _get_available_tool_modules()
207
+ logger.debug(f"Found {len(available_modules)} tool modules: {available_modules}")
208
+
209
+ tool_mapping = {}
210
+
211
+ try:
212
+ for json_file in glob.glob(os.path.join(data_dir, "*.json")):
213
+ try:
214
+ config_name = os.path.basename(json_file).replace(".json", "")
215
+
216
+ # Step 2: Smart matching to find the best module for this config
217
+ module_name = _smart_match_config_to_module(
218
+ config_name, available_modules
219
+ )
220
+
221
+ if not module_name:
222
+ logger.debug(f"No module match found for config: {config_name}")
223
+ continue
224
+
225
+ # Step 3: Extract tool types from config
226
+ with open(json_file, "r", encoding="utf-8") as f:
227
+ config_data = json.load(f)
228
+
229
+ tool_types = set()
230
+ if isinstance(config_data, list):
231
+ for tool_config in config_data:
232
+ if isinstance(tool_config, dict) and "type" in tool_config:
233
+ tool_types.add(tool_config["type"])
234
+ elif isinstance(config_data, dict) and "type" in config_data:
235
+ tool_types.add(config_data["type"])
236
+
237
+ if tool_types and module_name:
238
+ if module_name not in tool_mapping:
239
+ tool_mapping[module_name] = []
240
+ tool_mapping[module_name].extend(list(tool_types))
241
+ logger.debug(
242
+ f"Dynamic mapping: {config_name} -> {module_name} -> {tool_types}"
243
+ )
244
+
245
+ except Exception as e:
246
+ logger.debug(f"Skipped config file {json_file}: {e}")
247
+ continue
248
+ except Exception as e:
249
+ logger.warning(f"Error reading config files: {e}")
250
+
251
+ logger.debug(f"Dynamically discovered {len(tool_mapping)} modules from configs")
252
+ return tool_mapping
253
+
254
+
255
+ def _get_available_tool_modules():
256
+ """
257
+ Get all available tool modules by scanning the tooluniverse package.
258
+ """
259
+ try:
260
+ import tooluniverse
261
+
262
+ package_path = tooluniverse.__path__
263
+ except ImportError:
264
+ logger.warning("Cannot import tooluniverse package")
265
+ return []
266
+
267
+ modules = []
268
+ for _importer, modname, _ispkg in pkgutil.iter_modules(package_path):
269
+ if "_tool" in modname or modname in [
270
+ "compose_tool",
271
+ "agentic_tool",
272
+ ]: # Include compose_tool and agentic_tool
273
+ modules.append(modname)
274
+
275
+ return modules
276
+
277
+
278
+ def _smart_match_config_to_module(config_name, available_modules):
279
+ """
280
+ Smart matching algorithm to find the best module for a config file.
281
+ Uses multiple strategies in order of preference.
282
+ """
283
+ # Strategy 1: Direct name matching
284
+ # "chembl_tools" -> "chem_tool"
285
+ if config_name.endswith("_tools"):
286
+ candidate = config_name.replace("_tools", "_tool")
287
+ if candidate in available_modules:
288
+ return candidate
289
+
290
+ # Strategy 2: Exact match
291
+ # "chem_tool" -> "chem_tool"
292
+ if config_name in available_modules:
293
+ return config_name
294
+
295
+ # Strategy 3: Fuzzy matching based on keywords
296
+ # Extract key parts from config name and match with modules
297
+ config_parts = set(config_name.replace("_", " ").split())
298
+
299
+ best_match = None
300
+ best_score = 0
301
+
302
+ for module in available_modules:
303
+ module_parts = set(module.replace("_", " ").split())
304
+
305
+ # Calculate similarity score
306
+ common_parts = config_parts & module_parts
307
+ if common_parts:
308
+ score = len(common_parts) / max(len(config_parts), len(module_parts))
309
+ if score > best_score:
310
+ best_score = score
311
+ best_match = module
312
+
313
+ # Only return match if score is reasonably high
314
+ if best_score > 0.3: # At least 30% similarity
315
+ return best_match
316
+
317
+ # Strategy 4: Pattern-based matching for known patterns
318
+ patterns = [
319
+ # FDA patterns
320
+ ("fda", "openfda_tool"),
321
+ ("clinicaltrials", "ctg_tool"),
322
+ ("clinical_trials", "ctg_tool"),
323
+ ("opentargets", "graphql_tool"),
324
+ ("monarch", "restful_tool"),
325
+ ("url_fetch", "url_tool"),
326
+ ("europe_pmc", "europe_pmc_tool"),
327
+ ("semantic_scholar", "semantic_scholar_tool"),
328
+ # ChEMBL pattern
329
+ ("chembl", "chem_tool"),
330
+ ]
331
+
332
+ for pattern, module in patterns:
333
+ if pattern in config_name and module in available_modules:
334
+ return module
335
+
336
+ return None
337
+
338
+
339
+ def auto_discover_tools(package_name=None, lazy=True):
340
+ """
341
+ Automatically discover and import all tool modules.
342
+ If lazy=True, only builds the mapping without importing any modules.
343
+ If lazy=False, imports all tool modules immediately.
344
+ """
345
+ global _discovery_completed
346
+
347
+ if package_name is None:
348
+ package_name = "tooluniverse"
349
+
350
+ # In lazy mode, just build the registry without importing anything
351
+ if lazy:
352
+ if not _lazy_registry:
353
+ build_lazy_registry(package_name)
354
+ logger.debug(
355
+ f"Lazy discovery complete. Registry contains {len(_lazy_registry)} tool mappings (no modules imported)"
356
+ )
357
+ return _tool_registry.copy()
358
+
359
+ # Return cached registry if full discovery already done
360
+ if _discovery_completed:
361
+ return _tool_registry.copy()
362
+
363
+ try:
364
+ package = importlib.import_module(package_name)
365
+ package_path = package.__path__
366
+ except (ImportError, AttributeError):
367
+ logger.warning(f"Could not import package {package_name}")
368
+ return _tool_registry.copy()
369
+
370
+ logger.info(
371
+ f"Auto-discovering tools in package: {package_name} (lazy={lazy}) - importing ALL modules"
372
+ )
373
+
374
+ # Import all tool modules (non-lazy mode)
375
+ imported_count = 0
376
+ for _importer, modname, _ispkg in pkgutil.iter_modules(package_path):
377
+ if "_tool" in modname or modname in ["compose_tool", "agentic_tool"]:
378
+ try:
379
+ importlib.import_module(f"{package_name}.{modname}")
380
+ logger.debug(f"Imported tool module: {modname}")
381
+ imported_count += 1
382
+ except ImportError as e:
383
+ logger.warning(f"Could not import {modname}: {e}")
384
+
385
+ _discovery_completed = True
386
+ logger.info(
387
+ f"Full discovery complete. Imported {imported_count} modules, registered {len(_tool_registry)} tools"
388
+ )
389
+ return _tool_registry.copy()
390
+
391
+
392
+ def get_tool_class_lazy(tool_name):
393
+ """
394
+ Get a tool class by name, using lazy loading if possible.
395
+ Only imports the specific module needed, not all modules.
396
+ """
397
+ # First try lazy import
398
+ tool_class = lazy_import_tool(tool_name)
399
+ if tool_class:
400
+ return tool_class
401
+
402
+ # If lazy loading fails and we haven't done full discovery yet,
403
+ # check if the tool exists in the current registry
404
+ if tool_name in _tool_registry:
405
+ return _tool_registry[tool_name]
406
+
407
+ # As a last resort, if full discovery hasn't been done, do it
408
+ # But this should be rare with a properly configured lazy registry
409
+ if not _discovery_completed:
410
+ logger.warning(
411
+ f"Tool {tool_name} not found in lazy registry, falling back to full discovery"
412
+ )
413
+ auto_discover_tools(lazy=False)
414
+ return _tool_registry.get(tool_name)
415
+
416
+ return None
@@ -0,0 +1,155 @@
1
+ import requests
2
+ from typing import Any, Dict
3
+ from .base_tool import BaseTool
4
+ from .tool_registry import register_tool
5
+
6
+
7
+ @register_tool("UniProtRESTTool")
8
+ class UniProtRESTTool(BaseTool):
9
+ def __init__(self, tool_config: Dict):
10
+ super().__init__(tool_config)
11
+ self.endpoint = tool_config["fields"]["endpoint"]
12
+ self.extract_path = tool_config["fields"].get("extract_path")
13
+ self.timeout = 15 # Increase timeout for large entries
14
+
15
+ def _build_url(self, args: Dict[str, Any]) -> str:
16
+ url = self.endpoint
17
+ for k, v in args.items():
18
+ url = url.replace(f"{{{k}}}", str(v))
19
+ return url
20
+
21
+ def _extract_data(self, data: Dict, extract_path: str) -> Any:
22
+ """Custom data extraction with support for filtering"""
23
+
24
+ # Handle specific UniProt extraction patterns
25
+ if extract_path == "comments[?(@.commentType=='FUNCTION')].texts[*].value":
26
+ # Extract function comments
27
+ result = []
28
+ for comment in data.get("comments", []):
29
+ if comment.get("commentType") == "FUNCTION":
30
+ for text in comment.get("texts", []):
31
+ if "value" in text:
32
+ result.append(text["value"])
33
+ return result
34
+
35
+ elif (
36
+ extract_path
37
+ == "comments[?(@.commentType=='SUBCELLULAR LOCATION')].subcellularLocations[*].location.value"
38
+ ):
39
+ # Extract subcellular locations
40
+ result = []
41
+ for comment in data.get("comments", []):
42
+ if comment.get("commentType") == "SUBCELLULAR LOCATION":
43
+ for location in comment.get("subcellularLocations", []):
44
+ if "location" in location and "value" in location["location"]:
45
+ result.append(location["location"]["value"])
46
+ return result
47
+
48
+ elif extract_path == "features[?(@.type=='VARIANT')]":
49
+ # Extract variant features (correct type is "Natural variant")
50
+ result = []
51
+ for feature in data.get("features", []):
52
+ if feature.get("type") == "Natural variant":
53
+ result.append(feature)
54
+ return result
55
+
56
+ elif (
57
+ extract_path
58
+ == "features[?(@.type=='MODIFIED RESIDUE' || @.type=='SIGNAL')]"
59
+ ):
60
+ # Extract PTM and signal features (correct types are "Modified residue" and "Signal")
61
+ result = []
62
+ for feature in data.get("features", []):
63
+ if feature.get("type") in ["Modified residue", "Signal"]:
64
+ result.append(feature)
65
+ return result
66
+
67
+ elif (
68
+ extract_path
69
+ == "comments[?(@.commentType=='ALTERNATIVE PRODUCTS')].isoforms[*].isoformIds[*]"
70
+ ):
71
+ # Extract isoform IDs
72
+ result = []
73
+ for comment in data.get("comments", []):
74
+ if comment.get("commentType") == "ALTERNATIVE PRODUCTS":
75
+ for isoform in comment.get("isoforms", []):
76
+ for isoform_id in isoform.get("isoformIds", []):
77
+ result.append(isoform_id)
78
+ return result
79
+
80
+ # For simple paths, use jsonpath_ng
81
+ try:
82
+ from jsonpath_ng import parse
83
+
84
+ expr = parse(extract_path)
85
+ matches = expr.find(data)
86
+ extracted_data = [m.value for m in matches]
87
+
88
+ # Return single item if only one match, otherwise return list
89
+ if len(extracted_data) == 0:
90
+ return {"error": f"No data found for JSONPath: {extract_path}"}
91
+ elif len(extracted_data) == 1:
92
+ return extracted_data[0]
93
+ else:
94
+ return extracted_data
95
+
96
+ except ImportError:
97
+ return {"error": "jsonpath_ng library is required for data extraction"}
98
+ except Exception as e:
99
+ return {
100
+ "error": f"Failed to extract UniProt fields using JSONPath '{extract_path}': {e}"
101
+ }
102
+
103
+ def run(self, arguments: Dict[str, Any]) -> Any:
104
+ # Build URL
105
+ url = self._build_url(arguments)
106
+ try:
107
+ resp = requests.get(url, timeout=self.timeout)
108
+ if resp.status_code != 200:
109
+ return {
110
+ "error": f"UniProt API returned status code: {resp.status_code}",
111
+ "detail": resp.text,
112
+ }
113
+ data = resp.json()
114
+ except requests.exceptions.Timeout:
115
+ return {"error": "Request to UniProt API timed out"}
116
+ except requests.exceptions.RequestException as e:
117
+ return {"error": f"Request to UniProt API failed: {e}"}
118
+ except ValueError as e:
119
+ return {"error": f"Failed to parse JSON response: {e}"}
120
+
121
+ # If extract_path is configured, extract the corresponding subset
122
+ if self.extract_path:
123
+ result = self._extract_data(data, self.extract_path)
124
+
125
+ # Handle empty results
126
+ if isinstance(result, list) and len(result) == 0:
127
+ return {"error": f"No data found for path: {self.extract_path}"}
128
+ elif isinstance(result, dict) and "error" in result:
129
+ return result
130
+
131
+ return result
132
+
133
+ return data
134
+
135
+ # Method bindings for backward compatibility
136
+ def get_entry_by_accession(self, accession: str) -> Any:
137
+ return self.run({"accession": accession})
138
+
139
+ def get_function_by_accession(self, accession: str) -> Any:
140
+ return self.run({"accession": accession})
141
+
142
+ def get_names_taxonomy_by_accession(self, accession: str) -> Any:
143
+ return self.run({"accession": accession})
144
+
145
+ def get_subcellular_location_by_accession(self, accession: str) -> Any:
146
+ return self.run({"accession": accession})
147
+
148
+ def get_disease_variants_by_accession(self, accession: str) -> Any:
149
+ return self.run({"accession": accession})
150
+
151
+ def get_ptm_processing_by_accession(self, accession: str) -> Any:
152
+ return self.run({"accession": accession})
153
+
154
+ def get_sequence_isoforms_by_accession(self, accession: str) -> Any:
155
+ return self.run({"accession": accession})