tooluniverse 1.0.10__py3-none-any.whl → 1.0.11.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.

Files changed (151) hide show
  1. tooluniverse/__init__.py +57 -1
  2. tooluniverse/blast_tool.py +132 -0
  3. tooluniverse/boltz_tool.py +2 -2
  4. tooluniverse/cbioportal_tool.py +42 -0
  5. tooluniverse/clinvar_tool.py +268 -74
  6. tooluniverse/compose_scripts/tool_discover.py +1941 -443
  7. tooluniverse/data/agentic_tools.json +0 -370
  8. tooluniverse/data/alphafold_tools.json +6 -6
  9. tooluniverse/data/blast_tools.json +112 -0
  10. tooluniverse/data/cbioportal_tools.json +87 -0
  11. tooluniverse/data/clinvar_tools.json +235 -0
  12. tooluniverse/data/compose_tools.json +0 -89
  13. tooluniverse/data/dbsnp_tools.json +275 -0
  14. tooluniverse/data/emdb_tools.json +61 -0
  15. tooluniverse/data/ensembl_tools.json +259 -0
  16. tooluniverse/data/file_download_tools.json +275 -0
  17. tooluniverse/data/geo_tools.json +200 -48
  18. tooluniverse/data/gnomad_tools.json +109 -0
  19. tooluniverse/data/gtopdb_tools.json +68 -0
  20. tooluniverse/data/gwas_tools.json +32 -0
  21. tooluniverse/data/interpro_tools.json +199 -0
  22. tooluniverse/data/jaspar_tools.json +70 -0
  23. tooluniverse/data/kegg_tools.json +356 -0
  24. tooluniverse/data/mpd_tools.json +87 -0
  25. tooluniverse/data/ols_tools.json +314 -0
  26. tooluniverse/data/package_discovery_tools.json +64 -0
  27. tooluniverse/data/packages/categorized_tools.txt +0 -1
  28. tooluniverse/data/packages/machine_learning_tools.json +0 -47
  29. tooluniverse/data/paleobiology_tools.json +91 -0
  30. tooluniverse/data/pride_tools.json +62 -0
  31. tooluniverse/data/pypi_package_inspector_tools.json +158 -0
  32. tooluniverse/data/python_executor_tools.json +341 -0
  33. tooluniverse/data/regulomedb_tools.json +50 -0
  34. tooluniverse/data/remap_tools.json +89 -0
  35. tooluniverse/data/screen_tools.json +89 -0
  36. tooluniverse/data/tool_discovery_agents.json +428 -0
  37. tooluniverse/data/tool_discovery_agents.json.backup +1343 -0
  38. tooluniverse/data/uniprot_tools.json +77 -0
  39. tooluniverse/data/web_search_tools.json +250 -0
  40. tooluniverse/data/worms_tools.json +55 -0
  41. tooluniverse/dbsnp_tool.py +196 -58
  42. tooluniverse/default_config.py +35 -2
  43. tooluniverse/emdb_tool.py +30 -0
  44. tooluniverse/ensembl_tool.py +140 -47
  45. tooluniverse/execute_function.py +78 -14
  46. tooluniverse/file_download_tool.py +269 -0
  47. tooluniverse/geo_tool.py +81 -28
  48. tooluniverse/gnomad_tool.py +100 -52
  49. tooluniverse/gtopdb_tool.py +41 -0
  50. tooluniverse/interpro_tool.py +72 -0
  51. tooluniverse/jaspar_tool.py +30 -0
  52. tooluniverse/kegg_tool.py +230 -0
  53. tooluniverse/mpd_tool.py +42 -0
  54. tooluniverse/ncbi_eutils_tool.py +96 -0
  55. tooluniverse/ols_tool.py +435 -0
  56. tooluniverse/package_discovery_tool.py +217 -0
  57. tooluniverse/paleobiology_tool.py +30 -0
  58. tooluniverse/pride_tool.py +30 -0
  59. tooluniverse/pypi_package_inspector_tool.py +593 -0
  60. tooluniverse/python_executor_tool.py +711 -0
  61. tooluniverse/regulomedb_tool.py +30 -0
  62. tooluniverse/remap_tool.py +44 -0
  63. tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +1 -1
  64. tooluniverse/screen_tool.py +44 -0
  65. tooluniverse/smcp.py +10 -2
  66. tooluniverse/smcp_server.py +3 -3
  67. tooluniverse/tool_finder_embedding.py +3 -1
  68. tooluniverse/tool_finder_keyword.py +3 -1
  69. tooluniverse/tool_finder_llm.py +6 -2
  70. tooluniverse/tools/{UCSC_get_genes_by_region.py → BLAST_nucleotide_search.py} +22 -26
  71. tooluniverse/tools/BLAST_protein_search.py +63 -0
  72. tooluniverse/tools/ClinVar_search_variants.py +26 -15
  73. tooluniverse/tools/CodeQualityAnalyzer.py +3 -3
  74. tooluniverse/tools/EMDB_get_structure.py +46 -0
  75. tooluniverse/tools/GtoPdb_get_targets.py +52 -0
  76. tooluniverse/tools/InterPro_get_domain_details.py +46 -0
  77. tooluniverse/tools/InterPro_get_protein_domains.py +49 -0
  78. tooluniverse/tools/InterPro_search_domains.py +52 -0
  79. tooluniverse/tools/JASPAR_get_transcription_factors.py +52 -0
  80. tooluniverse/tools/MPD_get_phenotype_data.py +59 -0
  81. tooluniverse/tools/PRIDE_search_proteomics.py +52 -0
  82. tooluniverse/tools/PackageAnalyzer.py +55 -0
  83. tooluniverse/tools/Paleobiology_get_fossils.py +52 -0
  84. tooluniverse/tools/PyPIPackageInspector.py +59 -0
  85. tooluniverse/tools/ReMap_get_transcription_factor_binding.py +59 -0
  86. tooluniverse/tools/ReferenceInfoAnalyzer.py +55 -0
  87. tooluniverse/tools/RegulomeDB_query_variant.py +46 -0
  88. tooluniverse/tools/SCREEN_get_regulatory_elements.py +59 -0
  89. tooluniverse/tools/{ArgumentDescriptionOptimizer.py → TestResultsAnalyzer.py} +13 -13
  90. tooluniverse/tools/ToolDiscover.py +11 -11
  91. tooluniverse/tools/UniProt_id_mapping.py +63 -0
  92. tooluniverse/tools/UniProt_search.py +63 -0
  93. tooluniverse/tools/UnifiedToolGenerator.py +59 -0
  94. tooluniverse/tools/WoRMS_search_species.py +49 -0
  95. tooluniverse/tools/XMLToolOptimizer.py +55 -0
  96. tooluniverse/tools/__init__.py +119 -29
  97. tooluniverse/tools/alphafold_get_annotations.py +3 -3
  98. tooluniverse/tools/alphafold_get_prediction.py +3 -3
  99. tooluniverse/tools/alphafold_get_summary.py +3 -3
  100. tooluniverse/tools/cBioPortal_get_cancer_studies.py +46 -0
  101. tooluniverse/tools/cBioPortal_get_mutations.py +52 -0
  102. tooluniverse/tools/{gnomAD_query_variant.py → clinvar_get_clinical_significance.py} +8 -11
  103. tooluniverse/tools/clinvar_get_variant_details.py +49 -0
  104. tooluniverse/tools/dbSNP_get_variant_by_rsid.py +7 -7
  105. tooluniverse/tools/dbsnp_get_frequencies.py +46 -0
  106. tooluniverse/tools/dbsnp_search_by_gene.py +52 -0
  107. tooluniverse/tools/download_binary_file.py +66 -0
  108. tooluniverse/tools/download_file.py +71 -0
  109. tooluniverse/tools/download_text_content.py +55 -0
  110. tooluniverse/tools/dynamic_package_discovery.py +59 -0
  111. tooluniverse/tools/ensembl_get_sequence.py +52 -0
  112. tooluniverse/tools/{Ensembl_lookup_gene_by_symbol.py → ensembl_get_variants.py} +11 -11
  113. tooluniverse/tools/ensembl_lookup_gene.py +46 -0
  114. tooluniverse/tools/geo_get_dataset_info.py +46 -0
  115. tooluniverse/tools/geo_get_sample_info.py +46 -0
  116. tooluniverse/tools/geo_search_datasets.py +67 -0
  117. tooluniverse/tools/gnomad_get_gene_constraints.py +49 -0
  118. tooluniverse/tools/kegg_find_genes.py +52 -0
  119. tooluniverse/tools/kegg_get_gene_info.py +46 -0
  120. tooluniverse/tools/kegg_get_pathway_info.py +46 -0
  121. tooluniverse/tools/kegg_list_organisms.py +44 -0
  122. tooluniverse/tools/kegg_search_pathway.py +46 -0
  123. tooluniverse/tools/ols_find_similar_terms.py +63 -0
  124. tooluniverse/tools/{get_hyperopt_info.py → ols_get_ontology_info.py} +13 -10
  125. tooluniverse/tools/ols_get_term_ancestors.py +67 -0
  126. tooluniverse/tools/ols_get_term_children.py +67 -0
  127. tooluniverse/tools/{TestCaseGenerator.py → ols_get_term_info.py} +12 -9
  128. tooluniverse/tools/{CodeOptimizer.py → ols_search_ontologies.py} +22 -14
  129. tooluniverse/tools/ols_search_terms.py +71 -0
  130. tooluniverse/tools/python_code_executor.py +79 -0
  131. tooluniverse/tools/python_script_runner.py +79 -0
  132. tooluniverse/tools/web_api_documentation_search.py +63 -0
  133. tooluniverse/tools/web_search.py +71 -0
  134. tooluniverse/uniprot_tool.py +219 -16
  135. tooluniverse/url_tool.py +18 -0
  136. tooluniverse/utils.py +2 -2
  137. tooluniverse/web_search_tool.py +229 -0
  138. tooluniverse/worms_tool.py +64 -0
  139. {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/METADATA +3 -2
  140. {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/RECORD +144 -55
  141. tooluniverse/data/genomics_tools.json +0 -174
  142. tooluniverse/tools/ToolDescriptionOptimizer.py +0 -67
  143. tooluniverse/tools/ToolImplementationGenerator.py +0 -67
  144. tooluniverse/tools/ToolOptimizer.py +0 -59
  145. tooluniverse/tools/ToolSpecificationGenerator.py +0 -67
  146. tooluniverse/tools/ToolSpecificationOptimizer.py +0 -63
  147. tooluniverse/ucsc_tool.py +0 -60
  148. {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/WHEEL +0 -0
  149. {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/entry_points.txt +0 -0
  150. {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/licenses/LICENSE +0 -0
  151. {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,711 @@
1
+ """
2
+ Python Code Execution Tools for ToolUniverse
3
+
4
+ This module provides two specialized tools for executing Python code:
5
+ 1. python_code_executor - Execute Python code snippets safely in sandboxed environment
6
+ 2. python_script_runner - Run Python script files in isolated subprocess
7
+ """
8
+
9
+ import ast
10
+ import io
11
+ import os
12
+ import signal
13
+ import subprocess
14
+ import sys
15
+ import time
16
+ import traceback
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ from .base_tool import BaseTool
20
+ from .tool_registry import register_tool
21
+
22
+
23
+ class BasePythonExecutor:
24
+ """Base class for Python execution tools with shared security features."""
25
+
26
+ # Safe builtins (whitelist approach)
27
+ SAFE_BUILTINS = {
28
+ "print",
29
+ "len",
30
+ "range",
31
+ "enumerate",
32
+ "zip",
33
+ "map",
34
+ "filter",
35
+ "sorted",
36
+ "sum",
37
+ "min",
38
+ "max",
39
+ "abs",
40
+ "round",
41
+ "int",
42
+ "float",
43
+ "str",
44
+ "bool",
45
+ "list",
46
+ "dict",
47
+ "set",
48
+ "tuple",
49
+ "isinstance",
50
+ "any",
51
+ "all",
52
+ "reversed",
53
+ "slice",
54
+ "type",
55
+ "getattr",
56
+ "setattr",
57
+ "hasattr",
58
+ "callable",
59
+ "__import__", # Needed for import statements
60
+ }
61
+
62
+ # Default allowed modules
63
+ DEFAULT_ALLOWED_MODULES = {
64
+ "math",
65
+ "json",
66
+ "datetime",
67
+ "collections",
68
+ "itertools",
69
+ "re",
70
+ "typing",
71
+ "dataclasses",
72
+ "decimal",
73
+ "fractions",
74
+ "statistics",
75
+ "random",
76
+ }
77
+
78
+ # Forbidden AST node types and their dangerous attributes
79
+ FORBIDDEN_AST_NODES = {
80
+ "Import": ["os", "sys", "subprocess", "socket", "urllib", "requests", "http"],
81
+ "Call": ["open", "eval", "exec", "compile", "__import__", "input", "raw_input"],
82
+ "Attribute": ["__import__", "open", "file"],
83
+ }
84
+
85
+ def __init__(self, tool_config: Dict[str, Any]):
86
+ """Initialize the executor with tool configuration."""
87
+ self.tool_config = tool_config
88
+ self.allowed_modules = set(self.DEFAULT_ALLOWED_MODULES)
89
+
90
+ # Add custom allowed modules if specified
91
+ if "allowed_imports" in tool_config:
92
+ self.allowed_modules.update(tool_config["allowed_imports"])
93
+
94
+ def _check_ast_safety(self, code: str) -> tuple[bool, List[str]]:
95
+ """
96
+ Check code AST for dangerous operations.
97
+
98
+ Returns:
99
+ (is_safe, warnings)
100
+ """
101
+ warnings = []
102
+
103
+ try:
104
+ tree = ast.parse(code)
105
+ except SyntaxError as e:
106
+ return False, [f"Syntax error: {e.msg} at line {e.lineno}"]
107
+
108
+ for node in ast.walk(tree):
109
+ # Check for forbidden imports
110
+ if isinstance(node, ast.Import):
111
+ for alias in node.names:
112
+ # Check if import is forbidden AND not explicitly allowed
113
+ if (
114
+ alias.name in self.FORBIDDEN_AST_NODES["Import"]
115
+ and alias.name not in self.allowed_modules
116
+ ):
117
+ warnings.append(f"Forbidden import: {alias.name}")
118
+
119
+ # Check for forbidden function calls
120
+ elif isinstance(node, ast.Call):
121
+ if isinstance(node.func, ast.Name):
122
+ if node.func.id in self.FORBIDDEN_AST_NODES["Call"]:
123
+ warnings.append(f"Forbidden function call: {node.func.id}")
124
+ elif isinstance(node.func, ast.Attribute):
125
+ if node.func.attr in self.FORBIDDEN_AST_NODES["Call"]:
126
+ warnings.append(f"Forbidden method call: {node.func.attr}")
127
+
128
+ # Check for forbidden attribute access
129
+ elif isinstance(node, ast.Attribute):
130
+ if node.attr in self.FORBIDDEN_AST_NODES["Attribute"]:
131
+ warnings.append(f"Forbidden attribute access: {node.attr}")
132
+
133
+ return len(warnings) == 0, warnings
134
+
135
+ def _create_safe_globals(
136
+ self, additional_vars: Optional[Dict[str, Any]] = None
137
+ ) -> Dict[str, Any]:
138
+ """Create a safe globals dictionary with restricted builtins."""
139
+ # Create restricted builtins
140
+ safe_builtins = {}
141
+ for name in self.SAFE_BUILTINS:
142
+ if hasattr(__builtins__, name):
143
+ safe_builtins[name] = getattr(__builtins__, name)
144
+ elif hasattr(__builtins__, "__dict__") and name in __builtins__.__dict__:
145
+ safe_builtins[name] = __builtins__.__dict__[name]
146
+ else:
147
+ # Try to get from builtins module directly
148
+ try:
149
+ import builtins
150
+
151
+ if hasattr(builtins, name):
152
+ safe_builtins[name] = getattr(builtins, name)
153
+ except ImportError:
154
+ pass
155
+
156
+ # Create safe __import__ function
157
+ def safe_import(name, globals=None, locals=None, fromlist=(), level=0):
158
+ """Safe import function that only allows pre-approved modules."""
159
+ if name in self.allowed_modules:
160
+ return __import__(name, globals, locals, fromlist, level)
161
+ else:
162
+ raise ImportError(
163
+ f"Module '{name}' is not allowed. Allowed modules: {list(self.allowed_modules)}"
164
+ )
165
+
166
+ safe_builtins["__import__"] = safe_import
167
+
168
+ # Pre-import allowed modules
169
+ safe_modules = {}
170
+ for module_name in self.allowed_modules:
171
+ try:
172
+ safe_modules[module_name] = __import__(module_name)
173
+ except ImportError:
174
+ pass # Skip modules that can't be imported
175
+
176
+ globals_dict = {"__builtins__": safe_builtins, **safe_modules}
177
+
178
+ # Add additional variables
179
+ if additional_vars:
180
+ globals_dict.update(additional_vars)
181
+
182
+ return globals_dict
183
+
184
+ def _capture_output(self, func, *args, **kwargs):
185
+ """Capture stdout and stderr during function execution."""
186
+ old_stdout = sys.stdout
187
+ old_stderr = sys.stderr
188
+
189
+ stdout_capture = io.StringIO()
190
+ stderr_capture = io.StringIO()
191
+
192
+ sys.stdout = stdout_capture
193
+ sys.stderr = stderr_capture
194
+
195
+ try:
196
+ result = func(*args, **kwargs)
197
+ stdout_content = stdout_capture.getvalue()
198
+ stderr_content = stderr_capture.getvalue()
199
+ return result, stdout_content, stderr_content
200
+ finally:
201
+ sys.stdout = old_stdout
202
+ sys.stderr = old_stderr
203
+
204
+ def _handle_timeout(self, signum, frame):
205
+ """Handle execution timeout."""
206
+ raise TimeoutError("Code execution timed out")
207
+
208
+ def _execute_with_timeout(self, func, timeout_seconds: int, *args, **kwargs):
209
+ """Execute function with timeout using signal (Unix only)."""
210
+ if hasattr(signal, "SIGALRM"): # Unix systems
211
+ old_handler = signal.signal(signal.SIGALRM, self._handle_timeout)
212
+ signal.alarm(timeout_seconds)
213
+ try:
214
+ result = func(*args, **kwargs)
215
+ return result
216
+ finally:
217
+ signal.alarm(0)
218
+ signal.signal(signal.SIGALRM, old_handler)
219
+ else: # Windows or other systems
220
+ # Fallback to threading timeout (simpler but less reliable)
221
+ import threading
222
+
223
+ result_container = [None]
224
+ exception_container = [None]
225
+
226
+ def target():
227
+ try:
228
+ result_container[0] = func(*args, **kwargs)
229
+ except Exception as e:
230
+ exception_container[0] = e
231
+
232
+ thread = threading.Thread(target=target)
233
+ thread.daemon = True
234
+ thread.start()
235
+ thread.join(timeout_seconds)
236
+
237
+ if thread.is_alive():
238
+ raise TimeoutError("Code execution timed out")
239
+
240
+ if exception_container[0]:
241
+ raise exception_container[0]
242
+
243
+ return result_container[0]
244
+
245
+ def _format_error_response(
246
+ self,
247
+ error: Exception,
248
+ error_type: str,
249
+ stdout: str = "",
250
+ stderr: str = "",
251
+ execution_time: float = 0,
252
+ ) -> Dict[str, Any]:
253
+ """Format error response with detailed information."""
254
+ return {
255
+ "success": False,
256
+ "result": None,
257
+ "stdout": stdout,
258
+ "stderr": stderr,
259
+ "execution_time_ms": int(execution_time * 1000),
260
+ "memory_used_mb": 0, # Not easily measurable in this context
261
+ "error": str(error),
262
+ "error_type": error_type,
263
+ "traceback": traceback.format_exc(),
264
+ "metadata": {
265
+ "code_lines": 0,
266
+ "ast_warnings": [],
267
+ "allowed_modules": list(self.allowed_modules),
268
+ },
269
+ }
270
+
271
+ def _format_success_response(
272
+ self,
273
+ result: Any,
274
+ stdout: str,
275
+ stderr: str,
276
+ execution_time: float,
277
+ code_lines: int = 0,
278
+ ast_warnings: List[str] = None,
279
+ ) -> Dict[str, Any]:
280
+ """Format success response with execution details."""
281
+ return {
282
+ "success": True,
283
+ "result": result,
284
+ "stdout": stdout,
285
+ "stderr": stderr,
286
+ "execution_time_ms": int(execution_time * 1000),
287
+ "memory_used_mb": 0, # Not easily measurable in this context
288
+ "error": None,
289
+ "error_type": None,
290
+ "traceback": None,
291
+ "metadata": {
292
+ "code_lines": code_lines,
293
+ "ast_warnings": ast_warnings or [],
294
+ "allowed_modules": list(self.allowed_modules),
295
+ },
296
+ }
297
+
298
+ def _get_package_to_install(self, package: str) -> str:
299
+ """Get the actual package name to install (parent package for submodules)"""
300
+ if "." in package:
301
+ # For submodules like 'keggtools.keggrest', install the parent package 'keggtools'
302
+ return package.split(".")[0]
303
+ return package
304
+
305
+ def _check_and_install_dependencies(
306
+ self,
307
+ dependencies: List[str],
308
+ auto_install: bool = False,
309
+ require_confirmation: bool = True,
310
+ ) -> Dict[str, Any]:
311
+ """Check and optionally install missing dependencies with user confirmation."""
312
+ if not dependencies:
313
+ return {"success": True, "message": "No dependencies to check"}
314
+
315
+ missing_packages = []
316
+ installed_packages = []
317
+
318
+ print(f"📦 Checking dependencies: {dependencies}")
319
+
320
+ for package in dependencies:
321
+ # Try multiple import strategies
322
+ import_success = False
323
+
324
+ # Strategy 1: Direct package name
325
+ try:
326
+ __import__(package.replace("-", "_"))
327
+ print(f" ✅ {package} is installed (direct import)")
328
+ import_success = True
329
+ except ImportError:
330
+ pass
331
+
332
+ # Strategy 2: Try common submodule patterns
333
+ if not import_success:
334
+ patterns = [
335
+ package.replace("-", "_"),
336
+ package.replace("-", ""),
337
+ package.split("-")[0], # For packages like 'keggtools' -> 'kegg'
338
+ ]
339
+
340
+ for pattern in patterns:
341
+ try:
342
+ __import__(pattern)
343
+ print(f" ✅ {package} is installed (as {pattern})")
344
+ import_success = True
345
+ break
346
+ except ImportError:
347
+ continue
348
+
349
+ # Strategy 3: Check if it's a submodule (e.g., keggtools.api)
350
+ if not import_success and "." in package:
351
+ try:
352
+ __import__(package)
353
+ print(f" ✅ {package} is installed (submodule)")
354
+ import_success = True
355
+ except ImportError:
356
+ pass
357
+
358
+ # Strategy 4: Check parent package for submodules
359
+ if not import_success and "." in package:
360
+ parent_package = package.split(".")[0]
361
+ try:
362
+ parent_module = __import__(parent_package)
363
+ # Try to access the submodule
364
+ submodule_name = package.split(".")[1]
365
+ if hasattr(parent_module, submodule_name):
366
+ print(
367
+ f" ✅ {package} is available (submodule of {parent_package})"
368
+ )
369
+ import_success = True
370
+ else:
371
+ # Try importing the submodule directly
372
+ __import__(package)
373
+ print(f" ✅ {package} is installed (submodule)")
374
+ import_success = True
375
+ except ImportError:
376
+ pass
377
+
378
+ if not import_success:
379
+ print(f" ❌ {package} is not installed")
380
+ missing_packages.append(package)
381
+
382
+ if not missing_packages:
383
+ return {"success": True, "message": "All dependencies are available"}
384
+
385
+ print(f"\n⚠️ Missing packages: {missing_packages}")
386
+
387
+ # Get packages to actually install (parent packages for submodules)
388
+ packages_to_install = [
389
+ self._get_package_to_install(pkg) for pkg in missing_packages
390
+ ]
391
+ packages_to_install = list(set(packages_to_install)) # Remove duplicates
392
+
393
+ # Handle missing packages
394
+ if not auto_install:
395
+ if require_confirmation:
396
+ print("\n🔐 Security Notice:")
397
+ print(
398
+ f" The following packages need to be installed: {packages_to_install}"
399
+ )
400
+ print(f" This will run: pip install {' '.join(packages_to_install)}")
401
+ print(" ⚠️ Only install packages from trusted sources!")
402
+
403
+ # In a real implementation, this would prompt the user
404
+ # For now, we'll return a message asking for confirmation
405
+ return {
406
+ "success": False,
407
+ "requires_confirmation": True,
408
+ "missing_packages": missing_packages,
409
+ "packages_to_install": packages_to_install,
410
+ "install_command": f"pip install {' '.join(packages_to_install)}",
411
+ "message": "User confirmation required for package installation",
412
+ }
413
+ else:
414
+ return {
415
+ "success": False,
416
+ "missing_packages": missing_packages,
417
+ "packages_to_install": packages_to_install,
418
+ "message": "Missing dependencies detected, auto-install disabled",
419
+ }
420
+
421
+ # Auto-install missing packages
422
+ print("💿 Installing missing packages...")
423
+
424
+ for package_to_install in packages_to_install:
425
+ try:
426
+ print(f" 📥 Installing {package_to_install}...")
427
+ result = subprocess.run(
428
+ [sys.executable, "-m", "pip", "install", package_to_install],
429
+ capture_output=True,
430
+ text=True,
431
+ timeout=300,
432
+ )
433
+
434
+ if result.returncode == 0:
435
+ print(f" ✅ Successfully installed {package_to_install}")
436
+ installed_packages.append(package_to_install)
437
+
438
+ # Verify installation
439
+ try:
440
+ __import__(package_to_install.replace("-", "_"))
441
+ print(f" ✅ {package_to_install} import verified")
442
+ except ImportError:
443
+ print(
444
+ f" ⚠️ {package_to_install} installed but import may need different name"
445
+ )
446
+ else:
447
+ print(
448
+ f" ❌ Failed to install {package_to_install}: {result.stderr}"
449
+ )
450
+ return {
451
+ "success": False,
452
+ "error": f"Failed to install {package_to_install}: {result.stderr}",
453
+ "installed_packages": installed_packages,
454
+ }
455
+
456
+ except Exception as e:
457
+ print(f" ❌ Error installing {package_to_install}: {e}")
458
+ return {
459
+ "success": False,
460
+ "error": f"Error installing {package_to_install}: {e}",
461
+ "installed_packages": installed_packages,
462
+ }
463
+
464
+ print("✅ All dependencies installed successfully")
465
+ return {
466
+ "success": True,
467
+ "installed_packages": installed_packages,
468
+ "message": f"Successfully installed {len(installed_packages)} packages",
469
+ }
470
+
471
+
472
+ @register_tool("PythonCodeExecutor")
473
+ class PythonCodeExecutor(BasePythonExecutor, BaseTool):
474
+ """Execute Python code snippets safely in sandboxed environment."""
475
+
476
+ def __init__(self, tool_config: Dict[str, Any]):
477
+ BasePythonExecutor.__init__(self, tool_config)
478
+ BaseTool.__init__(self, tool_config)
479
+
480
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
481
+ """Execute Python code snippet with safety checks and timeout."""
482
+ try:
483
+ # Extract parameters
484
+ code = arguments.get("code", "")
485
+ if not code:
486
+ return self._format_error_response(
487
+ ValueError("Code parameter is required"),
488
+ "ValueError",
489
+ execution_time=0,
490
+ )
491
+
492
+ timeout = arguments.get("timeout", 30)
493
+ timeout = min(max(timeout, 1), 300) # Clamp between 1-300 seconds
494
+
495
+ return_variable = arguments.get("return_variable", "result")
496
+ additional_vars = arguments.get("arguments", {})
497
+
498
+ # Update allowed modules if specified
499
+ if "allowed_imports" in arguments:
500
+ self.allowed_modules.update(arguments["allowed_imports"])
501
+
502
+ # Check AST safety
503
+ is_safe, ast_warnings = self._check_ast_safety(code)
504
+ if not is_safe:
505
+ return self._format_error_response(
506
+ ValueError(
507
+ f"Code contains forbidden operations: "
508
+ f"{', '.join(ast_warnings)}"
509
+ ),
510
+ "SecurityError",
511
+ execution_time=0,
512
+ )
513
+
514
+ # Check dependencies if provided
515
+ dependencies = arguments.get("dependencies", [])
516
+ auto_install = arguments.get("auto_install_dependencies", False)
517
+ require_confirmation = arguments.get("require_confirmation", True)
518
+
519
+ if dependencies:
520
+ dep_result = self._check_and_install_dependencies(
521
+ dependencies, auto_install, require_confirmation
522
+ )
523
+
524
+ if not dep_result["success"]:
525
+ if dep_result.get("requires_confirmation"):
526
+ return {
527
+ "success": False,
528
+ "requires_confirmation": True,
529
+ "missing_packages": dep_result["missing_packages"],
530
+ "packages_to_install": dep_result.get(
531
+ "packages_to_install", []
532
+ ),
533
+ "install_command": dep_result["install_command"],
534
+ "message": dep_result["message"],
535
+ }
536
+ else:
537
+ return self._format_error_response(
538
+ RuntimeError(
539
+ dep_result.get("error", dep_result["message"])
540
+ ),
541
+ "DependencyError",
542
+ execution_time=0,
543
+ )
544
+
545
+ # Create safe execution environment
546
+ safe_globals = self._create_safe_globals(additional_vars)
547
+ safe_locals = {}
548
+
549
+ # Execute with timeout and output capture
550
+ start_time = time.time()
551
+
552
+ def execute_code():
553
+ return self._capture_output(exec, code, safe_globals, safe_locals)
554
+
555
+ try:
556
+ result, stdout, stderr = self._execute_with_timeout(
557
+ execute_code, timeout
558
+ )
559
+ execution_time = time.time() - start_time
560
+
561
+ # Extract result from locals
562
+ final_result = safe_locals.get(return_variable, None)
563
+
564
+ # Count code lines
565
+ code_lines = len(code.splitlines())
566
+
567
+ return self._format_success_response(
568
+ final_result,
569
+ stdout,
570
+ stderr,
571
+ execution_time,
572
+ code_lines,
573
+ ast_warnings,
574
+ )
575
+
576
+ except TimeoutError:
577
+ execution_time = time.time() - start_time
578
+ return self._format_error_response(
579
+ TimeoutError(
580
+ f"Code execution timed out after " f"{timeout} seconds"
581
+ ),
582
+ "TimeoutError",
583
+ execution_time=execution_time,
584
+ )
585
+
586
+ except Exception as e:
587
+ return self._format_error_response(e, type(e).__name__, execution_time=0)
588
+
589
+
590
+ @register_tool("PythonScriptRunner")
591
+ class PythonScriptRunner(BasePythonExecutor, BaseTool):
592
+ """Run Python script files in isolated subprocess with resource limits."""
593
+
594
+ def __init__(self, tool_config: Dict[str, Any]):
595
+ BasePythonExecutor.__init__(self, tool_config)
596
+ BaseTool.__init__(self, tool_config)
597
+
598
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
599
+ """Run Python script file in subprocess with safety limits."""
600
+ try:
601
+ # Extract parameters
602
+ script_path = arguments.get("script_path", "")
603
+ if not script_path:
604
+ return self._format_error_response(
605
+ ValueError("script_path parameter is required"),
606
+ "ValueError",
607
+ execution_time=0,
608
+ )
609
+
610
+ if not os.path.exists(script_path):
611
+ return self._format_error_response(
612
+ FileNotFoundError(f"Script file not found: {script_path}"),
613
+ "FileNotFoundError",
614
+ execution_time=0,
615
+ )
616
+
617
+ script_args = arguments.get("script_args", [])
618
+ timeout = arguments.get("timeout", 60)
619
+ working_dir = arguments.get("working_directory", os.getcwd())
620
+ env_vars = arguments.get("env_vars", {})
621
+
622
+ # Check dependencies if provided
623
+ dependencies = arguments.get("dependencies", [])
624
+ auto_install = arguments.get("auto_install_dependencies", False)
625
+ require_confirmation = arguments.get("require_confirmation", True)
626
+
627
+ if dependencies:
628
+ dep_result = self._check_and_install_dependencies(
629
+ dependencies, auto_install, require_confirmation
630
+ )
631
+
632
+ if not dep_result["success"]:
633
+ if dep_result.get("requires_confirmation"):
634
+ return {
635
+ "success": False,
636
+ "requires_confirmation": True,
637
+ "missing_packages": dep_result["missing_packages"],
638
+ "packages_to_install": dep_result.get(
639
+ "packages_to_install", []
640
+ ),
641
+ "install_command": dep_result["install_command"],
642
+ "message": dep_result["message"],
643
+ }
644
+ else:
645
+ return self._format_error_response(
646
+ RuntimeError(
647
+ dep_result.get("error", dep_result["message"])
648
+ ),
649
+ "DependencyError",
650
+ execution_time=0,
651
+ )
652
+
653
+ # Create restricted environment
654
+ restricted_env = os.environ.copy()
655
+ restricted_env.update(env_vars)
656
+ # Remove potentially dangerous environment variables
657
+ dangerous_vars = ["PYTHONPATH", "PATH"]
658
+ for var in dangerous_vars:
659
+ if var in restricted_env:
660
+ del restricted_env[var]
661
+
662
+ # Prepare command
663
+ cmd = [sys.executable, script_path] + script_args
664
+
665
+ # Execute in subprocess
666
+ start_time = time.time()
667
+
668
+ try:
669
+ result = subprocess.run(
670
+ cmd,
671
+ cwd=working_dir,
672
+ env=restricted_env,
673
+ capture_output=True,
674
+ text=True,
675
+ timeout=timeout,
676
+ )
677
+
678
+ execution_time = time.time() - start_time
679
+
680
+ if result.returncode == 0:
681
+ return self._format_success_response(
682
+ f"Script executed successfully "
683
+ f"(exit code: {result.returncode})",
684
+ result.stdout,
685
+ result.stderr,
686
+ execution_time,
687
+ code_lines=0, # Not easily measurable for external scripts
688
+ )
689
+ else:
690
+ return self._format_error_response(
691
+ RuntimeError(
692
+ f"Script failed with exit code " f"{result.returncode}"
693
+ ),
694
+ "RuntimeError",
695
+ result.stdout,
696
+ result.stderr,
697
+ execution_time,
698
+ )
699
+
700
+ except subprocess.TimeoutExpired:
701
+ execution_time = time.time() - start_time
702
+ return self._format_error_response(
703
+ TimeoutError(
704
+ f"Script execution timed out after " f"{timeout} seconds"
705
+ ),
706
+ "TimeoutError",
707
+ execution_time=execution_time,
708
+ )
709
+
710
+ except Exception as e:
711
+ return self._format_error_response(e, type(e).__name__, execution_time=0)