camel-ai 0.2.72a8__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.

Files changed (53) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +140 -345
  3. camel/memories/agent_memories.py +18 -17
  4. camel/societies/__init__.py +2 -0
  5. camel/societies/workforce/prompts.py +36 -10
  6. camel/societies/workforce/single_agent_worker.py +7 -5
  7. camel/societies/workforce/workforce.py +6 -4
  8. camel/storages/key_value_storages/mem0_cloud.py +48 -47
  9. camel/storages/vectordb_storages/__init__.py +1 -0
  10. camel/storages/vectordb_storages/surreal.py +100 -150
  11. camel/toolkits/__init__.py +6 -1
  12. camel/toolkits/base.py +60 -2
  13. camel/toolkits/excel_toolkit.py +153 -64
  14. camel/toolkits/file_write_toolkit.py +67 -0
  15. camel/toolkits/hybrid_browser_toolkit/config_loader.py +136 -413
  16. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +131 -1966
  17. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1177 -0
  18. camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4356 -0
  19. camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
  20. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
  21. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +945 -0
  22. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +226 -0
  23. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +522 -0
  24. camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
  25. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +110 -0
  26. camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +26 -0
  27. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +254 -0
  28. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +582 -0
  29. camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
  30. camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +447 -0
  31. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2077 -0
  32. camel/toolkits/mcp_toolkit.py +341 -46
  33. camel/toolkits/message_integration.py +719 -0
  34. camel/toolkits/note_taking_toolkit.py +18 -29
  35. camel/toolkits/notion_mcp_toolkit.py +234 -0
  36. camel/toolkits/screenshot_toolkit.py +116 -31
  37. camel/toolkits/search_toolkit.py +20 -2
  38. camel/toolkits/slack_toolkit.py +43 -48
  39. camel/toolkits/terminal_toolkit.py +288 -46
  40. camel/toolkits/video_analysis_toolkit.py +13 -13
  41. camel/toolkits/video_download_toolkit.py +11 -11
  42. camel/toolkits/web_deploy_toolkit.py +207 -12
  43. camel/types/enums.py +6 -0
  44. {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/METADATA +49 -9
  45. {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/RECORD +53 -36
  46. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/actions.py +0 -0
  47. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/agent.py +0 -0
  48. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/browser_session.py +0 -0
  49. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/snapshot.py +0 -0
  50. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/stealth_script.js +0 -0
  51. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/unified_analyzer.js +0 -0
  52. {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/WHEEL +0 -0
  53. {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/licenses/LICENSE +0 -0
@@ -128,18 +128,15 @@ class SlackToolkit(BaseToolkit):
128
128
  return f"Error creating conversation: {e.response['error']}"
129
129
 
130
130
  def join_slack_channel(self, channel_id: str) -> str:
131
- r"""Joins an existing Slack channel.
131
+ r"""Joins an existing Slack channel. When use this function you must
132
+ call `get_slack_channel_information` function first to get the
133
+ `channel id`.
132
134
 
133
135
  Args:
134
136
  channel_id (str): The ID of the Slack channel to join.
135
137
 
136
138
  Returns:
137
- str: A confirmation message indicating whether join successfully
138
- or an error message.
139
-
140
- Raises:
141
- SlackApiError: If there is an error during get slack channel
142
- information.
139
+ str: A string containing the API response from Slack.
143
140
  """
144
141
  from slack_sdk.errors import SlackApiError
145
142
 
@@ -148,21 +145,18 @@ class SlackToolkit(BaseToolkit):
148
145
  response = slack_client.conversations_join(channel=channel_id)
149
146
  return str(response)
150
147
  except SlackApiError as e:
151
- return f"Error creating conversation: {e.response['error']}"
148
+ return f"Error joining channel: {e.response['error']}"
152
149
 
153
150
  def leave_slack_channel(self, channel_id: str) -> str:
154
- r"""Leaves an existing Slack channel.
151
+ r"""Leaves an existing Slack channel. When use this function you must
152
+ call `get_slack_channel_information` function first to get the
153
+ `channel id`.
155
154
 
156
155
  Args:
157
156
  channel_id (str): The ID of the Slack channel to leave.
158
157
 
159
158
  Returns:
160
- str: A confirmation message indicating whether leave successfully
161
- or an error message.
162
-
163
- Raises:
164
- SlackApiError: If there is an error during get slack channel
165
- information.
159
+ str: A string containing the API response from Slack.
166
160
  """
167
161
  from slack_sdk.errors import SlackApiError
168
162
 
@@ -171,18 +165,18 @@ class SlackToolkit(BaseToolkit):
171
165
  response = slack_client.conversations_leave(channel=channel_id)
172
166
  return str(response)
173
167
  except SlackApiError as e:
174
- return f"Error creating conversation: {e.response['error']}"
168
+ return f"Error leaving channel: {e.response['error']}"
175
169
 
176
170
  def get_slack_channel_information(self) -> str:
177
- r"""Retrieve Slack channels and return relevant information in JSON
178
- format.
171
+ r"""Retrieve a list of all public channels in the Slack workspace.
179
172
 
180
- Returns:
181
- str: JSON string containing information about Slack channels.
173
+ This function is crucial for discovering available channels and their
174
+ `channel_id`s, which are required by many other functions.
182
175
 
183
- Raises:
184
- SlackApiError: If there is an error during get slack channel
185
- information.
176
+ Returns:
177
+ str: A JSON string representing a list of channels. Each channel
178
+ object in the list contains 'id', 'name', 'created', and
179
+ 'num_members'. Returns an error message string on failure.
186
180
  """
187
181
  from slack_sdk.errors import SlackApiError
188
182
 
@@ -204,21 +198,20 @@ class SlackToolkit(BaseToolkit):
204
198
  ]
205
199
  return json.dumps(filtered_result, ensure_ascii=False)
206
200
  except SlackApiError as e:
207
- return f"Error creating conversation: {e.response['error']}"
201
+ return f"Error retrieving channel list: {e.response['error']}"
208
202
 
209
203
  def get_slack_channel_message(self, channel_id: str) -> str:
210
- r"""Retrieve messages from a Slack channel.
204
+ r"""Retrieve messages from a Slack channel. When use this function you
205
+ must call `get_slack_channel_information` function first to get the
206
+ `channel id`.
211
207
 
212
208
  Args:
213
209
  channel_id (str): The ID of the Slack channel to retrieve messages
214
210
  from.
215
211
 
216
212
  Returns:
217
- str: JSON string containing filtered message data.
218
-
219
- Raises:
220
- SlackApiError: If there is an error during get
221
- slack channel message.
213
+ str: A JSON string representing a list of messages. Each message
214
+ object contains 'user', 'text', and 'ts' (timestamp).
222
215
  """
223
216
  from slack_sdk.errors import SlackApiError
224
217
 
@@ -242,19 +235,21 @@ class SlackToolkit(BaseToolkit):
242
235
  file_path: Optional[str] = None,
243
236
  user: Optional[str] = None,
244
237
  ) -> str:
245
- r"""Send a message to a Slack channel.
238
+ r"""Send a message to a Slack channel. When use this function you must
239
+ call `get_slack_channel_information` function first to get the
240
+ `channel id`.
246
241
 
247
242
  Args:
248
243
  message (str): The message to send.
249
- channel_id (str): The ID of the Slack channel to send message.
250
- file_path (Optional[str]): The path of the file to send.
251
- Defaults to `None`.
252
- user (Optional[str]): The user ID of the recipient.
253
- Defaults to `None`.
244
+ channel_id (str): The ID of the channel to send the message to.
245
+ file_path (Optional[str]): The local path of a file to upload
246
+ with the message.
247
+ user (Optional[str]): The ID of a user to send an ephemeral
248
+ message to (visible only to that user).
254
249
 
255
250
  Returns:
256
- str: A confirmation message indicating whether the message was sent
257
- successfully or an error message.
251
+ str: A confirmation message indicating success or an error
252
+ message.
258
253
  """
259
254
  from slack_sdk.errors import SlackApiError
260
255
 
@@ -280,25 +275,25 @@ class SlackToolkit(BaseToolkit):
280
275
  f"got response: {response}"
281
276
  )
282
277
  except SlackApiError as e:
283
- return f"Error creating conversation: {e.response['error']}"
278
+ return f"Error sending message: {e.response['error']}"
284
279
 
285
280
  def delete_slack_message(
286
281
  self,
287
282
  time_stamp: str,
288
283
  channel_id: str,
289
284
  ) -> str:
290
- r"""Delete a message to a Slack channel.
285
+ r"""Delete a message from a Slack channel. When use this function you
286
+ must call `get_slack_channel_information` function first to get the
287
+ `channel id`.
291
288
 
292
289
  Args:
293
- time_stamp (str): Timestamp of the message to be deleted.
294
- channel_id (str): The ID of the Slack channel to delete message.
290
+ time_stamp (str): The 'ts' value of the message to be deleted.
291
+ You can get this from the `get_slack_channel_message` function.
292
+ channel_id (str): The ID of the channel where the message is. Use
293
+ `get_slack_channel_information` to find the `channel_id`.
295
294
 
296
295
  Returns:
297
- str: A confirmation message indicating whether the message
298
- was delete successfully or an error message.
299
-
300
- Raises:
301
- SlackApiError: If an error occurs while sending the message.
296
+ str: A string containing the API response from Slack.
302
297
  """
303
298
  from slack_sdk.errors import SlackApiError
304
299
 
@@ -309,7 +304,7 @@ class SlackToolkit(BaseToolkit):
309
304
  )
310
305
  return str(response)
311
306
  except SlackApiError as e:
312
- return f"Error creating conversation: {e.response['error']}"
307
+ return f"Error deleting message: {e.response['error']}"
313
308
 
314
309
  def get_tools(self) -> List[FunctionTool]:
315
310
  r"""Returns a list of FunctionTool objects representing the
@@ -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:`None`)
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] = None,
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
- # Create virtual environment with pip
201
- venv.create(self.cloned_env_path, with_pip=True)
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
- # Ensure pip is properly available by upgrading it
204
- if self.os_type == 'Windows':
205
- python_path = os.path.join(
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
- else:
209
- python_path = os.path.join(
210
- self.cloned_env_path, "bin", "python"
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
- # Verify python executable exists
214
- if os.path.exists(python_path):
215
- # Use python -m pip to ensure pip is available
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
- [python_path, "-m", "pip", "install", "--upgrade", "pip"],
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=60,
259
+ timeout=300,
222
260
  )
261
+
223
262
  self._update_terminal_output(
224
- "New Python environment created successfully with pip!\n"
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
- f"Warning: Python executable not found at {python_path}\n"
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
- # Check if we should use uv
280
- if self._is_uv_environment():
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
- ["uv", "venv", "--python", "3.10", self.initial_env_path],
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 = ["pip", "setuptools", "wheel"]
510
+ essential_packages = [
511
+ "pip",
512
+ "setuptools",
513
+ "wheel",
514
+ "pyautogui",
515
+ "plotly",
516
+ "ffmpeg",
517
+ ]
323
518
  subprocess.run(
324
519
  [
325
- "uv",
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 = ["pip", "setuptools", "wheel"]
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. It can run commands in both non-interactive
814
- (capturing output) and interactive modes. Each session is identified by
815
- a unique ID. If a session with the given ID does not exist, it will be
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 `interactive` is set to `True`, this function may block if the
835
- command requires input. In safe mode, some commands that are
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
- stdout, stderr = proc.communicate()
949
- output = stdout or ""
950
- if stderr:
951
- output += f"\nStderr Output:\n{stderr}"
952
- self.shell_sessions[id]["output"] = output
953
- self._update_terminal_output(output + "\n")
954
- return output
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