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.
- 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/note_taking_toolkit.py +18 -29
- 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.72a8.dist-info → camel_ai-0.2.73.dist-info}/METADATA +49 -9
- {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/RECORD +53 -36
- /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.72a8.dist-info → camel_ai-0.2.73.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/licenses/LICENSE +0 -0
camel/toolkits/slack_toolkit.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
168
|
+
return f"Error leaving channel: {e.response['error']}"
|
|
175
169
|
|
|
176
170
|
def get_slack_channel_information(self) -> str:
|
|
177
|
-
r"""Retrieve
|
|
178
|
-
format.
|
|
171
|
+
r"""Retrieve a list of all public channels in the Slack workspace.
|
|
179
172
|
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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
|
|
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
|
|
250
|
-
file_path (Optional[str]): The path of
|
|
251
|
-
|
|
252
|
-
user (Optional[str]): The
|
|
253
|
-
|
|
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
|
|
257
|
-
|
|
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
|
|
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
|
|
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):
|
|
294
|
-
|
|
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
|
|
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
|
|
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:`
|
|
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
|
|