tooluniverse 1.0.7__py3-none-any.whl → 1.0.9__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 (96) hide show
  1. tooluniverse/__init__.py +37 -14
  2. tooluniverse/admetai_tool.py +16 -5
  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/compose_scripts/output_summarizer.py +87 -33
  12. tooluniverse/compose_tool.py +2 -2
  13. tooluniverse/custom_tool.py +28 -0
  14. tooluniverse/data/adverse_event_tools.json +97 -98
  15. tooluniverse/data/agentic_tools.json +81 -162
  16. tooluniverse/data/arxiv_tools.json +1 -4
  17. tooluniverse/data/compose_tools.json +0 -54
  18. tooluniverse/data/core_tools.json +1 -4
  19. tooluniverse/data/dataset_tools.json +7 -7
  20. tooluniverse/data/doaj_tools.json +1 -3
  21. tooluniverse/data/drug_discovery_agents.json +282 -0
  22. tooluniverse/data/europe_pmc_tools.json +1 -2
  23. tooluniverse/data/genomics_tools.json +174 -0
  24. tooluniverse/data/geo_tools.json +86 -0
  25. tooluniverse/data/literature_search_tools.json +15 -35
  26. tooluniverse/data/markitdown_tools.json +51 -0
  27. tooluniverse/data/monarch_tools.json +1 -2
  28. tooluniverse/data/openalex_tools.json +1 -5
  29. tooluniverse/data/opentarget_tools.json +8 -16
  30. tooluniverse/data/output_summarization_tools.json +23 -20
  31. tooluniverse/data/packages/bioinformatics_core_tools.json +2 -2
  32. tooluniverse/data/packages/cheminformatics_tools.json +1 -1
  33. tooluniverse/data/packages/genomics_tools.json +1 -1
  34. tooluniverse/data/packages/single_cell_tools.json +1 -1
  35. tooluniverse/data/packages/structural_biology_tools.json +1 -1
  36. tooluniverse/data/pmc_tools.json +1 -4
  37. tooluniverse/data/ppi_tools.json +139 -0
  38. tooluniverse/data/pubmed_tools.json +1 -3
  39. tooluniverse/data/semantic_scholar_tools.json +1 -2
  40. tooluniverse/data/tool_composition_tools.json +2 -4
  41. tooluniverse/data/unified_guideline_tools.json +206 -4
  42. tooluniverse/data/xml_tools.json +15 -15
  43. tooluniverse/data/zenodo_tools.json +1 -2
  44. tooluniverse/dbsnp_tool.py +71 -0
  45. tooluniverse/default_config.py +6 -0
  46. tooluniverse/ensembl_tool.py +61 -0
  47. tooluniverse/execute_function.py +235 -76
  48. tooluniverse/generate_tools.py +303 -20
  49. tooluniverse/genomics_gene_search_tool.py +56 -0
  50. tooluniverse/geo_tool.py +116 -0
  51. tooluniverse/gnomad_tool.py +63 -0
  52. tooluniverse/logging_config.py +64 -2
  53. tooluniverse/markitdown_tool.py +159 -0
  54. tooluniverse/mcp_client_tool.py +10 -5
  55. tooluniverse/molecule_2d_tool.py +9 -3
  56. tooluniverse/molecule_3d_tool.py +9 -3
  57. tooluniverse/output_hook.py +217 -150
  58. tooluniverse/smcp.py +18 -10
  59. tooluniverse/smcp_server.py +89 -199
  60. tooluniverse/string_tool.py +112 -0
  61. tooluniverse/tools/{MultiAgentLiteratureSearch.py → ADMETAnalyzerAgent.py} +18 -18
  62. tooluniverse/tools/ArXiv_search_papers.py +3 -3
  63. tooluniverse/tools/CMA_Guidelines_Search.py +52 -0
  64. tooluniverse/tools/CORE_search_papers.py +3 -3
  65. tooluniverse/tools/ClinVar_search_variants.py +52 -0
  66. tooluniverse/tools/ClinicalTrialDesignAgent.py +63 -0
  67. tooluniverse/tools/CompoundDiscoveryAgent.py +59 -0
  68. tooluniverse/tools/DOAJ_search_articles.py +2 -2
  69. tooluniverse/tools/DiseaseAnalyzerAgent.py +52 -0
  70. tooluniverse/tools/DrugInteractionAnalyzerAgent.py +52 -0
  71. tooluniverse/tools/DrugOptimizationAgent.py +63 -0
  72. tooluniverse/tools/Ensembl_lookup_gene_by_symbol.py +52 -0
  73. tooluniverse/tools/EuropePMC_search_articles.py +1 -1
  74. tooluniverse/tools/GIN_Guidelines_Search.py +52 -0
  75. tooluniverse/tools/GWAS_search_associations_by_gene.py +52 -0
  76. tooluniverse/tools/LiteratureSynthesisAgent.py +59 -0
  77. tooluniverse/tools/PMC_search_papers.py +3 -3
  78. tooluniverse/tools/PubMed_search_articles.py +2 -2
  79. tooluniverse/tools/SemanticScholar_search_papers.py +1 -1
  80. tooluniverse/tools/UCSC_get_genes_by_region.py +67 -0
  81. tooluniverse/tools/Zenodo_search_records.py +1 -1
  82. tooluniverse/tools/__init__.py +33 -3
  83. tooluniverse/tools/convert_to_markdown.py +59 -0
  84. tooluniverse/tools/dbSNP_get_variant_by_rsid.py +46 -0
  85. tooluniverse/tools/gnomAD_query_variant.py +52 -0
  86. tooluniverse/tools/openalex_literature_search.py +4 -4
  87. tooluniverse/ucsc_tool.py +60 -0
  88. tooluniverse/unified_guideline_tools.py +1175 -57
  89. tooluniverse/utils.py +51 -4
  90. tooluniverse/zenodo_tool.py +2 -1
  91. {tooluniverse-1.0.7.dist-info → tooluniverse-1.0.9.dist-info}/METADATA +10 -3
  92. {tooluniverse-1.0.7.dist-info → tooluniverse-1.0.9.dist-info}/RECORD +96 -61
  93. {tooluniverse-1.0.7.dist-info → tooluniverse-1.0.9.dist-info}/entry_points.txt +0 -3
  94. {tooluniverse-1.0.7.dist-info → tooluniverse-1.0.9.dist-info}/WHEEL +0 -0
  95. {tooluniverse-1.0.7.dist-info → tooluniverse-1.0.9.dist-info}/licenses/LICENSE +0 -0
  96. {tooluniverse-1.0.7.dist-info → tooluniverse-1.0.9.dist-info}/top_level.txt +0 -0
