tooluniverse 1.0.9.1__py3-none-any.whl → 1.0.11__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 +57 -1
- tooluniverse/admetai_tool.py +1 -1
- tooluniverse/agentic_tool.py +65 -17
- tooluniverse/base_tool.py +19 -8
- tooluniverse/blast_tool.py +132 -0
- tooluniverse/boltz_tool.py +3 -3
- tooluniverse/cache/result_cache_manager.py +167 -12
- tooluniverse/cbioportal_tool.py +42 -0
- tooluniverse/clinvar_tool.py +268 -74
- tooluniverse/compose_scripts/drug_safety_analyzer.py +1 -1
- tooluniverse/compose_scripts/multi_agent_literature_search.py +1 -1
- tooluniverse/compose_scripts/output_summarizer.py +4 -4
- tooluniverse/compose_scripts/tool_discover.py +1941 -443
- tooluniverse/compose_scripts/tool_graph_composer.py +1 -1
- tooluniverse/compose_scripts/tool_metadata_generator.py +1 -1
- tooluniverse/compose_tool.py +9 -9
- tooluniverse/core_tool.py +2 -2
- tooluniverse/ctg_tool.py +4 -4
- tooluniverse/custom_tool.py +1 -1
- tooluniverse/data/agentic_tools.json +0 -370
- tooluniverse/data/alphafold_tools.json +6 -6
- tooluniverse/data/blast_tools.json +112 -0
- tooluniverse/data/cbioportal_tools.json +87 -0
- tooluniverse/data/clinvar_tools.json +235 -0
- tooluniverse/data/compose_tools.json +0 -89
- tooluniverse/data/dbsnp_tools.json +275 -0
- tooluniverse/data/emdb_tools.json +61 -0
- tooluniverse/data/ensembl_tools.json +259 -0
- tooluniverse/data/file_download_tools.json +275 -0
- tooluniverse/data/geo_tools.json +200 -48
- tooluniverse/data/gnomad_tools.json +109 -0
- tooluniverse/data/gtopdb_tools.json +68 -0
- tooluniverse/data/gwas_tools.json +32 -0
- tooluniverse/data/interpro_tools.json +199 -0
- tooluniverse/data/jaspar_tools.json +70 -0
- tooluniverse/data/kegg_tools.json +356 -0
- tooluniverse/data/mpd_tools.json +87 -0
- tooluniverse/data/ols_tools.json +314 -0
- tooluniverse/data/package_discovery_tools.json +64 -0
- tooluniverse/data/packages/categorized_tools.txt +0 -1
- tooluniverse/data/packages/machine_learning_tools.json +0 -47
- tooluniverse/data/paleobiology_tools.json +91 -0
- tooluniverse/data/pride_tools.json +62 -0
- tooluniverse/data/pypi_package_inspector_tools.json +158 -0
- tooluniverse/data/python_executor_tools.json +341 -0
- tooluniverse/data/regulomedb_tools.json +50 -0
- tooluniverse/data/remap_tools.json +89 -0
- tooluniverse/data/screen_tools.json +89 -0
- tooluniverse/data/tool_discovery_agents.json +428 -0
- tooluniverse/data/tool_discovery_agents.json.backup +1343 -0
- tooluniverse/data/uniprot_tools.json +77 -0
- tooluniverse/data/web_search_tools.json +250 -0
- tooluniverse/data/worms_tools.json +55 -0
- tooluniverse/dataset_tool.py +2 -2
- tooluniverse/dbsnp_tool.py +196 -58
- tooluniverse/default_config.py +36 -3
- tooluniverse/emdb_tool.py +30 -0
- tooluniverse/enrichr_tool.py +14 -14
- tooluniverse/ensembl_tool.py +140 -47
- tooluniverse/execute_function.py +594 -29
- tooluniverse/extended_hooks.py +4 -4
- tooluniverse/file_download_tool.py +269 -0
- tooluniverse/gene_ontology_tool.py +1 -1
- tooluniverse/generate_tools.py +3 -3
- tooluniverse/geo_tool.py +81 -28
- tooluniverse/gnomad_tool.py +100 -52
- tooluniverse/gtopdb_tool.py +41 -0
- tooluniverse/humanbase_tool.py +10 -10
- tooluniverse/interpro_tool.py +72 -0
- tooluniverse/jaspar_tool.py +30 -0
- tooluniverse/kegg_tool.py +230 -0
- tooluniverse/logging_config.py +2 -2
- tooluniverse/mcp_client_tool.py +57 -129
- tooluniverse/mcp_integration.py +52 -49
- tooluniverse/mcp_tool_registry.py +147 -528
- tooluniverse/mpd_tool.py +42 -0
- tooluniverse/ncbi_eutils_tool.py +96 -0
- tooluniverse/ols_tool.py +435 -0
- tooluniverse/openalex_tool.py +8 -8
- tooluniverse/openfda_tool.py +2 -2
- tooluniverse/output_hook.py +15 -15
- tooluniverse/package_discovery_tool.py +217 -0
- tooluniverse/package_tool.py +1 -1
- tooluniverse/paleobiology_tool.py +30 -0
- tooluniverse/pmc_tool.py +2 -2
- tooluniverse/pride_tool.py +30 -0
- tooluniverse/pypi_package_inspector_tool.py +593 -0
- tooluniverse/python_executor_tool.py +711 -0
- tooluniverse/regulomedb_tool.py +30 -0
- tooluniverse/remap_tool.py +44 -0
- tooluniverse/remote/boltz/boltz_mcp_server.py +1 -1
- tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +3 -3
- tooluniverse/remote/immune_compass/compass_tool.py +3 -3
- tooluniverse/remote/pinnacle/pinnacle_tool.py +2 -2
- tooluniverse/remote/transcriptformer/transcriptformer_tool.py +3 -3
- tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +3 -3
- tooluniverse/remote_tool.py +4 -4
- tooluniverse/screen_tool.py +44 -0
- tooluniverse/scripts/filter_tool_files.py +2 -2
- tooluniverse/smcp.py +93 -12
- tooluniverse/smcp_server.py +100 -21
- tooluniverse/space/__init__.py +46 -0
- tooluniverse/space/loader.py +133 -0
- tooluniverse/space/validator.py +353 -0
- tooluniverse/tool_finder_embedding.py +5 -3
- tooluniverse/tool_finder_keyword.py +12 -10
- tooluniverse/tool_finder_llm.py +12 -8
- tooluniverse/tools/{UCSC_get_genes_by_region.py → BLAST_nucleotide_search.py} +22 -26
- tooluniverse/tools/BLAST_protein_search.py +63 -0
- tooluniverse/tools/ClinVar_search_variants.py +26 -15
- tooluniverse/tools/CodeQualityAnalyzer.py +3 -3
- tooluniverse/tools/EMDB_get_structure.py +46 -0
- tooluniverse/tools/GtoPdb_get_targets.py +52 -0
- tooluniverse/tools/InterPro_get_domain_details.py +46 -0
- tooluniverse/tools/InterPro_get_protein_domains.py +49 -0
- tooluniverse/tools/InterPro_search_domains.py +52 -0
- tooluniverse/tools/JASPAR_get_transcription_factors.py +52 -0
- tooluniverse/tools/MPD_get_phenotype_data.py +59 -0
- tooluniverse/tools/PRIDE_search_proteomics.py +52 -0
- tooluniverse/tools/PackageAnalyzer.py +55 -0
- tooluniverse/tools/Paleobiology_get_fossils.py +52 -0
- tooluniverse/tools/PyPIPackageInspector.py +59 -0
- tooluniverse/tools/ReMap_get_transcription_factor_binding.py +59 -0
- tooluniverse/tools/ReferenceInfoAnalyzer.py +55 -0
- tooluniverse/tools/RegulomeDB_query_variant.py +46 -0
- tooluniverse/tools/SCREEN_get_regulatory_elements.py +59 -0
- tooluniverse/tools/{ArgumentDescriptionOptimizer.py → TestResultsAnalyzer.py} +13 -13
- tooluniverse/tools/ToolDiscover.py +11 -11
- tooluniverse/tools/UniProt_id_mapping.py +63 -0
- tooluniverse/tools/UniProt_search.py +63 -0
- tooluniverse/tools/UnifiedToolGenerator.py +59 -0
- tooluniverse/tools/WoRMS_search_species.py +49 -0
- tooluniverse/tools/XMLToolOptimizer.py +55 -0
- tooluniverse/tools/__init__.py +119 -29
- tooluniverse/tools/_shared_client.py +3 -3
- tooluniverse/tools/alphafold_get_annotations.py +3 -3
- tooluniverse/tools/alphafold_get_prediction.py +3 -3
- tooluniverse/tools/alphafold_get_summary.py +3 -3
- tooluniverse/tools/cBioPortal_get_cancer_studies.py +46 -0
- tooluniverse/tools/cBioPortal_get_mutations.py +52 -0
- tooluniverse/tools/{gnomAD_query_variant.py → clinvar_get_clinical_significance.py} +8 -11
- tooluniverse/tools/clinvar_get_variant_details.py +49 -0
- tooluniverse/tools/dbSNP_get_variant_by_rsid.py +7 -7
- tooluniverse/tools/dbsnp_get_frequencies.py +46 -0
- tooluniverse/tools/dbsnp_search_by_gene.py +52 -0
- tooluniverse/tools/download_binary_file.py +66 -0
- tooluniverse/tools/download_file.py +71 -0
- tooluniverse/tools/download_text_content.py +55 -0
- tooluniverse/tools/dynamic_package_discovery.py +59 -0
- tooluniverse/tools/ensembl_get_sequence.py +52 -0
- tooluniverse/tools/{Ensembl_lookup_gene_by_symbol.py → ensembl_get_variants.py} +11 -11
- tooluniverse/tools/ensembl_lookup_gene.py +46 -0
- tooluniverse/tools/geo_get_dataset_info.py +46 -0
- tooluniverse/tools/geo_get_sample_info.py +46 -0
- tooluniverse/tools/geo_search_datasets.py +67 -0
- tooluniverse/tools/gnomad_get_gene_constraints.py +49 -0
- tooluniverse/tools/kegg_find_genes.py +52 -0
- tooluniverse/tools/kegg_get_gene_info.py +46 -0
- tooluniverse/tools/kegg_get_pathway_info.py +46 -0
- tooluniverse/tools/kegg_list_organisms.py +44 -0
- tooluniverse/tools/kegg_search_pathway.py +46 -0
- tooluniverse/tools/ols_find_similar_terms.py +63 -0
- tooluniverse/tools/{get_hyperopt_info.py → ols_get_ontology_info.py} +13 -10
- tooluniverse/tools/ols_get_term_ancestors.py +67 -0
- tooluniverse/tools/ols_get_term_children.py +67 -0
- tooluniverse/tools/{TestCaseGenerator.py → ols_get_term_info.py} +12 -9
- tooluniverse/tools/{CodeOptimizer.py → ols_search_ontologies.py} +22 -14
- tooluniverse/tools/ols_search_terms.py +71 -0
- tooluniverse/tools/python_code_executor.py +79 -0
- tooluniverse/tools/python_script_runner.py +79 -0
- tooluniverse/tools/web_api_documentation_search.py +63 -0
- tooluniverse/tools/web_search.py +71 -0
- tooluniverse/uniprot_tool.py +219 -16
- tooluniverse/url_tool.py +19 -1
- tooluniverse/uspto_tool.py +1 -1
- tooluniverse/utils.py +12 -12
- tooluniverse/web_search_tool.py +229 -0
- tooluniverse/worms_tool.py +64 -0
- {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/METADATA +8 -3
- {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/RECORD +184 -92
- tooluniverse/data/genomics_tools.json +0 -174
- tooluniverse/tools/ToolDescriptionOptimizer.py +0 -67
- tooluniverse/tools/ToolImplementationGenerator.py +0 -67
- tooluniverse/tools/ToolOptimizer.py +0 -59
- tooluniverse/tools/ToolSpecificationGenerator.py +0 -67
- tooluniverse/tools/ToolSpecificationOptimizer.py +0 -63
- tooluniverse/ucsc_tool.py +0 -60
- {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/WHEEL +0 -0
- {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/entry_points.txt +0 -0
- {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.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)
|