tooluniverse 1.0.7__py3-none-any.whl → 1.0.8__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 (76) hide show
  1. tooluniverse/__init__.py +29 -14
  2. tooluniverse/admetai_tool.py +8 -4
  3. tooluniverse/base_tool.py +36 -0
  4. tooluniverse/biogrid_tool.py +118 -0
  5. tooluniverse/build_optimizer.py +87 -0
  6. tooluniverse/cache/__init__.py +3 -0
  7. tooluniverse/cache/memory_cache.py +99 -0
  8. tooluniverse/cache/result_cache_manager.py +235 -0
  9. tooluniverse/cache/sqlite_backend.py +257 -0
  10. tooluniverse/clinvar_tool.py +90 -0
  11. tooluniverse/custom_tool.py +28 -0
  12. tooluniverse/data/arxiv_tools.json +1 -4
  13. tooluniverse/data/core_tools.json +1 -4
  14. tooluniverse/data/dataset_tools.json +7 -7
  15. tooluniverse/data/doaj_tools.json +1 -3
  16. tooluniverse/data/drug_discovery_agents.json +292 -0
  17. tooluniverse/data/europe_pmc_tools.json +1 -2
  18. tooluniverse/data/genomics_tools.json +174 -0
  19. tooluniverse/data/geo_tools.json +86 -0
  20. tooluniverse/data/markitdown_tools.json +51 -0
  21. tooluniverse/data/openalex_tools.json +1 -5
  22. tooluniverse/data/pmc_tools.json +1 -4
  23. tooluniverse/data/ppi_tools.json +139 -0
  24. tooluniverse/data/pubmed_tools.json +1 -3
  25. tooluniverse/data/semantic_scholar_tools.json +1 -2
  26. tooluniverse/data/unified_guideline_tools.json +206 -4
  27. tooluniverse/data/xml_tools.json +15 -15
  28. tooluniverse/data/zenodo_tools.json +1 -2
  29. tooluniverse/dbsnp_tool.py +71 -0
  30. tooluniverse/default_config.py +6 -0
  31. tooluniverse/ensembl_tool.py +61 -0
  32. tooluniverse/execute_function.py +196 -75
  33. tooluniverse/generate_tools.py +303 -20
  34. tooluniverse/genomics_gene_search_tool.py +56 -0
  35. tooluniverse/geo_tool.py +116 -0
  36. tooluniverse/gnomad_tool.py +63 -0
  37. tooluniverse/markitdown_tool.py +159 -0
  38. tooluniverse/mcp_client_tool.py +10 -5
  39. tooluniverse/smcp.py +10 -9
  40. tooluniverse/string_tool.py +112 -0
  41. tooluniverse/tools/ADMETAnalyzerAgent.py +59 -0
  42. tooluniverse/tools/ArXiv_search_papers.py +3 -3
  43. tooluniverse/tools/CMA_Guidelines_Search.py +52 -0
  44. tooluniverse/tools/CORE_search_papers.py +3 -3
  45. tooluniverse/tools/ClinVar_search_variants.py +52 -0
  46. tooluniverse/tools/ClinicalTrialDesignAgent.py +63 -0
  47. tooluniverse/tools/CompoundDiscoveryAgent.py +59 -0
  48. tooluniverse/tools/DOAJ_search_articles.py +2 -2
  49. tooluniverse/tools/DiseaseAnalyzerAgent.py +52 -0
  50. tooluniverse/tools/DrugInteractionAnalyzerAgent.py +52 -0
  51. tooluniverse/tools/DrugOptimizationAgent.py +63 -0
  52. tooluniverse/tools/Ensembl_lookup_gene_by_symbol.py +52 -0
  53. tooluniverse/tools/EuropePMC_search_articles.py +1 -1
  54. tooluniverse/tools/GIN_Guidelines_Search.py +52 -0
  55. tooluniverse/tools/GWAS_search_associations_by_gene.py +52 -0
  56. tooluniverse/tools/LiteratureSynthesisAgent.py +59 -0
  57. tooluniverse/tools/PMC_search_papers.py +3 -3
  58. tooluniverse/tools/PubMed_search_articles.py +2 -2
  59. tooluniverse/tools/SemanticScholar_search_papers.py +1 -1
  60. tooluniverse/tools/UCSC_get_genes_by_region.py +67 -0
  61. tooluniverse/tools/Zenodo_search_records.py +1 -1
  62. tooluniverse/tools/__init__.py +33 -1
  63. tooluniverse/tools/convert_to_markdown.py +59 -0
  64. tooluniverse/tools/dbSNP_get_variant_by_rsid.py +46 -0
  65. tooluniverse/tools/gnomAD_query_variant.py +52 -0
  66. tooluniverse/tools/openalex_literature_search.py +4 -4
  67. tooluniverse/ucsc_tool.py +60 -0
  68. tooluniverse/unified_guideline_tools.py +1175 -57
  69. tooluniverse/utils.py +51 -4
  70. tooluniverse/zenodo_tool.py +2 -1
  71. {tooluniverse-1.0.7.dist-info → tooluniverse-1.0.8.dist-info}/METADATA +9 -3
  72. {tooluniverse-1.0.7.dist-info → tooluniverse-1.0.8.dist-info}/RECORD +76 -40
  73. {tooluniverse-1.0.7.dist-info → tooluniverse-1.0.8.dist-info}/WHEEL +0 -0
  74. {tooluniverse-1.0.7.dist-info → tooluniverse-1.0.8.dist-info}/entry_points.txt +0 -0
  75. {tooluniverse-1.0.7.dist-info → tooluniverse-1.0.8.dist-info}/licenses/LICENSE +0 -0
  76. {tooluniverse-1.0.7.dist-info → tooluniverse-1.0.8.dist-info}/top_level.txt +0 -0