@@ -33,6 +33,8 @@ import os
33
33
  import time
34
34
  import hashlib
35
35
  import warnings
36
+ from pathlib import Path
37
+ from contextlib import nullcontext
36
38
  from typing import Any, Dict, List, Optional
37
39
  from .utils import read_json_list, evaluate_function_call, extract_function_call_json
38
40
  from .exceptions import (
@@ -58,6 +60,7 @@ from .logging_config import (
58
60
  error,
59
61
  set_log_level,
60
62
  )
63
+ from .cache.result_cache_manager import ResultCacheManager
61
64
  from .output_hook import HookManager
62
65
  from .default_config import default_tool_files, get_default_hook_config
63
66
 
@@ -146,7 +149,38 @@ class ToolNamespace:
146
149
  """Return a ToolCallable for the requested tool name."""
147
150
  if name in self.engine.all_tool_dict:
148
151
  return ToolCallable(self.engine, name)
149
- raise AttributeError(f"Tool '{name}' not found")
152
+
153
+ # Attempt a targeted on-demand load for this tool name
154
+ try:
155
+ self.engine.load_tools(include_tools=[name])
156
+ except Exception:
157
+ # Ignore load errors here; we'll surface a clearer error below if still missing
158
+ pass
159
+ if name in self.engine.all_tool_dict:
160
+ return ToolCallable(self.engine, name)
161
+
162
+ # As a fallback, force full discovery once
163
+ try:
164
+ self.engine.force_full_discovery()
165
+ except Exception:
166
+ # Ignore discovery errors; report consolidated reason below
167
+ pass
168
+ if name in self.engine.all_tool_dict:
169
+ return ToolCallable(self.engine, name)
170
+
171
+ # Build a helpful reason summary
172
+ try:
173
+ status = self.engine.get_lazy_loading_status()
174
+ reason = (
175
+ f"after targeted load and full discovery; "
176
+ f"lazy_loading_enabled={status.get('lazy_loading_enabled')}, "
177
+ f"loaded_tools_count={status.get('loaded_tools_count')}, "
178
+ f"immediately_available_tools={status.get('immediately_available_tools')}"
179
+ )
180
+ except Exception:
181
+ reason = "after targeted load and full discovery"
182
+
183
+ raise AttributeError(f"Tool '{name}' not found ({reason})")
150
184
 
151
185
  def __len__(self) -> int:
152
186
  """Return the number of available tools."""
@@ -260,9 +294,39 @@ class ToolUniverse:
260
294
  self.hook_manager = None
261
295
  self.logger.debug("Output hooks disabled")
262
296
 
263
- # Initialize new attributes for enhanced functionality
264
- self._cache = {} # Simple cache for tool results
265
- self._cache_size = int(os.getenv("TOOLUNIVERSE_CACHE_SIZE", "100"))
297
+ # Initialize caching configuration
298
+ cache_enabled = os.getenv("TOOLUNIVERSE_CACHE_ENABLED", "true").lower() in (
299
+ "true",
300
+ "1",
301
+ "yes",
302
+ )
303
+ persistence_enabled = os.getenv(
304
+ "TOOLUNIVERSE_CACHE_PERSIST", "true"
305
+ ).lower() in ("true", "1", "yes")
306
+ memory_size = int(os.getenv("TOOLUNIVERSE_CACHE_MEMORY_SIZE", "256"))
307
+ default_ttl_env = os.getenv("TOOLUNIVERSE_CACHE_DEFAULT_TTL")
308
+ default_ttl = int(default_ttl_env) if default_ttl_env else None
309
+ singleflight_enabled = os.getenv(
310
+ "TOOLUNIVERSE_CACHE_SINGLEFLIGHT", "true"
311
+ ).lower() in ("true", "1", "yes")
312
+
313
+ cache_path = os.getenv("TOOLUNIVERSE_CACHE_PATH")
314
+ if not cache_path and persistence_enabled:
315
+ base_dir = os.getenv("TOOLUNIVERSE_CACHE_DIR")
316
+ if not base_dir:
317
+ base_dir = os.path.join(str(Path.home()), ".tooluniverse")
318
+ os.makedirs(base_dir, exist_ok=True)
319
+ cache_path = os.path.join(base_dir, "cache.sqlite")
320
+
321
+ self.cache_manager = ResultCacheManager(
322
+ memory_size=memory_size,
323
+ persistent_path=cache_path if persistence_enabled else None,
324
+ enabled=cache_enabled,
325
+ persistence_enabled=persistence_enabled,
326
+ singleflight=singleflight_enabled,
327
+ default_ttl=default_ttl,
328
+ )
329
+
266
330
  self._strict_validation = os.getenv(
267
331
  "TOOLUNIVERSE_STRICT_VALIDATION", "false"
268
332
  ).lower() in ("true", "1", "yes")
@@ -1041,9 +1105,14 @@ class ToolUniverse:
1041
1105
  - When scan_all=True, all JSON files in data/ and subdirectories are scanned
1042
1106
  """
1043
1107
  if mode not in ["config", "type", "list_name", "list_spec"]:
1044
- raise ValueError(
1045
- "Mode must be one of: 'config', 'type', 'list_name', 'list_spec'"
1046
- )
1108
+ # Handle invalid modes gracefully
1109
+ if mode is None:
1110
+ mode = "config" # Default to config mode
1111
+ else:
1112
+ # For invalid string modes, return error info instead of raising
1113
+ return {
1114
+ "error": f"Invalid mode '{mode}'. Must be one of: 'config', 'type', 'list_name', 'list_spec'"
1115
+ }
1047
1116
 
1048
1117
  # For list_name and list_spec modes, we can return early with just the data
1049
1118
  if mode in ["list_name", "list_spec"]:
@@ -1693,84 +1762,139 @@ class ToolUniverse:
1693
1762
  Returns:
1694
1763
  str or dict: Result from the tool execution, or error message if validation fails.
1695
1764
  """
1696
- function_name = function_call_json["name"]
1697
- arguments = function_call_json["arguments"]
1698
-
1699
- # Check cache first if enabled
1700
- if use_cache:
1701
- cache_key = self._make_cache_key(function_name, arguments)
1702
- if cache_key in self._cache:
1703
- self.logger.debug(f"Cache hit for {function_name}")
1704
- return self._cache[cache_key]
1705
-
1706
- # Validate parameters if requested
1707
- if validate:
1708
- validation_error = self._validate_parameters(function_name, arguments)
1709
- if validation_error:
1710
- return self._create_dual_format_error(validation_error)
1711
-
1712
- # Check function call format (existing validation)
1713
- check_status, check_message = self.check_function_call(function_call_json)
1714
- if check_status is False:
1715
- error_msg = "Invalid function call: " + check_message
1716
- return self._create_dual_format_error(
1717
- ToolValidationError(error_msg, details={"check_message": check_message})
1718
- )
1765
+ function_name = function_call_json.get("name", "")
1766
+ arguments = function_call_json.get("arguments", {})
1767
+
1768
+ # Handle malformed queries gracefully
1769
+ if not function_name:
1770
+ return {"error": "Missing or empty function name"}
1771
+
1772
+ if not isinstance(arguments, dict):
1773
+ return {
1774
+ "error": f"Arguments must be a dictionary, got {type(arguments).__name__}"
1775
+ }
1719
1776
 
1720
- # Execute the tool
1721
1777
  tool_instance = None
1722
- tool_arguments = arguments
1723
- try:
1724
- # Get or create tool instance (optimized to avoid duplication)
1725
- tool_instance = self._get_tool_instance(function_name, cache=True)
1778
+ cache_namespace = None
1779
+ cache_version = None
1780
+ cache_key = None
1781
+ composed_cache_key = None
1782
+ cache_guard = nullcontext()
1783
+
1784
+ cache_enabled = (
1785
+ use_cache and self.cache_manager is not None and self.cache_manager.enabled
1786
+ )
1726
1787
 
1727
- if tool_instance:
1728
- result, tool_arguments = self._execute_tool_with_stream(
1729
- tool_instance, arguments, stream_callback, use_cache, validate
1788
+ if cache_enabled:
1789
+ tool_instance = self._get_tool_instance(function_name, cache=True)
1790
+ if tool_instance and tool_instance.supports_caching():
1791
+ cache_namespace = tool_instance.get_cache_namespace()
1792
+ cache_version = tool_instance.get_cache_version()
1793
+ cache_key = self._make_cache_key(function_name, arguments)
1794
+ composed_cache_key = self.cache_manager.compose_key(
1795
+ cache_namespace, cache_version, cache_key
1730
1796
  )
1797
+ cached_value = self.cache_manager.get(
1798
+ namespace=cache_namespace,
1799
+ version=cache_version,
1800
+ cache_key=cache_key,
1801
+ )
1802
+ if cached_value is not None:
1803
+ self.logger.debug(f"Cache hit for {function_name}")
1804
+ return cached_value
1805
+ cache_guard = self.cache_manager.singleflight_guard(composed_cache_key)
1731
1806
  else:
1732
- error_msg = f"Tool '{function_name}' not found"
1807
+ cache_enabled = False
1808
+
1809
+ with cache_guard:
1810
+ if cache_enabled:
1811
+ cached_value = self.cache_manager.get(
1812
+ namespace=cache_namespace,
1813
+ version=cache_version,
1814
+ cache_key=cache_key,
1815
+ )
1816
+ if cached_value is not None:
1817
+ self.logger.debug(
1818
+ f"Cache hit for {function_name} (after singleflight wait)"
1819
+ )
1820
+ return cached_value
1821
+
1822
+ # Validate parameters if requested
1823
+ if validate:
1824
+ validation_error = self._validate_parameters(function_name, arguments)
1825
+ if validation_error:
1826
+ return self._create_dual_format_error(validation_error)
1827
+
1828
+ # Check function call format (existing validation)
1829
+ check_status, check_message = self.check_function_call(function_call_json)
1830
+ if check_status is False:
1831
+ error_msg = "Invalid function call: " + check_message
1733
1832
  return self._create_dual_format_error(
1734
- ToolUnavailableError(
1735
- error_msg,
1736
- next_steps=[
1737
- "Check tool name spelling",
1738
- "Run tu.tools.refresh()",
1739
- ],
1833
+ ToolValidationError(
1834
+ error_msg, details={"check_message": check_message}
1740
1835
  )
1741
1836
  )
1742
- except Exception as e:
1743
- # Classify and return structured error
1744
- classified_error = self._classify_exception(e, function_name, arguments)
1745
- return self._create_dual_format_error(classified_error)
1746
-
1747
- # Apply output hooks if enabled
1748
- if self.hook_manager:
1749
- context = {
1750
- "tool_name": function_name,
1751
- "tool_type": (
1752
- tool_instance.__class__.__name__
1753
- if tool_instance is not None
1754
- else "unknown"
1755
- ),
1756
- "execution_time": time.time(),
1757
- "arguments": tool_arguments,
1758
- }
1759
- result = self.hook_manager.apply_hooks(
1760
- result, function_name, tool_arguments, context
1761
- )
1762
1837
 
1763
- # Cache result if enabled
1764
- if use_cache:
1765
- cache_key = self._make_cache_key(function_name, arguments)
1766
- self._cache[cache_key] = result
1767
- # Simple cache size management
1768
- if len(self._cache) > self._cache_size:
1769
- # Remove oldest entries (simple FIFO)
1770
- oldest_key = next(iter(self._cache))
1771
- del self._cache[oldest_key]
1838
+ # Execute the tool
1839
+ tool_arguments = arguments
1840
+ try:
1841
+ if tool_instance is None:
1842
+ tool_instance = self._get_tool_instance(function_name, cache=True)
1772
1843
 
1773
- return result
1844
+ if tool_instance:
1845
+ result, tool_arguments = self._execute_tool_with_stream(
1846
+ tool_instance, arguments, stream_callback, use_cache, validate
1847
+ )
1848
+ else:
1849
+ error_msg = f"Tool '{function_name}' not found"
1850
+ return self._create_dual_format_error(
1851
+ ToolUnavailableError(
1852
+ error_msg,
1853
+ next_steps=[
1854
+ "Check tool name spelling",
1855
+ "Run tu.tools.refresh()",
1856
+ ],
1857
+ )
1858
+ )
1859
+ except Exception as e:
1860
+ # Classify and return structured error
1861
+ classified_error = self._classify_exception(e, function_name, arguments)
1862
+ return self._create_dual_format_error(classified_error)
1863
+
1864
+ # Apply output hooks if enabled
1865
+ if self.hook_manager:
1866
+ context = {
1867
+ "tool_name": function_name,
1868
+ "tool_type": (
1869
+ tool_instance.__class__.__name__
1870
+ if tool_instance is not None
1871
+ else "unknown"
1872
+ ),
1873
+ "execution_time": time.time(),
1874
+ "arguments": tool_arguments,
1875
+ }
1876
+ result = self.hook_manager.apply_hooks(
1877
+ result, function_name, tool_arguments, context
1878
+ )
1879
+
1880
+ # Cache result if enabled
1881
+ if cache_enabled and tool_instance and tool_instance.supports_caching():
1882
+ if cache_key is None:
1883
+ cache_key = self._make_cache_key(function_name, arguments)
1884
+ if cache_namespace is None:
1885
+ cache_namespace = tool_instance.get_cache_namespace()
1886
+ if cache_version is None:
1887
+ cache_version = tool_instance.get_cache_version()
1888
+ ttl = tool_instance.get_cache_ttl(result)
1889
+ self.cache_manager.set(
1890
+ namespace=cache_namespace,
1891
+ version=cache_version,
1892
+ cache_key=cache_key,
1893
+ value=result,
1894
+ ttl=ttl,
1895
+ )
1896
+
1897
+ return result
1774
1898
 
1775
1899
  def _execute_tool_with_stream(
1776
1900
  self, tool_instance, arguments, stream_callback, use_cache=False, validate=True
@@ -2037,11 +2161,42 @@ class ToolUniverse:
2037
2161
  f"Eager loading completed. {len(self.callable_functions)} tools cached."
2038
2162
  )
2039
2163
 
2164
+ @property
2165
+ def _cache(self):
2166
+ """Access to the internal cache for testing purposes."""
2167
+ if self.cache_manager:
2168
+ return self.cache_manager.memory
2169
+ return {}
2170
+
2040
2171
  def clear_cache(self):
2041
2172
  """Clear the result cache."""
2042
- self._cache.clear()
2173
+ if self.cache_manager:
2174
+ self.cache_manager.clear()
2043
2175
  self.logger.info("Result cache cleared")
2044
2176
 
2177
+ def get_cache_stats(self) -> Dict[str, Any]:
2178
+ """Return cache statistics."""
2179
+ if not self.cache_manager:
2180
+ return {"enabled": False}
2181
+ return self.cache_manager.stats()
2182
+
2183
+ def dump_cache(self, namespace: Optional[str] = None):
2184
+ """Iterate over cached entries (persistent layer only)."""
2185
+ if not self.cache_manager:
2186
+ return iter([])
2187
+ return self.cache_manager.dump(namespace=namespace)
2188
+
2189
+ def close(self):
2190
+ """Release resources."""
2191
+ if self.cache_manager:
2192
+ self.cache_manager.close()
2193
+
2194
+ def __del__(self):
2195
+ try:
2196
+ self.close()
2197
+ except Exception:
2198
+ pass
2199
+
2045
2200
  def get_tool_health(self, tool_name: str = None) -> dict:
2046
2201
  """Get health status for tool(s)."""
2047
2202
  tool_errors = get_tool_errors()
@@ -2248,6 +2403,10 @@ class ToolUniverse:
2248
2403
  self.logger.warning("No tools loaded. Call load_tools() first.")
2249
2404
  return []
2250
2405
 
2406
+ # Handle None or empty pattern
2407
+ if pattern is None or pattern == "":
2408
+ return self.all_tools
2409
+
2251
2410
  import re
2252
2411
 
2253
2412
  flags = 0 if case_sensitive else re.IGNORECASE