camel-ai 0.2.76a4__py3-none-any.whl → 0.2.76a6__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 camel-ai might be problematic. Click here for more details.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +276 -21
- camel/configs/__init__.py +3 -0
- camel/configs/cometapi_config.py +104 -0
- camel/interpreters/docker/Dockerfile +3 -12
- camel/memories/blocks/chat_history_block.py +4 -1
- camel/memories/records.py +52 -8
- camel/messages/base.py +1 -1
- camel/models/__init__.py +2 -0
- camel/models/azure_openai_model.py +0 -6
- camel/models/cometapi_model.py +83 -0
- camel/models/model_factory.py +2 -0
- camel/models/openai_compatible_model.py +0 -6
- camel/models/zhipuai_model.py +61 -2
- camel/retrievers/auto_retriever.py +1 -0
- camel/societies/workforce/workforce.py +9 -7
- camel/storages/key_value_storages/json.py +15 -2
- camel/storages/vectordb_storages/tidb.py +8 -6
- camel/toolkits/__init__.py +4 -0
- camel/toolkits/dingtalk.py +1135 -0
- camel/toolkits/edgeone_pages_mcp_toolkit.py +11 -31
- camel/toolkits/google_drive_mcp_toolkit.py +12 -31
- camel/toolkits/message_integration.py +3 -0
- camel/toolkits/notion_mcp_toolkit.py +16 -26
- camel/toolkits/origene_mcp_toolkit.py +8 -49
- camel/toolkits/playwright_mcp_toolkit.py +12 -31
- camel/toolkits/resend_toolkit.py +168 -0
- camel/toolkits/terminal_toolkit/__init__.py +18 -0
- camel/toolkits/terminal_toolkit/terminal_toolkit.py +924 -0
- camel/toolkits/terminal_toolkit/utils.py +580 -0
- camel/types/enums.py +109 -0
- camel/types/unified_model_type.py +5 -0
- camel/utils/commons.py +2 -0
- {camel_ai-0.2.76a4.dist-info → camel_ai-0.2.76a6.dist-info}/METADATA +25 -6
- {camel_ai-0.2.76a4.dist-info → camel_ai-0.2.76a6.dist-info}/RECORD +37 -31
- camel/toolkits/terminal_toolkit.py +0 -1798
- {camel_ai-0.2.76a4.dist-info → camel_ai-0.2.76a6.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.76a4.dist-info → camel_ai-0.2.76a6.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import platform
|
|
17
|
+
import re
|
|
18
|
+
import shlex
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import venv
|
|
23
|
+
from typing import Optional, Set, Tuple
|
|
24
|
+
|
|
25
|
+
from camel.logger import get_logger
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def contains_command_chaining(command: str) -> bool:
|
|
31
|
+
r"""Check if command contains chaining operators that could be used to
|
|
32
|
+
bypass security.
|
|
33
|
+
"""
|
|
34
|
+
# Pattern to match command chaining operators: ;, &&, ||, |
|
|
35
|
+
# But exclude cases where they are inside quotes or escaped
|
|
36
|
+
chaining_pattern = r'''
|
|
37
|
+
(?<!\\) # Not preceded by backslash (not escaped)
|
|
38
|
+
(?: # Group for alternation
|
|
39
|
+
; # Semicolon
|
|
40
|
+
| # OR
|
|
41
|
+
\|\| # Logical OR
|
|
42
|
+
| # OR
|
|
43
|
+
&& # Logical AND
|
|
44
|
+
| # OR
|
|
45
|
+
(?<!\|) # Not preceded by pipe (to avoid matching ||)
|
|
46
|
+
\| # Single pipe
|
|
47
|
+
(?!\|) # Not followed by pipe (to avoid matching ||)
|
|
48
|
+
)
|
|
49
|
+
(?= # Positive lookahead
|
|
50
|
+
(?: # Group
|
|
51
|
+
[^"'] # Not a quote
|
|
52
|
+
| # OR
|
|
53
|
+
"[^"]*" # Content in double quotes
|
|
54
|
+
| # OR
|
|
55
|
+
'[^']*' # Content in single quotes
|
|
56
|
+
)* # Zero or more times
|
|
57
|
+
$ # End of string
|
|
58
|
+
)
|
|
59
|
+
'''
|
|
60
|
+
|
|
61
|
+
return bool(re.search(chaining_pattern, command, re.VERBOSE))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def sanitize_command(
|
|
65
|
+
command: str,
|
|
66
|
+
use_docker_backend: bool = False,
|
|
67
|
+
safe_mode: bool = True,
|
|
68
|
+
working_dir: Optional[str] = None,
|
|
69
|
+
allowed_commands: Optional[Set[str]] = None,
|
|
70
|
+
) -> Tuple[bool, str]:
|
|
71
|
+
r"""A comprehensive command sanitizer for both local and Docker backends.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
command (str): The command to sanitize
|
|
75
|
+
use_docker_backend (bool): Whether using Docker backend
|
|
76
|
+
safe_mode (bool): Whether to apply security checks
|
|
77
|
+
working_dir (Optional[str]): Working directory for path validation
|
|
78
|
+
allowed_commands (Optional[Set[str]]): Set of allowed commands
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Tuple[bool, str]: (is_safe, message_or_command)
|
|
82
|
+
"""
|
|
83
|
+
# Apply security checks to both backends - security should be consistent
|
|
84
|
+
if not safe_mode:
|
|
85
|
+
return True, command # Skip all checks if safe_mode is disabled
|
|
86
|
+
|
|
87
|
+
# First check for command chaining and pipes
|
|
88
|
+
if contains_command_chaining(command):
|
|
89
|
+
return (
|
|
90
|
+
False,
|
|
91
|
+
"Command chaining (;, &&, ||, |) is not allowed "
|
|
92
|
+
"for security reasons.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
parts = shlex.split(command)
|
|
96
|
+
if not parts:
|
|
97
|
+
return False, "Empty command is not allowed."
|
|
98
|
+
base_cmd = parts[0].lower()
|
|
99
|
+
|
|
100
|
+
# If whitelist is defined, only allow whitelisted commands
|
|
101
|
+
if allowed_commands is not None:
|
|
102
|
+
if base_cmd not in allowed_commands:
|
|
103
|
+
return (
|
|
104
|
+
False,
|
|
105
|
+
f"Command '{base_cmd}' is not in the allowed commands list.",
|
|
106
|
+
)
|
|
107
|
+
# If command is whitelisted, skip the dangerous commands check
|
|
108
|
+
# but still apply other safety checks
|
|
109
|
+
else:
|
|
110
|
+
# Block dangerous commands (only when no whitelist is defined)
|
|
111
|
+
dangerous_commands = [
|
|
112
|
+
# System administration
|
|
113
|
+
'sudo',
|
|
114
|
+
'su',
|
|
115
|
+
'reboot',
|
|
116
|
+
'shutdown',
|
|
117
|
+
'halt',
|
|
118
|
+
'poweroff',
|
|
119
|
+
'init',
|
|
120
|
+
# File system manipulation
|
|
121
|
+
'rm',
|
|
122
|
+
'mv',
|
|
123
|
+
'chmod',
|
|
124
|
+
'chown',
|
|
125
|
+
'chgrp',
|
|
126
|
+
'umount',
|
|
127
|
+
'mount',
|
|
128
|
+
# Disk operations
|
|
129
|
+
'dd',
|
|
130
|
+
'mkfs',
|
|
131
|
+
'fdisk',
|
|
132
|
+
'parted',
|
|
133
|
+
'fsck',
|
|
134
|
+
'mkswap',
|
|
135
|
+
'swapon',
|
|
136
|
+
'swapoff',
|
|
137
|
+
# Process management
|
|
138
|
+
'kill',
|
|
139
|
+
'killall',
|
|
140
|
+
'pkill',
|
|
141
|
+
'service',
|
|
142
|
+
'systemctl',
|
|
143
|
+
'systemd',
|
|
144
|
+
# Network configuration
|
|
145
|
+
'iptables',
|
|
146
|
+
'ip6tables',
|
|
147
|
+
'ifconfig',
|
|
148
|
+
'route',
|
|
149
|
+
'iptables-save',
|
|
150
|
+
# Cron and scheduling
|
|
151
|
+
'crontab',
|
|
152
|
+
'at',
|
|
153
|
+
'batch',
|
|
154
|
+
# User management
|
|
155
|
+
'useradd',
|
|
156
|
+
'userdel',
|
|
157
|
+
'usermod',
|
|
158
|
+
'passwd',
|
|
159
|
+
'chpasswd',
|
|
160
|
+
'newgrp',
|
|
161
|
+
# Kernel modules
|
|
162
|
+
'modprobe',
|
|
163
|
+
'rmmod',
|
|
164
|
+
'insmod',
|
|
165
|
+
'lsmod',
|
|
166
|
+
# System information that could leak sensitive data
|
|
167
|
+
'dmesg',
|
|
168
|
+
'last',
|
|
169
|
+
'lastlog',
|
|
170
|
+
'who',
|
|
171
|
+
'w',
|
|
172
|
+
]
|
|
173
|
+
if base_cmd in dangerous_commands:
|
|
174
|
+
# Special handling for rm command - use regex for precise checking
|
|
175
|
+
if base_cmd == 'rm':
|
|
176
|
+
# Check for dangerous rm options using regex
|
|
177
|
+
dangerous_rm_pattern = (
|
|
178
|
+
r'\s-[^-\s]*[rf][^-\s]*\s|\s--force\s|'
|
|
179
|
+
r'\s--recursive\s|\s-rf\s|\s-fr\s'
|
|
180
|
+
)
|
|
181
|
+
if re.search(dangerous_rm_pattern, command, re.IGNORECASE):
|
|
182
|
+
return (
|
|
183
|
+
False,
|
|
184
|
+
f"Command '{base_cmd}' with forceful or "
|
|
185
|
+
f"recursive options is blocked for safety.",
|
|
186
|
+
)
|
|
187
|
+
# Also block rm without any target (could be dangerous)
|
|
188
|
+
if len(parts) < 2:
|
|
189
|
+
return (
|
|
190
|
+
False,
|
|
191
|
+
"rm command requires target "
|
|
192
|
+
"file/directory specification.",
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
return False, f"Command '{base_cmd}' is blocked for safety."
|
|
196
|
+
|
|
197
|
+
# For local backend only: prevent changing
|
|
198
|
+
# directory outside the workspace
|
|
199
|
+
# Docker containers are already sandboxed,
|
|
200
|
+
# so this check is not needed there
|
|
201
|
+
if (
|
|
202
|
+
not use_docker_backend
|
|
203
|
+
and base_cmd == 'cd'
|
|
204
|
+
and len(parts) > 1
|
|
205
|
+
and working_dir
|
|
206
|
+
):
|
|
207
|
+
target_dir = os.path.abspath(os.path.join(working_dir, parts[1]))
|
|
208
|
+
if not target_dir.startswith(working_dir):
|
|
209
|
+
return False, "Cannot 'cd' outside of the working directory."
|
|
210
|
+
|
|
211
|
+
return True, command
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# Environment management utilities
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def is_uv_environment() -> bool:
|
|
218
|
+
r"""Detect whether the current Python runtime is managed by uv."""
|
|
219
|
+
return (
|
|
220
|
+
"UV_CACHE_DIR" in os.environ
|
|
221
|
+
or "uv" in sys.executable
|
|
222
|
+
or shutil.which("uv") is not None
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def ensure_uv_available(update_callback=None) -> Tuple[bool, Optional[str]]:
|
|
227
|
+
r"""Ensure uv is available, installing it if necessary.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
update_callback: Optional callback function to receive status updates
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Tuple[bool, Optional[str]]: (success, uv_path)
|
|
234
|
+
"""
|
|
235
|
+
# Check if uv is already available
|
|
236
|
+
existing_uv = shutil.which("uv")
|
|
237
|
+
if existing_uv is not None:
|
|
238
|
+
if update_callback:
|
|
239
|
+
update_callback(f"uv is already available at: {existing_uv}\n")
|
|
240
|
+
return True, existing_uv
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
if update_callback:
|
|
244
|
+
update_callback("uv not found, installing...\n")
|
|
245
|
+
|
|
246
|
+
os_type = platform.system()
|
|
247
|
+
|
|
248
|
+
# Install uv using the official installer script
|
|
249
|
+
if os_type in ['darwin', 'linux'] or os_type.startswith('linux'):
|
|
250
|
+
# Use curl to download and execute the installer
|
|
251
|
+
install_cmd = "curl -LsSf https://astral.sh/uv/install.sh | sh"
|
|
252
|
+
result = subprocess.run(
|
|
253
|
+
install_cmd,
|
|
254
|
+
shell=True,
|
|
255
|
+
capture_output=True,
|
|
256
|
+
text=True,
|
|
257
|
+
timeout=60,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if result.returncode != 0:
|
|
261
|
+
if update_callback:
|
|
262
|
+
update_callback(f"Failed to install uv: {result.stderr}\n")
|
|
263
|
+
return False, None
|
|
264
|
+
|
|
265
|
+
# Check if uv was installed in the expected location
|
|
266
|
+
home = os.path.expanduser("~")
|
|
267
|
+
uv_bin_path = os.path.join(home, ".cargo", "bin")
|
|
268
|
+
uv_executable = os.path.join(uv_bin_path, "uv")
|
|
269
|
+
|
|
270
|
+
if os.path.exists(uv_executable):
|
|
271
|
+
if update_callback:
|
|
272
|
+
update_callback(
|
|
273
|
+
f"uv installed successfully at: {uv_executable}\n"
|
|
274
|
+
)
|
|
275
|
+
return True, uv_executable
|
|
276
|
+
|
|
277
|
+
elif os_type == 'Windows':
|
|
278
|
+
# Use PowerShell to install uv on Windows
|
|
279
|
+
install_cmd = (
|
|
280
|
+
"powershell -ExecutionPolicy Bypass -c "
|
|
281
|
+
"\"irm https://astral.sh/uv/install.ps1 | iex\""
|
|
282
|
+
)
|
|
283
|
+
result = subprocess.run(
|
|
284
|
+
install_cmd,
|
|
285
|
+
shell=True,
|
|
286
|
+
capture_output=True,
|
|
287
|
+
text=True,
|
|
288
|
+
timeout=60,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if result.returncode != 0:
|
|
292
|
+
if update_callback:
|
|
293
|
+
update_callback(f"Failed to install uv: {result.stderr}\n")
|
|
294
|
+
return False, None
|
|
295
|
+
|
|
296
|
+
# Check if uv was installed in the expected location on Windows
|
|
297
|
+
home = os.path.expanduser("~")
|
|
298
|
+
uv_bin_path = os.path.join(home, ".cargo", "bin")
|
|
299
|
+
uv_executable = os.path.join(uv_bin_path, "uv.exe")
|
|
300
|
+
|
|
301
|
+
if os.path.exists(uv_executable):
|
|
302
|
+
if update_callback:
|
|
303
|
+
update_callback(
|
|
304
|
+
f"uv installed successfully at: {uv_executable}\n"
|
|
305
|
+
)
|
|
306
|
+
return True, uv_executable
|
|
307
|
+
|
|
308
|
+
if update_callback:
|
|
309
|
+
update_callback("Failed to verify uv installation\n")
|
|
310
|
+
return False, None
|
|
311
|
+
|
|
312
|
+
except Exception as e:
|
|
313
|
+
if update_callback:
|
|
314
|
+
update_callback(f"Error installing uv: {e!s}\n")
|
|
315
|
+
logger.error(f"Failed to install uv: {e}")
|
|
316
|
+
return False, None
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def setup_initial_env_with_uv(
|
|
320
|
+
env_path: str, uv_path: str, working_dir: str, update_callback=None
|
|
321
|
+
) -> bool:
|
|
322
|
+
r"""Set up initial environment using uv."""
|
|
323
|
+
try:
|
|
324
|
+
# Create virtual environment with Python 3.10 using uv
|
|
325
|
+
subprocess.run(
|
|
326
|
+
[uv_path, "venv", "--python", "3.10", env_path],
|
|
327
|
+
check=True,
|
|
328
|
+
capture_output=True,
|
|
329
|
+
cwd=working_dir,
|
|
330
|
+
timeout=300,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Get the python path from the new environment
|
|
334
|
+
if platform.system() == 'Windows':
|
|
335
|
+
python_path = os.path.join(env_path, "Scripts", "python.exe")
|
|
336
|
+
else:
|
|
337
|
+
python_path = os.path.join(env_path, "bin", "python")
|
|
338
|
+
|
|
339
|
+
# Install essential packages using uv
|
|
340
|
+
essential_packages = [
|
|
341
|
+
"pip",
|
|
342
|
+
"setuptools",
|
|
343
|
+
"wheel",
|
|
344
|
+
"pyautogui",
|
|
345
|
+
"plotly",
|
|
346
|
+
]
|
|
347
|
+
subprocess.run(
|
|
348
|
+
[
|
|
349
|
+
uv_path,
|
|
350
|
+
"pip",
|
|
351
|
+
"install",
|
|
352
|
+
"--python",
|
|
353
|
+
python_path,
|
|
354
|
+
*essential_packages,
|
|
355
|
+
],
|
|
356
|
+
check=True,
|
|
357
|
+
capture_output=True,
|
|
358
|
+
cwd=working_dir,
|
|
359
|
+
timeout=300,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if update_callback:
|
|
363
|
+
update_callback(
|
|
364
|
+
"[UV] Initial environment created with Python 3.10 "
|
|
365
|
+
"and essential packages"
|
|
366
|
+
)
|
|
367
|
+
return True
|
|
368
|
+
|
|
369
|
+
except subprocess.CalledProcessError as e:
|
|
370
|
+
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
371
|
+
if update_callback:
|
|
372
|
+
update_callback(f"UV setup failed: {error_msg}\n")
|
|
373
|
+
return False
|
|
374
|
+
except subprocess.TimeoutExpired:
|
|
375
|
+
if update_callback:
|
|
376
|
+
update_callback("UV setup timed out after 5 minutes\n")
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def setup_initial_env_with_venv(
|
|
381
|
+
env_path: str, working_dir: str, update_callback=None
|
|
382
|
+
) -> bool:
|
|
383
|
+
r"""Set up initial environment using standard venv."""
|
|
384
|
+
try:
|
|
385
|
+
# Create virtual environment with system Python
|
|
386
|
+
venv.create(
|
|
387
|
+
env_path, with_pip=True, system_site_packages=False, symlinks=False
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Get pip path
|
|
391
|
+
if platform.system() == 'Windows':
|
|
392
|
+
pip_path = os.path.join(env_path, "Scripts", "pip.exe")
|
|
393
|
+
else:
|
|
394
|
+
pip_path = os.path.join(env_path, "bin", "pip")
|
|
395
|
+
|
|
396
|
+
# Upgrade pip and install essential packages
|
|
397
|
+
essential_packages = [
|
|
398
|
+
"pip",
|
|
399
|
+
"setuptools",
|
|
400
|
+
"wheel",
|
|
401
|
+
"pyautogui",
|
|
402
|
+
"plotly",
|
|
403
|
+
]
|
|
404
|
+
subprocess.run(
|
|
405
|
+
[pip_path, "install", "--upgrade", *essential_packages],
|
|
406
|
+
check=True,
|
|
407
|
+
capture_output=True,
|
|
408
|
+
cwd=working_dir,
|
|
409
|
+
timeout=300,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if update_callback:
|
|
413
|
+
update_callback(
|
|
414
|
+
"Initial environment created with system Python "
|
|
415
|
+
"and essential packages"
|
|
416
|
+
)
|
|
417
|
+
return True
|
|
418
|
+
|
|
419
|
+
except subprocess.CalledProcessError as e:
|
|
420
|
+
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
421
|
+
if update_callback:
|
|
422
|
+
update_callback(f"Venv setup failed: {error_msg}\n")
|
|
423
|
+
return False
|
|
424
|
+
except subprocess.TimeoutExpired:
|
|
425
|
+
if update_callback:
|
|
426
|
+
update_callback("Venv setup timed out after 5 minutes\n")
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def clone_current_environment(
|
|
431
|
+
env_path: str, working_dir: str, update_callback=None
|
|
432
|
+
) -> bool:
|
|
433
|
+
r"""Create a new Python virtual environment, optionally using uv."""
|
|
434
|
+
try:
|
|
435
|
+
if os.path.exists(env_path):
|
|
436
|
+
if update_callback:
|
|
437
|
+
update_callback(f"Using existing environment: {env_path}\n")
|
|
438
|
+
return True
|
|
439
|
+
|
|
440
|
+
if update_callback:
|
|
441
|
+
update_callback(
|
|
442
|
+
f"Creating new Python environment at: {env_path}\n"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Try to use uv if available
|
|
446
|
+
success, uv_path = ensure_uv_available(update_callback)
|
|
447
|
+
if success and uv_path:
|
|
448
|
+
# Get current Python version
|
|
449
|
+
current_version = (
|
|
450
|
+
f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
subprocess.run(
|
|
454
|
+
[uv_path, "venv", "--python", current_version, env_path],
|
|
455
|
+
check=True,
|
|
456
|
+
capture_output=True,
|
|
457
|
+
cwd=working_dir,
|
|
458
|
+
timeout=300,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Get the python path from the new environment
|
|
462
|
+
if platform.system() == 'Windows':
|
|
463
|
+
python_path = os.path.join(env_path, "Scripts", "python.exe")
|
|
464
|
+
else:
|
|
465
|
+
python_path = os.path.join(env_path, "bin", "python")
|
|
466
|
+
|
|
467
|
+
# Install pip and setuptools using uv
|
|
468
|
+
subprocess.run(
|
|
469
|
+
[
|
|
470
|
+
uv_path,
|
|
471
|
+
"pip",
|
|
472
|
+
"install",
|
|
473
|
+
"--python",
|
|
474
|
+
python_path,
|
|
475
|
+
"pip",
|
|
476
|
+
"setuptools",
|
|
477
|
+
"wheel",
|
|
478
|
+
],
|
|
479
|
+
check=True,
|
|
480
|
+
capture_output=True,
|
|
481
|
+
cwd=working_dir,
|
|
482
|
+
timeout=300,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if update_callback:
|
|
486
|
+
update_callback(
|
|
487
|
+
"[UV] Cloned Python environment created successfully!\n"
|
|
488
|
+
)
|
|
489
|
+
return True
|
|
490
|
+
else:
|
|
491
|
+
# Fallback to standard venv
|
|
492
|
+
if update_callback:
|
|
493
|
+
update_callback(
|
|
494
|
+
"Falling back to standard venv for cloning environment\n"
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
venv.create(env_path, with_pip=True, symlinks=False)
|
|
498
|
+
|
|
499
|
+
# Ensure pip is properly available
|
|
500
|
+
if platform.system() == 'Windows':
|
|
501
|
+
python_path = os.path.join(env_path, "Scripts", "python.exe")
|
|
502
|
+
else:
|
|
503
|
+
python_path = os.path.join(env_path, "bin", "python")
|
|
504
|
+
|
|
505
|
+
if os.path.exists(python_path):
|
|
506
|
+
subprocess.run(
|
|
507
|
+
[python_path, "-m", "pip", "install", "--upgrade", "pip"],
|
|
508
|
+
check=True,
|
|
509
|
+
capture_output=True,
|
|
510
|
+
cwd=working_dir,
|
|
511
|
+
timeout=60,
|
|
512
|
+
)
|
|
513
|
+
if update_callback:
|
|
514
|
+
update_callback(
|
|
515
|
+
"New Python environment created successfully with pip!"
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
if update_callback:
|
|
519
|
+
update_callback(
|
|
520
|
+
f"Warning: Python executable not found at "
|
|
521
|
+
f"{python_path}"
|
|
522
|
+
)
|
|
523
|
+
return True
|
|
524
|
+
|
|
525
|
+
except subprocess.CalledProcessError as e:
|
|
526
|
+
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
527
|
+
if update_callback:
|
|
528
|
+
update_callback(f"Failed to create environment: {error_msg}\n")
|
|
529
|
+
logger.error(f"Failed to create environment: {error_msg}")
|
|
530
|
+
return False
|
|
531
|
+
except subprocess.TimeoutExpired:
|
|
532
|
+
if update_callback:
|
|
533
|
+
update_callback("Environment creation timed out\n")
|
|
534
|
+
return False
|
|
535
|
+
except Exception as e:
|
|
536
|
+
if update_callback:
|
|
537
|
+
update_callback(f"Failed to create environment: {e!s}\n")
|
|
538
|
+
logger.error(f"Failed to create environment: {e}")
|
|
539
|
+
return False
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def check_nodejs_availability(update_callback=None) -> Tuple[bool, str]:
|
|
543
|
+
r"""Check if Node.js is available without modifying the system."""
|
|
544
|
+
try:
|
|
545
|
+
# Check if Node.js is already available in the system
|
|
546
|
+
node_result = subprocess.run(
|
|
547
|
+
["node", "--version"],
|
|
548
|
+
check=False,
|
|
549
|
+
capture_output=True,
|
|
550
|
+
timeout=10,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
npm_result = subprocess.run(
|
|
554
|
+
["npm", "--version"],
|
|
555
|
+
check=False,
|
|
556
|
+
capture_output=True,
|
|
557
|
+
timeout=10,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
if node_result.returncode == 0 and npm_result.returncode == 0:
|
|
561
|
+
node_version = node_result.stdout.decode().strip()
|
|
562
|
+
npm_version = npm_result.stdout.decode().strip()
|
|
563
|
+
info = (
|
|
564
|
+
f"Node.js {node_version} and npm {npm_version} are available"
|
|
565
|
+
)
|
|
566
|
+
if update_callback:
|
|
567
|
+
update_callback(f"{info}\n")
|
|
568
|
+
return True, info
|
|
569
|
+
else:
|
|
570
|
+
info = "Node.js not found. If needed, please install it manually."
|
|
571
|
+
if update_callback:
|
|
572
|
+
update_callback(f"Note: {info}\n")
|
|
573
|
+
return False, info
|
|
574
|
+
|
|
575
|
+
except Exception as e:
|
|
576
|
+
info = f"Could not check Node.js availability - {e}"
|
|
577
|
+
if update_callback:
|
|
578
|
+
update_callback(f"Note: {info}.\n")
|
|
579
|
+
logger.warning(f"Failed to check Node.js: {e}")
|
|
580
|
+
return False, info
|