tooluniverse/__init__.py CHANGED
@@ -1,39 +1,54 @@
1
1
  from importlib.metadata import version
2
+ import os
2
3
  import warnings
3
4
  from typing import Any, Optional, List
5
+
4
6
  from .execute_function import ToolUniverse
5
7
  from .base_tool import BaseTool
6
8
  from .default_config import default_tool_files
9
+ from .tool_registry import register_tool, get_tool_registry
10
+
11
+ _LIGHT_IMPORT = os.getenv("TOOLUNIVERSE_LIGHT_IMPORT", "false").lower() in (
12
+ "true",
13
+ "1",
14
+ "yes",
15
+ )
7
16
 
8
17
  # Version information - read from package metadata or pyproject.toml
9
18
  __version__ = version("tooluniverse")
10
- from .tool_registry import register_tool, get_tool_registry
11
19
 
12
20
  # Import tools with graceful fallback
13
- try:
14
- from . import tools
21
+ if not _LIGHT_IMPORT:
22
+ try:
23
+ from . import tools
15
24
 
16
- _TOOLS_AVAILABLE = True
17
- except ImportError:
25
+ _TOOLS_AVAILABLE = True
26
+ except ImportError:
27
+ _TOOLS_AVAILABLE = False
28
+ tools = None # type: ignore
29
+ else:
18
30
  _TOOLS_AVAILABLE = False
19
31
  tools = None # type: ignore
20
32
 
21
33
  # Check if lazy loading is enabled
22
- # LAZY_LOADING_ENABLED = os.getenv('TOOLUNIVERSE_LAZY_LOADING', 'true').lower() in ('true', '1', 'yes')
34
+ # LAZY_LOADING_ENABLED = os.getenv('TOOLUNIVERSE_LAZY_LOADING', 'true').lower() in (
35
+ # 'true', '1', 'yes'
36
+ # )
23
37
  LAZY_LOADING_ENABLED = (
24
38
  False # LAZY LOADING DISABLED BECAUSE IT'S STILL UNDER DEVELOPMENT
25
39
  )
26
40
 
27
41
  # Import MCP functionality
28
- try:
29
- from .mcp_integration import _patch_tooluniverse
42
+ if not _LIGHT_IMPORT:
43
+ try:
44
+ from .mcp_integration import _patch_tooluniverse
30
45
 
