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,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})
|