camel-ai 0.2.72a10__py3-none-any.whl → 0.2.73__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 +140 -345
- camel/memories/agent_memories.py +18 -17
- camel/societies/__init__.py +2 -0
- camel/societies/workforce/prompts.py +36 -10
- camel/societies/workforce/single_agent_worker.py +7 -5
- camel/societies/workforce/workforce.py +6 -4
- camel/storages/key_value_storages/mem0_cloud.py +48 -47
- camel/storages/vectordb_storages/__init__.py +1 -0
- camel/storages/vectordb_storages/surreal.py +100 -150
- camel/toolkits/__init__.py +6 -1
- camel/toolkits/base.py +60 -2
- camel/toolkits/excel_toolkit.py +153 -64
- camel/toolkits/file_write_toolkit.py +67 -0
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +136 -413
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +131 -1966
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1177 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4356 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +945 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +522 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +110 -0
- camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +26 -0
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +254 -0
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +582 -0
- camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
- camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +447 -0
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2077 -0
- camel/toolkits/mcp_toolkit.py +341 -46
- camel/toolkits/message_integration.py +719 -0
- camel/toolkits/notion_mcp_toolkit.py +234 -0
- camel/toolkits/screenshot_toolkit.py +116 -31
- camel/toolkits/search_toolkit.py +20 -2
- camel/toolkits/slack_toolkit.py +43 -48
- camel/toolkits/terminal_toolkit.py +288 -46
- camel/toolkits/video_analysis_toolkit.py +13 -13
- camel/toolkits/video_download_toolkit.py +11 -11
- camel/toolkits/web_deploy_toolkit.py +207 -12
- camel/types/enums.py +6 -0
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73.dist-info}/METADATA +49 -9
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73.dist-info}/RECORD +52 -35
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/actions.py +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/agent.py +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/browser_session.py +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/snapshot.py +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/stealth_script.js +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/unified_analyzer.js +0 -0
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73.dist-info}/licenses/LICENSE +0 -0
|
@@ -17,6 +17,7 @@ import os
|
|
|
17
17
|
import platform
|
|
18
18
|
import queue
|
|
19
19
|
import shutil
|
|
20
|
+
import signal
|
|
20
21
|
import subprocess
|
|
21
22
|
import sys
|
|
22
23
|
import threading
|
|
@@ -42,7 +43,7 @@ class TerminalToolkit(BaseToolkit):
|
|
|
42
43
|
|
|
43
44
|
Args:
|
|
44
45
|
timeout (Optional[float]): The timeout for terminal operations.
|
|
45
|
-
(default: :obj:`
|
|
46
|
+
(default: :obj:`20.0`)
|
|
46
47
|
shell_sessions (Optional[Dict[str, Any]]): A dictionary to store
|
|
47
48
|
shell session information. If :obj:`None`, an empty dictionary
|
|
48
49
|
will be used. (default: :obj:`None`)
|
|
@@ -61,6 +62,10 @@ class TerminalToolkit(BaseToolkit):
|
|
|
61
62
|
environment. (default: :obj:`False`)
|
|
62
63
|
safe_mode (bool): Whether to enable safe mode to restrict
|
|
63
64
|
operations. (default: :obj:`True`)
|
|
65
|
+
interactive (bool): Whether to use interactive mode for shell commands,
|
|
66
|
+
connecting them to the terminal's standard input. This is useful
|
|
67
|
+
for commands that require user input, like `ssh`. Interactive mode
|
|
68
|
+
is only supported on macOS and Linux. (default: :obj:`False`)
|
|
64
69
|
|
|
65
70
|
Note:
|
|
66
71
|
Most functions are compatible with Unix-based systems (macOS, Linux).
|
|
@@ -70,14 +75,17 @@ class TerminalToolkit(BaseToolkit):
|
|
|
70
75
|
|
|
71
76
|
def __init__(
|
|
72
77
|
self,
|
|
73
|
-
timeout: Optional[float] =
|
|
78
|
+
timeout: Optional[float] = 20.0,
|
|
74
79
|
shell_sessions: Optional[Dict[str, Any]] = None,
|
|
75
80
|
working_directory: Optional[str] = None,
|
|
76
81
|
need_terminal: bool = True,
|
|
77
82
|
use_shell_mode: bool = True,
|
|
78
83
|
clone_current_env: bool = False,
|
|
79
84
|
safe_mode: bool = True,
|
|
85
|
+
interactive: bool = False,
|
|
80
86
|
):
|
|
87
|
+
# Store timeout before calling super().__init__
|
|
88
|
+
self._timeout = timeout
|
|
81
89
|
super().__init__(timeout=timeout)
|
|
82
90
|
self.shell_sessions = shell_sessions or {}
|
|
83
91
|
self.os_type = platform.system()
|
|
@@ -90,11 +98,13 @@ class TerminalToolkit(BaseToolkit):
|
|
|
90
98
|
self.cloned_env_path = None
|
|
91
99
|
self.use_shell_mode = use_shell_mode
|
|
92
100
|
self._human_takeover_active = False
|
|
101
|
+
self.interactive = interactive
|
|
93
102
|
|
|
94
103
|
self.python_executable = sys.executable
|
|
95
104
|
self.is_macos = platform.system() == 'Darwin'
|
|
96
105
|
self.initial_env_path: Optional[str] = None
|
|
97
106
|
self.initial_env_prepared = False
|
|
107
|
+
self.uv_path: Optional[str] = None
|
|
98
108
|
|
|
99
109
|
atexit.register(self.__del__)
|
|
100
110
|
|
|
@@ -197,37 +207,111 @@ class TerminalToolkit(BaseToolkit):
|
|
|
197
207
|
f"Creating new Python environment at: {self.cloned_env_path}\n"
|
|
198
208
|
)
|
|
199
209
|
|
|
200
|
-
#
|
|
201
|
-
|
|
210
|
+
# Try to use uv if available
|
|
211
|
+
if self._ensure_uv_available():
|
|
212
|
+
# Use uv to create environment with current Python version
|
|
213
|
+
uv_command = self.uv_path if self.uv_path else "uv"
|
|
202
214
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
self.cloned_env_path, "Scripts", "python.exe"
|
|
215
|
+
# Get current Python version
|
|
216
|
+
current_version = (
|
|
217
|
+
f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
207
218
|
)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
219
|
+
|
|
220
|
+
subprocess.run(
|
|
221
|
+
[
|
|
222
|
+
uv_command,
|
|
223
|
+
"venv",
|
|
224
|
+
"--python",
|
|
225
|
+
current_version,
|
|
226
|
+
self.cloned_env_path,
|
|
227
|
+
],
|
|
228
|
+
check=True,
|
|
229
|
+
capture_output=True,
|
|
230
|
+
cwd=self.working_dir,
|
|
231
|
+
timeout=300,
|
|
211
232
|
)
|
|
212
233
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
234
|
+
# Get the python path from the new environment
|
|
235
|
+
if self.os_type == 'Windows':
|
|
236
|
+
python_path = os.path.join(
|
|
237
|
+
self.cloned_env_path, "Scripts", "python.exe"
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
python_path = os.path.join(
|
|
241
|
+
self.cloned_env_path, "bin", "python"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Install pip and setuptools using uv
|
|
216
245
|
subprocess.run(
|
|
217
|
-
[
|
|
246
|
+
[
|
|
247
|
+
uv_command,
|
|
248
|
+
"pip",
|
|
249
|
+
"install",
|
|
250
|
+
"--python",
|
|
251
|
+
python_path,
|
|
252
|
+
"pip",
|
|
253
|
+
"setuptools",
|
|
254
|
+
"wheel",
|
|
255
|
+
],
|
|
218
256
|
check=True,
|
|
219
257
|
capture_output=True,
|
|
220
258
|
cwd=self.working_dir,
|
|
221
|
-
timeout=
|
|
259
|
+
timeout=300,
|
|
222
260
|
)
|
|
261
|
+
|
|
223
262
|
self._update_terminal_output(
|
|
224
|
-
"
|
|
263
|
+
"[UV] Cloned Python environment created successfully!\n"
|
|
225
264
|
)
|
|
265
|
+
|
|
226
266
|
else:
|
|
267
|
+
# Fallback to standard venv
|
|
227
268
|
self._update_terminal_output(
|
|
228
|
-
|
|
269
|
+
"Falling back to standard venv for cloning environment\n"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Create virtual environment with pip. On macOS, use
|
|
273
|
+
# symlinks=False to avoid dyld library loading issues
|
|
274
|
+
venv.create(
|
|
275
|
+
self.cloned_env_path, with_pip=True, symlinks=False
|
|
229
276
|
)
|
|
230
277
|
|
|
278
|
+
# Ensure pip is properly available by upgrading it
|
|
279
|
+
if self.os_type == 'Windows':
|
|
280
|
+
python_path = os.path.join(
|
|
281
|
+
self.cloned_env_path, "Scripts", "python.exe"
|
|
282
|
+
)
|
|
283
|
+
else:
|
|
284
|
+
python_path = os.path.join(
|
|
285
|
+
self.cloned_env_path, "bin", "python"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Verify python executable exists
|
|
289
|
+
if os.path.exists(python_path):
|
|
290
|
+
# Use python -m pip to ensure pip is available
|
|
291
|
+
subprocess.run(
|
|
292
|
+
[
|
|
293
|
+
python_path,
|
|
294
|
+
"-m",
|
|
295
|
+
"pip",
|
|
296
|
+
"install",
|
|
297
|
+
"--upgrade",
|
|
298
|
+
"pip",
|
|
299
|
+
],
|
|
300
|
+
check=True,
|
|
301
|
+
capture_output=True,
|
|
302
|
+
cwd=self.working_dir,
|
|
303
|
+
timeout=60,
|
|
304
|
+
)
|
|
305
|
+
self._update_terminal_output(
|
|
306
|
+
"New Python environment created successfully "
|
|
307
|
+
"with pip!\n"
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
self._update_terminal_output(
|
|
311
|
+
f"Warning: Python executable not found "
|
|
312
|
+
f"at {python_path}\n"
|
|
313
|
+
)
|
|
314
|
+
|
|
231
315
|
except subprocess.CalledProcessError as e:
|
|
232
316
|
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
233
317
|
self._update_terminal_output(
|
|
@@ -252,6 +336,97 @@ class TerminalToolkit(BaseToolkit):
|
|
|
252
336
|
or shutil.which("uv") is not None
|
|
253
337
|
)
|
|
254
338
|
|
|
339
|
+
def _ensure_uv_available(self) -> bool:
|
|
340
|
+
r"""Ensure uv is available, installing it if necessary.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
bool: True if uv is available (either already installed or
|
|
344
|
+
successfully installed), False otherwise.
|
|
345
|
+
"""
|
|
346
|
+
# Check if uv is already available
|
|
347
|
+
existing_uv = shutil.which("uv")
|
|
348
|
+
if existing_uv is not None:
|
|
349
|
+
self.uv_path = existing_uv
|
|
350
|
+
self._update_terminal_output(
|
|
351
|
+
f"uv is already available at: {self.uv_path}\n"
|
|
352
|
+
)
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
self._update_terminal_output("uv not found, installing...\n")
|
|
357
|
+
|
|
358
|
+
# Install uv using the official installer script
|
|
359
|
+
if self.os_type in ['Darwin', 'Linux']:
|
|
360
|
+
# Use curl to download and execute the installer
|
|
361
|
+
install_cmd = "curl -LsSf https://astral.sh/uv/install.sh | sh"
|
|
362
|
+
result = subprocess.run(
|
|
363
|
+
install_cmd,
|
|
364
|
+
shell=True,
|
|
365
|
+
capture_output=True,
|
|
366
|
+
text=True,
|
|
367
|
+
timeout=60,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
if result.returncode != 0:
|
|
371
|
+
self._update_terminal_output(
|
|
372
|
+
f"Failed to install uv: {result.stderr}\n"
|
|
373
|
+
)
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
# Check if uv was installed in the expected location
|
|
377
|
+
home = os.path.expanduser("~")
|
|
378
|
+
uv_bin_path = os.path.join(home, ".cargo", "bin")
|
|
379
|
+
uv_executable = os.path.join(uv_bin_path, "uv")
|
|
380
|
+
|
|
381
|
+
if os.path.exists(uv_executable):
|
|
382
|
+
# Store the full path to uv instead of modifying PATH
|
|
383
|
+
self.uv_path = uv_executable
|
|
384
|
+
self._update_terminal_output(
|
|
385
|
+
f"uv installed successfully at: {self.uv_path}\n"
|
|
386
|
+
)
|
|
387
|
+
return True
|
|
388
|
+
|
|
389
|
+
elif self.os_type == 'Windows':
|
|
390
|
+
# Use PowerShell to install uv on Windows
|
|
391
|
+
install_cmd = (
|
|
392
|
+
"powershell -ExecutionPolicy Bypass -c "
|
|
393
|
+
"\"irm https://astral.sh/uv/install.ps1 | iex\""
|
|
394
|
+
)
|
|
395
|
+
result = subprocess.run(
|
|
396
|
+
install_cmd,
|
|
397
|
+
shell=True,
|
|
398
|
+
capture_output=True,
|
|
399
|
+
text=True,
|
|
400
|
+
timeout=60,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if result.returncode != 0:
|
|
404
|
+
self._update_terminal_output(
|
|
405
|
+
f"Failed to install uv: {result.stderr}\n"
|
|
406
|
+
)
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
# Check if uv was installed in the expected location on Windows
|
|
410
|
+
home = os.path.expanduser("~")
|
|
411
|
+
uv_bin_path = os.path.join(home, ".cargo", "bin")
|
|
412
|
+
uv_executable = os.path.join(uv_bin_path, "uv.exe")
|
|
413
|
+
|
|
414
|
+
if os.path.exists(uv_executable):
|
|
415
|
+
# Store the full path to uv instead of modifying PATH
|
|
416
|
+
self.uv_path = uv_executable
|
|
417
|
+
self._update_terminal_output(
|
|
418
|
+
f"uv installed successfully at: {self.uv_path}\n"
|
|
419
|
+
)
|
|
420
|
+
return True
|
|
421
|
+
|
|
422
|
+
self._update_terminal_output("Failed to verify uv installation\n")
|
|
423
|
+
return False
|
|
424
|
+
|
|
425
|
+
except Exception as e:
|
|
426
|
+
self._update_terminal_output(f"Error installing uv: {e!s}\n")
|
|
427
|
+
logger.error(f"Failed to install uv: {e}")
|
|
428
|
+
return False
|
|
429
|
+
|
|
255
430
|
def _prepare_initial_environment(self):
|
|
256
431
|
r"""Prepare initial environment with Python 3.10, pip, and other
|
|
257
432
|
essential tools.
|
|
@@ -276,10 +451,14 @@ class TerminalToolkit(BaseToolkit):
|
|
|
276
451
|
# Create the initial environment directory
|
|
277
452
|
os.makedirs(self.initial_env_path, exist_ok=True)
|
|
278
453
|
|
|
279
|
-
#
|
|
280
|
-
if self.
|
|
454
|
+
# Try to ensure uv is available and use it preferentially
|
|
455
|
+
if self._ensure_uv_available():
|
|
281
456
|
self._setup_initial_env_with_uv()
|
|
282
457
|
else:
|
|
458
|
+
# Fallback to venv if uv installation failed
|
|
459
|
+
self._update_terminal_output(
|
|
460
|
+
"Falling back to standard venv for environment setup\n"
|
|
461
|
+
)
|
|
283
462
|
self._setup_initial_env_with_venv()
|
|
284
463
|
|
|
285
464
|
self.initial_env_prepared = True
|
|
@@ -299,9 +478,18 @@ class TerminalToolkit(BaseToolkit):
|
|
|
299
478
|
raise Exception("Initial environment path not set")
|
|
300
479
|
|
|
301
480
|
try:
|
|
481
|
+
# Use the stored uv path if available, otherwise fall back to "uv"
|
|
482
|
+
uv_command = self.uv_path if self.uv_path else "uv"
|
|
483
|
+
|
|
302
484
|
# Create virtual environment with Python 3.10 using uv
|
|
303
485
|
subprocess.run(
|
|
304
|
-
[
|
|
486
|
+
[
|
|
487
|
+
uv_command,
|
|
488
|
+
"venv",
|
|
489
|
+
"--python",
|
|
490
|
+
"3.10",
|
|
491
|
+
self.initial_env_path,
|
|
492
|
+
],
|
|
305
493
|
check=True,
|
|
306
494
|
capture_output=True,
|
|
307
495
|
cwd=self.working_dir,
|
|
@@ -319,10 +507,17 @@ class TerminalToolkit(BaseToolkit):
|
|
|
319
507
|
)
|
|
320
508
|
|
|
321
509
|
# Install essential packages using uv
|
|
322
|
-
essential_packages = [
|
|
510
|
+
essential_packages = [
|
|
511
|
+
"pip",
|
|
512
|
+
"setuptools",
|
|
513
|
+
"wheel",
|
|
514
|
+
"pyautogui",
|
|
515
|
+
"plotly",
|
|
516
|
+
"ffmpeg",
|
|
517
|
+
]
|
|
323
518
|
subprocess.run(
|
|
324
519
|
[
|
|
325
|
-
|
|
520
|
+
uv_command,
|
|
326
521
|
"pip",
|
|
327
522
|
"install",
|
|
328
523
|
"--python",
|
|
@@ -356,10 +551,12 @@ class TerminalToolkit(BaseToolkit):
|
|
|
356
551
|
|
|
357
552
|
try:
|
|
358
553
|
# Create virtual environment with system Python
|
|
554
|
+
# On macOS, use symlinks=False to avoid dyld library loading issues
|
|
359
555
|
venv.create(
|
|
360
556
|
self.initial_env_path,
|
|
361
557
|
with_pip=True,
|
|
362
558
|
system_site_packages=False,
|
|
559
|
+
symlinks=False,
|
|
363
560
|
)
|
|
364
561
|
|
|
365
562
|
# Get pip path
|
|
@@ -371,7 +568,14 @@ class TerminalToolkit(BaseToolkit):
|
|
|
371
568
|
pip_path = os.path.join(self.initial_env_path, "bin", "pip")
|
|
372
569
|
|
|
373
570
|
# Upgrade pip and install essential packages
|
|
374
|
-
essential_packages = [
|
|
571
|
+
essential_packages = [
|
|
572
|
+
"pip",
|
|
573
|
+
"setuptools",
|
|
574
|
+
"wheel",
|
|
575
|
+
"pyautogui",
|
|
576
|
+
"plotly",
|
|
577
|
+
"ffmpeg",
|
|
578
|
+
]
|
|
375
579
|
subprocess.run(
|
|
376
580
|
[pip_path, "install", "--upgrade", *essential_packages],
|
|
377
581
|
check=True,
|
|
@@ -804,26 +1008,18 @@ class TerminalToolkit(BaseToolkit):
|
|
|
804
1008
|
|
|
805
1009
|
return True, command
|
|
806
1010
|
|
|
807
|
-
def shell_exec(
|
|
808
|
-
self, id: str, command: str, interactive: bool = False
|
|
809
|
-
) -> str:
|
|
1011
|
+
def shell_exec(self, id: str, command: str) -> str:
|
|
810
1012
|
r"""Executes a shell command in a specified session.
|
|
811
1013
|
|
|
812
1014
|
This function creates and manages shell sessions to execute commands,
|
|
813
|
-
simulating a real terminal.
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
created.
|
|
1015
|
+
simulating a real terminal. The behavior depends on the toolkit's
|
|
1016
|
+
interactive mode setting. Each session is identified by a unique ID.
|
|
1017
|
+
If a session with the given ID does not exist, it will be created.
|
|
817
1018
|
|
|
818
1019
|
Args:
|
|
819
1020
|
id (str): A unique identifier for the shell session. This is used
|
|
820
1021
|
to manage multiple concurrent shell processes.
|
|
821
1022
|
command (str): The shell command to be executed.
|
|
822
|
-
interactive (bool, optional): If `True`, the command runs in
|
|
823
|
-
interactive mode, connecting it to the terminal's standard
|
|
824
|
-
input. This is useful for commands that require user input,
|
|
825
|
-
like `ssh`. Defaults to `False`. Interactive mode is only
|
|
826
|
-
supported on macOS and Linux. (default: :obj:`False`)
|
|
827
1023
|
|
|
828
1024
|
Returns:
|
|
829
1025
|
str: The standard output and standard error from the command. If an
|
|
@@ -831,8 +1027,8 @@ class TerminalToolkit(BaseToolkit):
|
|
|
831
1027
|
returned.
|
|
832
1028
|
|
|
833
1029
|
Note:
|
|
834
|
-
When
|
|
835
|
-
|
|
1030
|
+
When the toolkit is initialized with interactive mode, commands may
|
|
1031
|
+
block if they require input. In safe mode, some commands that are
|
|
836
1032
|
considered dangerous are restricted.
|
|
837
1033
|
"""
|
|
838
1034
|
error_msg = self._enforce_working_dir_for_execution(self.working_dir)
|
|
@@ -929,7 +1125,12 @@ class TerminalToolkit(BaseToolkit):
|
|
|
929
1125
|
elif pip_path and command.startswith('pip'):
|
|
930
1126
|
command = command.replace('pip', pip_path, 1)
|
|
931
1127
|
|
|
932
|
-
if not interactive:
|
|
1128
|
+
if not self.interactive:
|
|
1129
|
+
# Use preexec_fn to create a new process group on Unix systems
|
|
1130
|
+
preexec_fn = None
|
|
1131
|
+
if self.os_type in ['Darwin', 'Linux']:
|
|
1132
|
+
preexec_fn = os.setsid
|
|
1133
|
+
|
|
933
1134
|
proc = subprocess.Popen(
|
|
934
1135
|
command,
|
|
935
1136
|
shell=True,
|
|
@@ -941,17 +1142,55 @@ class TerminalToolkit(BaseToolkit):
|
|
|
941
1142
|
bufsize=1,
|
|
942
1143
|
universal_newlines=True,
|
|
943
1144
|
env=os.environ.copy(),
|
|
1145
|
+
preexec_fn=preexec_fn,
|
|
944
1146
|
)
|
|
945
1147
|
|
|
946
1148
|
self.shell_sessions[id]["process"] = proc
|
|
947
1149
|
self.shell_sessions[id]["running"] = True
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
output
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1150
|
+
try:
|
|
1151
|
+
# Use the instance timeout if available
|
|
1152
|
+
stdout, stderr = proc.communicate(timeout=self.timeout)
|
|
1153
|
+
output = stdout or ""
|
|
1154
|
+
if stderr:
|
|
1155
|
+
output += f"\nStderr Output:\n{stderr}"
|
|
1156
|
+
self.shell_sessions[id]["output"] = output
|
|
1157
|
+
self.shell_sessions[id]["running"] = False
|
|
1158
|
+
self._update_terminal_output(output + "\n")
|
|
1159
|
+
return output
|
|
1160
|
+
except subprocess.TimeoutExpired:
|
|
1161
|
+
# Kill the entire process group on Unix systems
|
|
1162
|
+
if self.os_type in ['Darwin', 'Linux']:
|
|
1163
|
+
try:
|
|
1164
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
|
1165
|
+
# Give it a short time to terminate
|
|
1166
|
+
stdout, stderr = proc.communicate(timeout=1)
|
|
1167
|
+
except subprocess.TimeoutExpired:
|
|
1168
|
+
# Force kill the process group
|
|
1169
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
1170
|
+
stdout, stderr = proc.communicate()
|
|
1171
|
+
except ProcessLookupError:
|
|
1172
|
+
# Process already dead
|
|
1173
|
+
stdout, stderr = proc.communicate()
|
|
1174
|
+
else:
|
|
1175
|
+
# Windows fallback
|
|
1176
|
+
proc.terminate()
|
|
1177
|
+
try:
|
|
1178
|
+
stdout, stderr = proc.communicate(timeout=1)
|
|
1179
|
+
except subprocess.TimeoutExpired:
|
|
1180
|
+
proc.kill()
|
|
1181
|
+
stdout, stderr = proc.communicate()
|
|
1182
|
+
|
|
1183
|
+
output = stdout or ""
|
|
1184
|
+
if stderr:
|
|
1185
|
+
output += f"\nStderr Output:\n{stderr}"
|
|
1186
|
+
error_msg = (
|
|
1187
|
+
f"\nCommand timed out after {self.timeout} seconds"
|
|
1188
|
+
)
|
|
1189
|
+
output += error_msg
|
|
1190
|
+
self.shell_sessions[id]["output"] = output
|
|
1191
|
+
self.shell_sessions[id]["running"] = False
|
|
1192
|
+
self._update_terminal_output(output + "\n")
|
|
1193
|
+
return output
|
|
955
1194
|
|
|
956
1195
|
# Interactive mode with real-time streaming via PTY
|
|
957
1196
|
if self.os_type not in ['Darwin', 'Linux']:
|
|
@@ -1061,6 +1300,9 @@ class TerminalToolkit(BaseToolkit):
|
|
|
1061
1300
|
f"Detailed information: {detailed_error}"
|
|
1062
1301
|
)
|
|
1063
1302
|
|
|
1303
|
+
# Mark shell_exec to skip automatic timeout wrapping
|
|
1304
|
+
shell_exec._manual_timeout = True # type: ignore[attr-defined]
|
|
1305
|
+
|
|
1064
1306
|
def shell_view(self, id: str) -> str:
|
|
1065
1307
|
r"""View the full output history of a specified shell session.
|
|
1066
1308
|
|
|
@@ -97,7 +97,7 @@ class VideoAnalysisToolkit(BaseToolkit):
|
|
|
97
97
|
r"""A class for analysing videos with vision-language model.
|
|
98
98
|
|
|
99
99
|
Args:
|
|
100
|
-
|
|
100
|
+
working_directory (Optional[str], optional): The directory where the
|
|
101
101
|
video will be downloaded to. If not provided, video will be stored
|
|
102
102
|
in a temporary directory and will be cleaned up after use.
|
|
103
103
|
(default: :obj:`None`)
|
|
@@ -123,7 +123,7 @@ class VideoAnalysisToolkit(BaseToolkit):
|
|
|
123
123
|
@dependencies_required("ffmpeg", "scenedetect")
|
|
124
124
|
def __init__(
|
|
125
125
|
self,
|
|
126
|
-
|
|
126
|
+
working_directory: Optional[str] = None,
|
|
127
127
|
model: Optional[BaseModelBackend] = None,
|
|
128
128
|
use_audio_transcription: bool = False,
|
|
129
129
|
use_ocr: bool = False,
|
|
@@ -133,30 +133,30 @@ class VideoAnalysisToolkit(BaseToolkit):
|
|
|
133
133
|
timeout: Optional[float] = None,
|
|
134
134
|
) -> None:
|
|
135
135
|
super().__init__(timeout=timeout)
|
|
136
|
-
self._cleanup =
|
|
136
|
+
self._cleanup = working_directory is None
|
|
137
137
|
self._temp_files: list[str] = [] # Track temporary files for cleanup
|
|
138
138
|
self._use_audio_transcription = use_audio_transcription
|
|
139
139
|
self._use_ocr = use_ocr
|
|
140
140
|
self.output_language = output_language
|
|
141
141
|
self.frame_interval = frame_interval
|
|
142
142
|
|
|
143
|
-
self.
|
|
144
|
-
|
|
143
|
+
self._working_directory = Path(
|
|
144
|
+
working_directory or tempfile.mkdtemp()
|
|
145
145
|
).resolve()
|
|
146
146
|
|
|
147
147
|
self.video_downloader_toolkit = VideoDownloaderToolkit(
|
|
148
|
-
|
|
148
|
+
working_directory=str(self._working_directory),
|
|
149
149
|
cookies_path=cookies_path,
|
|
150
150
|
)
|
|
151
151
|
|
|
152
152
|
try:
|
|
153
|
-
self.
|
|
153
|
+
self._working_directory.mkdir(parents=True, exist_ok=True)
|
|
154
154
|
except OSError as e:
|
|
155
155
|
raise ValueError(
|
|
156
|
-
f"Error creating directory {self.
|
|
156
|
+
f"Error creating directory {self._working_directory}: {e}"
|
|
157
157
|
)
|
|
158
158
|
|
|
159
|
-
logger.info(f"Video will be downloaded to {self.
|
|
159
|
+
logger.info(f"Video will be downloaded to {self._working_directory}")
|
|
160
160
|
|
|
161
161
|
self.vl_model = model
|
|
162
162
|
# Ensure ChatAgent is initialized with a model if provided
|
|
@@ -206,16 +206,16 @@ class VideoAnalysisToolkit(BaseToolkit):
|
|
|
206
206
|
)
|
|
207
207
|
|
|
208
208
|
# Clean up temporary directory if needed
|
|
209
|
-
if self._cleanup and os.path.exists(self.
|
|
209
|
+
if self._cleanup and os.path.exists(self._working_directory):
|
|
210
210
|
try:
|
|
211
211
|
import sys
|
|
212
212
|
|
|
213
213
|
if getattr(sys, 'modules', None) is not None:
|
|
214
214
|
import shutil
|
|
215
215
|
|
|
216
|
-
shutil.rmtree(self.
|
|
216
|
+
shutil.rmtree(self._working_directory)
|
|
217
217
|
logger.debug(
|
|
218
|
-
f"Removed temp directory: {self.
|
|
218
|
+
f"Removed temp directory: {self._working_directory}"
|
|
219
219
|
)
|
|
220
220
|
except (ImportError, AttributeError):
|
|
221
221
|
# Skip cleanup if interpreter is shutting down
|
|
@@ -223,7 +223,7 @@ class VideoAnalysisToolkit(BaseToolkit):
|
|
|
223
223
|
except OSError as e:
|
|
224
224
|
logger.warning(
|
|
225
225
|
f"Failed to remove temporary directory "
|
|
226
|
-
f"{self.
|
|
226
|
+
f"{self._working_directory}: {e}"
|
|
227
227
|
)
|
|
228
228
|
|
|
229
229
|
@dependencies_required("pytesseract", "cv2", "numpy")
|
|
@@ -62,7 +62,7 @@ class VideoDownloaderToolkit(BaseToolkit):
|
|
|
62
62
|
chunks.
|
|
63
63
|
|
|
64
64
|
Args:
|
|
65
|
-
|
|
65
|
+
working_directory (Optional[str], optional): The directory where the
|
|
66
66
|
video will be downloaded to. If not provided, video will be stored
|
|
67
67
|
in a temporary directory and will be cleaned up after use.
|
|
68
68
|
(default: :obj:`None`)
|
|
@@ -73,30 +73,30 @@ class VideoDownloaderToolkit(BaseToolkit):
|
|
|
73
73
|
@dependencies_required("yt_dlp", "ffmpeg")
|
|
74
74
|
def __init__(
|
|
75
75
|
self,
|
|
76
|
-
|
|
76
|
+
working_directory: Optional[str] = None,
|
|
77
77
|
cookies_path: Optional[str] = None,
|
|
78
78
|
timeout: Optional[float] = None,
|
|
79
79
|
) -> None:
|
|
80
80
|
super().__init__(timeout=timeout)
|
|
81
|
-
self._cleanup =
|
|
81
|
+
self._cleanup = working_directory is None
|
|
82
82
|
self._cookies_path = cookies_path
|
|
83
83
|
|
|
84
|
-
self.
|
|
85
|
-
|
|
84
|
+
self._working_directory = Path(
|
|
85
|
+
working_directory or tempfile.mkdtemp()
|
|
86
86
|
).resolve()
|
|
87
87
|
|
|
88
88
|
try:
|
|
89
|
-
self.
|
|
89
|
+
self._working_directory.mkdir(parents=True, exist_ok=True)
|
|
90
90
|
except FileExistsError:
|
|
91
91
|
raise ValueError(
|
|
92
|
-
f"{self.
|
|
92
|
+
f"{self._working_directory} is not a valid directory."
|
|
93
93
|
)
|
|
94
94
|
except OSError as e:
|
|
95
95
|
raise ValueError(
|
|
96
|
-
f"Error creating directory {self.
|
|
96
|
+
f"Error creating directory {self._working_directory}: {e}"
|
|
97
97
|
)
|
|
98
98
|
|
|
99
|
-
logger.info(f"Video will be downloaded to {self.
|
|
99
|
+
logger.info(f"Video will be downloaded to {self._working_directory}")
|
|
100
100
|
|
|
101
101
|
def __del__(self) -> None:
|
|
102
102
|
r"""Deconstructor for the VideoDownloaderToolkit class.
|
|
@@ -111,7 +111,7 @@ class VideoDownloaderToolkit(BaseToolkit):
|
|
|
111
111
|
if getattr(sys, 'modules', None) is not None:
|
|
112
112
|
import shutil
|
|
113
113
|
|
|
114
|
-
shutil.rmtree(self.
|
|
114
|
+
shutil.rmtree(self._working_directory, ignore_errors=True)
|
|
115
115
|
except (ImportError, AttributeError):
|
|
116
116
|
# Skip cleanup if interpreter is shutting down
|
|
117
117
|
pass
|
|
@@ -130,7 +130,7 @@ class VideoDownloaderToolkit(BaseToolkit):
|
|
|
130
130
|
"""
|
|
131
131
|
import yt_dlp
|
|
132
132
|
|
|
133
|
-
video_template = self.
|
|
133
|
+
video_template = self._working_directory / "%(title)s.%(ext)s"
|
|
134
134
|
ydl_opts = {
|
|
135
135
|
'format': 'bestvideo+bestaudio/best',
|
|
136
136
|
'outtmpl': str(video_template),
|