31
- # Automatically patch ToolUniverse with MCP methods
32
- _patch_tooluniverse()
46
+ # Automatically patch ToolUniverse with MCP methods
47
+ _patch_tooluniverse()
33
48
 
34
- except ImportError:
35
- # MCP functionality not available
36
- pass
49
+ except ImportError:
50
+ # MCP functionality not available
51
+ pass
37
52
 
38
53
  # Import SMCP with graceful fallback and consistent signatures for type checking
39
54
  try:
@@ -162,7 +177,7 @@ ComposeTool: Any
162
177
  CellosaurusSearchTool: Any
163
178
  CellosaurusQueryConverterTool: Any
164
179
  CellosaurusGetCellLineInfoTool: Any
165
- if not LAZY_LOADING_ENABLED:
180
+ if not _LIGHT_IMPORT and not LAZY_LOADING_ENABLED:
166
181
  # Import all tool classes immediately (old behavior) with warning suppression # noqa: E501
167
182
  with warnings.catch_warnings():
168
183
  warnings.filterwarnings("ignore", category=DeprecationWarning)
@@ -30,13 +30,15 @@ class ADMETAITool(BaseTool):
30
30
 
31
31
  def __init__(self, **kwargs):
32
32
  super().__init__(**kwargs)
33
+ # Initialize the model once during tool initialization
34
+ self.model = ADMETModel()
33
35
 
34
36
  def _predict(self, smiles: str) -> dict:
35
37
  """
36
38
  Gets ADMET predictions for the given smiles
37
39
  """
38
- model = ADMETModel()
39
- preds = model.predict(smiles=smiles)
40
+ # Reuse the pre-loaded model instead of creating a new one
41
+ preds = self.model.predict(smiles=smiles)
40
42
  return preds
41
43
 
42
44
  def run(self, arguments: dict) -> dict:
@@ -47,7 +49,8 @@ class ADMETAITool(BaseTool):
47
49
  smiles: The SMILES string(s) of the molecule(s).
48
50
 
49
51
  Returns:
50
- A dictionary mapping each SMILES string to a subdictionary of selected ADMET properties and their predicted values.
52
+ A dictionary mapping each SMILES string to a subdictionary of
53
+ selected ADMET properties and their predicted values.
51
54
  """
52
55
  smiles = arguments.get("smiles", [])
53
56
  if not smiles:
@@ -65,7 +68,8 @@ class ADMETAITool(BaseTool):
65
68
  ):
66
69
  return {"error": "No predictions could be extracted."}
67
70
 
68
- # Expand columns to include _drugbank_approved_percentile columns if present
71
+ # Expand columns to include _drugbank_approved_percentile columns
72
+ # if present
69
73
  if columns is not None:
70
74
  expanded_columns = []
71
75
  for col in columns:
tooluniverse/base_tool.py CHANGED
@@ -13,11 +13,15 @@ import json
13
13
  from pathlib import Path
14
14
  from typing import no_type_check, Optional, Dict, Any
15
15
  import hashlib
16
+ import inspect
16
17
 
17
18
 
18
19
  class BaseTool:
20
+ STATIC_CACHE_VERSION = "1"
21
+
19
22
  def __init__(self, tool_config):
20
23
  self.tool_config = self._apply_defaults(tool_config)
24
+ self._cached_version_hash: Optional[str] = None
21
25
 
22
26
  @classmethod
23
27
  def get_default_config_file(cls):
@@ -286,6 +290,38 @@ class BaseTool:
286
290
  """
287
291
  return self.tool_config.get("cacheable", True)
288
292
 
293
+ def get_cache_namespace(self) -> str:
294
+ """Return cache namespace identifier for this tool."""
295
+ return self.tool_config.get("name", self.__class__.__name__)
296
+
297
+ def get_cache_version(self) -> str:
298
+ """Return a stable cache version fingerprint for this tool."""
299
+ if self._cached_version_hash:
300
+ return self._cached_version_hash
301
+
302
+ hasher = hashlib.sha256()
303
+ hasher.update(self.STATIC_CACHE_VERSION.encode("utf-8"))
304
+
305
+ try:
306
+ source = inspect.getsource(self.__class__)
307
+ hasher.update(source.encode("utf-8"))
308
+ except (OSError, TypeError):
309
+ pass
310
+
311
+ try:
312
+ schema = json.dumps(self.tool_config.get("parameter", {}), sort_keys=True)
313
+ hasher.update(schema.encode("utf-8"))
314
+ except (TypeError, ValueError):
315
+ pass
316
+
317
+ self._cached_version_hash = hasher.hexdigest()[:16]
318
+ return self._cached_version_hash
319
+
320
+ def get_cache_ttl(self, result: Any = None) -> Optional[int]:
321
+ """Return TTL (seconds) for cached results; None means no expiration."""
322
+ ttl = self.tool_config.get("cache_ttl")
323
+ return int(ttl) if ttl is not None else None
324
+
289
325
  def get_tool_info(self) -> Dict[str, Any]:
290
326
  """
291
327
  Get comprehensive information about this tool.
@@ -0,0 +1,118 @@
1
+ """
2
+ BioGRID Database REST API Tool
3
+
4
+ This tool provides access to protein and genetic interaction data from the BioGRID database.
5
+ BioGRID is a comprehensive database of physical and genetic interactions.
6
+ """
7
+
8
+ import requests
9
+ from typing import Dict, Any, List
10
+ from .base_tool import BaseTool
11
+ from .tool_registry import register_tool
12
+
13
+ BIOGRID_BASE_URL = "https://webservice.thebiogrid.org"
14
+
15
+
16
+ @register_tool("BioGRIDRESTTool")
17
+ class BioGRIDRESTTool(BaseTool):
18
+ """
19
+ BioGRID Database REST API tool.
20
+ Generic wrapper for BioGRID API endpoints defined in ppi_tools.json.
21
+ """
22
+
23
+ def __init__(self, tool_config):
24
+ super().__init__(tool_config)
25
+ fields = tool_config.get("fields", {})
26
+ parameter = tool_config.get("parameter", {})
27
+
28
+ self.endpoint_template: str = fields.get("endpoint", "/interactions/")
29
+ self.required: List[str] = parameter.get("required", [])
30
+ self.output_format: str = fields.get("return_format", "JSON")
31
+
32
+ def _build_url(self, arguments: Dict[str, Any]) -> str | Dict[str, Any]:
33
+ """Build URL for BioGRID API request."""
34
+ url_path = self.endpoint_template
35
+ return BIOGRID_BASE_URL + url_path
36
+
37
+ def _build_params(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
38
+ """Build parameters for BioGRID API request."""
39
+ params = {"format": "json", "interSpeciesExcluded": "false"}
40
+
41
+ # Check for API key
42
+ api_key = arguments.get("api_key") or arguments.get("accesskey")
43
+ if not api_key:
44
+ # Try to get from environment variable
45
+ import os
46
+
47
+ api_key = os.getenv("BIOGRID_API_KEY")
48
+
49
+ if not api_key:
50
+ raise ValueError(
51
+ "BioGRID API key is required. Please provide 'api_key' parameter "
52
+ "or set BIOGRID_API_KEY environment variable. "
53
+ "Register at: https://webservice.thebiogrid.org/"
54
+ )
55
+
56
+ params["accesskey"] = api_key
57
+
58
+ # Map gene names to BioGRID format
59
+ if "gene_names" in arguments:
60
+ gene_names = arguments["gene_names"]
61
+ if isinstance(gene_names, list):
62
+ params["geneList"] = "|".join(gene_names)
63
+ else:
64
+ params["geneList"] = str(gene_names)
65
+
66
+ # Add other parameters
67
+ if "organism" in arguments:
68
+ # Convert organism name to taxonomy ID
69
+ organism = arguments["organism"]
70
+ if organism.lower() == "homo sapiens":
71
+ params["organism"] = 9606
72
+ elif organism.lower() == "mus musculus":
73
+ params["organism"] = 10090
74
+ else:
75
+ params["organism"] = organism
76
+
77
+ if "interaction_type" in arguments:
78
+ interaction_type = arguments["interaction_type"]
79
+ if interaction_type == "physical":
80
+ params["evidenceList"] = "physical"
81
+ elif interaction_type == "genetic":
82
+ params["evidenceList"] = "genetic"
83
+ # "both" means no evidence filter
84
+
85
+ if "limit" in arguments:
86
+ params["max"] = arguments["limit"]
87
+
88
+ return params
89
+
90
+ def _make_request(self, url: str, params: Dict[str, Any]) -> Dict[str, Any]:
91
+ """Perform a GET request and handle common errors."""
92
+ try:
93
+ response = requests.get(url, params=params, timeout=30)
94
+ response.raise_for_status()
95
+
96
+ if self.output_format == "JSON":
97
+ return response.json()
98
+ else:
99
+ return {"data": response.text}
100
+
101
+ except requests.exceptions.RequestException as e:
102
+ return {"error": f"Request failed: {str(e)}"}
103
+ except Exception as e:
104
+ return {"error": f"Unexpected error: {str(e)}"}
105
+
106
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
107
+ """Execute the tool with given arguments."""
108
+ # Validate required parameters
109
+ for param in self.required:
110
+ if param not in arguments:
111
+ return {"error": f"Missing required parameter: {param}"}
112
+
113
+ url = self._build_url(arguments)
114
+ if isinstance(url, dict) and "error" in url:
115
+ return url
116
+
117
+ params = self._build_params(arguments)
118
+ return self._make_request(url, params)
@@ -0,0 +1,87 @@
1
+ """Build optimization utilities for ToolUniverse tools."""
2
+
3
+ import json
4
+ import hashlib
5
+ from pathlib import Path
6
+ from typing import Dict, Any, Set, Tuple
7
+
8
+
9
+ def calculate_tool_hash(tool_config: Dict[str, Any]) -> str:
10
+ """Calculate a hash for tool configuration to detect changes."""
11
+ # Create a normalized version of the config for hashing
12
+ normalized_config = {}
13
+ for key, value in sorted(tool_config.items()):
14
+ if key not in ["timestamp", "last_updated", "created_at"]:
15
+ normalized_config[key] = value
16
+
17
+ config_str = json.dumps(normalized_config, sort_keys=True, separators=(",", ":"))
18
+ return hashlib.md5(config_str.encode("utf-8")).hexdigest()
19
+
20
+
21
+ def load_metadata(metadata_file: Path) -> Dict[str, str]:
22
+ """Load tool metadata from file."""
23
+ if not metadata_file.exists():
24
+ return {}
25
+
26
+ try:
27
+ with open(metadata_file, "r", encoding="utf-8") as f:
28
+ return json.load(f)
29
+ except (json.JSONDecodeError, IOError):
30
+ return {}
31
+
32
+
33
+ def save_metadata(metadata: Dict[str, str], metadata_file: Path) -> None:
34
+ """Save tool metadata to file."""
35
+ metadata_file.parent.mkdir(parents=True, exist_ok=True)
36
+ with open(metadata_file, "w", encoding="utf-8") as f:
37
+ json.dump(metadata, f, indent=2, sort_keys=True)
38
+
39
+
40
+ def cleanup_orphaned_files(tools_dir: Path, current_tool_names: Set[str]) -> int:
41
+ """Remove files for tools that no longer exist."""
42
+ if not tools_dir.exists():
43
+ return 0
44
+
45
+ cleaned_count = 0
46
+ keep_files = {"__init__", "_shared_client", "__pycache__"}
47
+
48
+ for file_path in tools_dir.iterdir():
49
+ if (
50
+ file_path.is_file()
51
+ and file_path.suffix == ".py"
52
+ and file_path.stem not in keep_files
53
+ and file_path.stem not in current_tool_names
54
+ ):
55
+ print(f"🗑️ Removing orphaned tool file: {file_path.name}")
56
+ file_path.unlink()
57
+ cleaned_count += 1
58
+
59
+ return cleaned_count
60
+
61
+
62
+ def get_changed_tools(
63
+ current_tools: Dict[str, Any], metadata_file: Path
64
+ ) -> Tuple[list, list, list]:
65
+ """Get lists of new, changed, and unchanged tools."""
66
+ old_metadata = load_metadata(metadata_file)
67
+ new_metadata = {}
68
+ new_tools = []
69
+ changed_tools = []
70
+ unchanged_tools = []
71
+
72
+ for tool_name, tool_config in current_tools.items():
73
+ current_hash = calculate_tool_hash(tool_config)
74
+ new_metadata[tool_name] = current_hash
75
+
76
+ old_hash = old_metadata.get(tool_name)
77
+ if old_hash is None:
78
+ new_tools.append(tool_name)
79
+ elif old_hash != current_hash:
80
+ changed_tools.append(tool_name)
81
+ else:
82
+ unchanged_tools.append(tool_name)
83
+
84
+ # Save updated metadata
85
+ save_metadata(new_metadata, metadata_file)
86
+
87
+ return new_tools, changed_tools, unchanged_tools
@@ -0,0 +1,3 @@
1
+ """Cache utilities for ToolUniverse."""
2
+
3
+ from .result_cache_manager import ResultCacheManager # noqa: F401
@@ -0,0 +1,99 @@
1
+ """
2
+ In-memory cache utilities for ToolUniverse.
3
+
4
+ Provides a lightweight, thread-safe LRU cache with optional singleflight
5
+ deduplication for expensive misses.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import threading
11
+ import time
12
+ from collections import OrderedDict
13
+ from contextlib import contextmanager
14
+ from typing import Any, Dict, Iterator, Optional, Tuple
15
+
16
+
17
+ class LRUCache:
18
+ """Thread-safe LRU cache with basic telemetry."""
19
+
20
+ def __init__(self, max_size: int = 128):
21
+ self.max_size = max(1, int(max_size))
22
+ self._data: "OrderedDict[str, Tuple[Any, float]]" = OrderedDict()
23
+ self._lock = threading.RLock()
24
+ self.hits = 0
25
+ self.misses = 0
26
+
27
+ def get(self, key: str) -> Optional[Any]:
28
+ with self._lock:
29
+ if key not in self._data:
30
+ self.misses += 1
31
+ return None
32
+
33
+ value, timestamp = self._data.pop(key)
34
+ self._data[key] = (value, timestamp)
35
+ self.hits += 1
36
+ return value
37
+
38
+ def set(self, key: str, value: Any):
39
+ with self._lock:
40
+ if key in self._data:
41
+ self._data.pop(key)
42
+ self._data[key] = (value, time.time())
43
+ self._evict_if_needed()
44
+
45
+ def delete(self, key: str):
46
+ with self._lock:
47
+ self._data.pop(key, None)
48
+
49
+ def clear(self):
50
+ with self._lock:
51
+ self._data.clear()
52
+ self.hits = 0
53
+ self.misses = 0
54
+
55
+ def _evict_if_needed(self):
56
+ while len(self._data) > self.max_size:
57
+ self._data.popitem(last=False)
58
+
59
+ def stats(self) -> Dict[str, Any]:
60
+ with self._lock:
61
+ return {
62
+ "max_size": self.max_size,
63
+ "current_size": len(self._data),
64
+ "hits": self.hits,
65
+ "misses": self.misses,
66
+ }
67
+
68
+ def __len__(self) -> int:
69
+ with self._lock:
70
+ return len(self._data)
71
+
72
+ def items(self) -> Iterator[Tuple[str, Any]]:
73
+ with self._lock:
74
+ for key, (value, _) in list(self._data.items()):
75
+ yield key, value
76
+
77
+
78
+ class SingleFlight:
79
+ """Per-key lock manager to collapse duplicate cache misses."""
80
+
81
+ def __init__(self):
82
+ self._locks: Dict[str, threading.Lock] = {}
83
+ self._global = threading.Lock()
84
+
85
+ @contextmanager
86
+ def acquire(self, key: str):
87
+ with self._global:
88
+ lock = self._locks.get(key)
89
+ if lock is None:
90
+ lock = threading.Lock()
91
+ self._locks[key] = lock
92
+ lock.acquire()
93
+ try:
94
+ yield
95
+ finally:
96
+ lock.release()
97
+ with self._global:
98
+ if not lock.locked():
99
+ self._locks.pop(key, None)