camel-ai 0.2.71a11__py3-none-any.whl → 0.2.72__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 +261 -489
- camel/memories/agent_memories.py +39 -0
- camel/memories/base.py +8 -0
- camel/models/gemini_model.py +30 -2
- camel/models/moonshot_model.py +36 -4
- camel/models/openai_model.py +29 -15
- camel/societies/workforce/prompts.py +25 -15
- camel/societies/workforce/role_playing_worker.py +1 -1
- camel/societies/workforce/single_agent_worker.py +9 -7
- camel/societies/workforce/worker.py +1 -1
- camel/societies/workforce/workforce.py +97 -34
- camel/storages/vectordb_storages/__init__.py +1 -0
- camel/storages/vectordb_storages/surreal.py +415 -0
- camel/tasks/task.py +9 -5
- camel/toolkits/__init__.py +10 -1
- camel/toolkits/base.py +57 -1
- camel/toolkits/human_toolkit.py +5 -1
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +127 -414
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +783 -1626
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +489 -0
- camel/toolkits/markitdown_toolkit.py +2 -2
- camel/toolkits/message_integration.py +592 -0
- camel/toolkits/note_taking_toolkit.py +195 -26
- camel/toolkits/openai_image_toolkit.py +5 -5
- camel/toolkits/origene_mcp_toolkit.py +97 -0
- camel/toolkits/screenshot_toolkit.py +213 -0
- camel/toolkits/search_toolkit.py +161 -79
- camel/toolkits/terminal_toolkit.py +379 -165
- camel/toolkits/video_analysis_toolkit.py +13 -13
- camel/toolkits/video_download_toolkit.py +11 -11
- camel/toolkits/web_deploy_toolkit.py +1024 -0
- camel/types/enums.py +6 -3
- camel/types/unified_model_type.py +16 -4
- camel/utils/mcp_client.py +8 -0
- camel/utils/tool_result.py +1 -1
- {camel_ai-0.2.71a11.dist-info → camel_ai-0.2.72.dist-info}/METADATA +6 -3
- {camel_ai-0.2.71a11.dist-info → camel_ai-0.2.72.dist-info}/RECORD +40 -40
- camel/toolkits/hybrid_browser_toolkit/actions.py +0 -417
- camel/toolkits/hybrid_browser_toolkit/agent.py +0 -311
- camel/toolkits/hybrid_browser_toolkit/browser_session.py +0 -739
- camel/toolkits/hybrid_browser_toolkit/snapshot.py +0 -227
- camel/toolkits/hybrid_browser_toolkit/stealth_script.js +0 -0
- camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +0 -1002
- {camel_ai-0.2.71a11.dist-info → camel_ai-0.2.72.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.71a11.dist-info → camel_ai-0.2.72.dist-info}/licenses/LICENSE +0 -0
|
@@ -16,6 +16,7 @@ import atexit
|
|
|
16
16
|
import os
|
|
17
17
|
import platform
|
|
18
18
|
import queue
|
|
19
|
+
import shutil
|
|
19
20
|
import subprocess
|
|
20
21
|
import sys
|
|
21
22
|
import threading
|
|
@@ -41,21 +42,25 @@ class TerminalToolkit(BaseToolkit):
|
|
|
41
42
|
|
|
42
43
|
Args:
|
|
43
44
|
timeout (Optional[float]): The timeout for terminal operations.
|
|
45
|
+
(default: :obj:`None`)
|
|
44
46
|
shell_sessions (Optional[Dict[str, Any]]): A dictionary to store
|
|
45
|
-
shell session information. If None
|
|
46
|
-
used. (default: :obj:`
|
|
47
|
-
|
|
48
|
-
If
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
shell session information. If :obj:`None`, an empty dictionary
|
|
48
|
+
will be used. (default: :obj:`None`)
|
|
49
|
+
working_directory (Optional[str]): The working directory for
|
|
50
|
+
operations. If not provided, it will be determined by the
|
|
51
|
+
`CAMEL_WORKDIR` environment variable (if set). If the
|
|
52
|
+
environment variable is not set, it defaults to `./workspace`. All
|
|
53
|
+
execution and write operations will be restricted to this
|
|
54
|
+
directory. Read operations can access paths outside this
|
|
55
|
+
directory. (default: :obj:`None`)
|
|
51
56
|
need_terminal (bool): Whether to create a terminal interface.
|
|
52
57
|
(default: :obj:`True`)
|
|
53
|
-
use_shell_mode (bool): Whether to use shell mode for command
|
|
54
|
-
(default: :obj:`True`)
|
|
58
|
+
use_shell_mode (bool): Whether to use shell mode for command
|
|
59
|
+
execution. (default: :obj:`True`)
|
|
55
60
|
clone_current_env (bool): Whether to clone the current Python
|
|
56
|
-
environment.(default: :obj:`False`)
|
|
57
|
-
safe_mode (bool): Whether to enable safe mode to restrict
|
|
58
|
-
(default: :obj:`True`)
|
|
61
|
+
environment. (default: :obj:`False`)
|
|
62
|
+
safe_mode (bool): Whether to enable safe mode to restrict
|
|
63
|
+
operations. (default: :obj:`True`)
|
|
59
64
|
|
|
60
65
|
Note:
|
|
61
66
|
Most functions are compatible with Unix-based systems (macOS, Linux).
|
|
@@ -67,7 +72,7 @@ class TerminalToolkit(BaseToolkit):
|
|
|
67
72
|
self,
|
|
68
73
|
timeout: Optional[float] = None,
|
|
69
74
|
shell_sessions: Optional[Dict[str, Any]] = None,
|
|
70
|
-
|
|
75
|
+
working_directory: Optional[str] = None,
|
|
71
76
|
need_terminal: bool = True,
|
|
72
77
|
use_shell_mode: bool = True,
|
|
73
78
|
clone_current_env: bool = False,
|
|
@@ -88,12 +93,22 @@ class TerminalToolkit(BaseToolkit):
|
|
|
88
93
|
|
|
89
94
|
self.python_executable = sys.executable
|
|
90
95
|
self.is_macos = platform.system() == 'Darwin'
|
|
96
|
+
self.initial_env_path: Optional[str] = None
|
|
97
|
+
self.initial_env_prepared = False
|
|
91
98
|
|
|
92
99
|
atexit.register(self.__del__)
|
|
93
100
|
|
|
94
|
-
if
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
if working_directory:
|
|
102
|
+
self.working_dir = os.path.abspath(working_directory)
|
|
103
|
+
else:
|
|
104
|
+
camel_workdir = os.environ.get("CAMEL_WORKDIR")
|
|
105
|
+
if camel_workdir:
|
|
106
|
+
self.working_dir = os.path.abspath(camel_workdir)
|
|
107
|
+
else:
|
|
108
|
+
self.working_dir = os.path.abspath("./workspace")
|
|
109
|
+
|
|
110
|
+
if not os.path.exists(self.working_dir):
|
|
111
|
+
os.makedirs(self.working_dir, exist_ok=True)
|
|
97
112
|
self._update_terminal_output(
|
|
98
113
|
f"Working directory set to: {self.working_dir}\n"
|
|
99
114
|
)
|
|
@@ -108,6 +123,7 @@ class TerminalToolkit(BaseToolkit):
|
|
|
108
123
|
self._clone_current_environment()
|
|
109
124
|
else:
|
|
110
125
|
self.cloned_env_path = None
|
|
126
|
+
self._prepare_initial_environment()
|
|
111
127
|
|
|
112
128
|
if need_terminal:
|
|
113
129
|
if self.is_macos:
|
|
@@ -178,20 +194,257 @@ class TerminalToolkit(BaseToolkit):
|
|
|
178
194
|
return
|
|
179
195
|
|
|
180
196
|
self._update_terminal_output(
|
|
181
|
-
f"Creating new Python environment at:{self.cloned_env_path}\n"
|
|
197
|
+
f"Creating new Python environment at: {self.cloned_env_path}\n"
|
|
182
198
|
)
|
|
183
199
|
|
|
200
|
+
# Create virtual environment with pip
|
|
184
201
|
venv.create(self.cloned_env_path, with_pip=True)
|
|
202
|
+
|
|
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"
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
python_path = os.path.join(
|
|
210
|
+
self.cloned_env_path, "bin", "python"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Verify python executable exists
|
|
214
|
+
if os.path.exists(python_path):
|
|
215
|
+
# Use python -m pip to ensure pip is available
|
|
216
|
+
subprocess.run(
|
|
217
|
+
[python_path, "-m", "pip", "install", "--upgrade", "pip"],
|
|
218
|
+
check=True,
|
|
219
|
+
capture_output=True,
|
|
220
|
+
cwd=self.working_dir,
|
|
221
|
+
timeout=60,
|
|
222
|
+
)
|
|
223
|
+
self._update_terminal_output(
|
|
224
|
+
"New Python environment created successfully with pip!\n"
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
self._update_terminal_output(
|
|
228
|
+
f"Warning: Python executable not found at {python_path}\n"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
except subprocess.CalledProcessError as e:
|
|
232
|
+
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
185
233
|
self._update_terminal_output(
|
|
186
|
-
"
|
|
234
|
+
f"Failed to upgrade pip in cloned environment: {error_msg}\n"
|
|
235
|
+
)
|
|
236
|
+
logger.error(f"Failed to upgrade pip: {error_msg}")
|
|
237
|
+
except subprocess.TimeoutExpired:
|
|
238
|
+
self._update_terminal_output(
|
|
239
|
+
"Pip upgrade timed out, but environment may still be usable\n"
|
|
187
240
|
)
|
|
188
|
-
|
|
189
241
|
except Exception as e:
|
|
190
242
|
self._update_terminal_output(
|
|
191
243
|
f"Failed to create environment: {e!s}\n"
|
|
192
244
|
)
|
|
193
245
|
logger.error(f"Failed to create environment: {e}")
|
|
194
246
|
|
|
247
|
+
def _is_uv_environment(self) -> bool:
|
|
248
|
+
r"""Detect whether the current Python runtime is managed by uv."""
|
|
249
|
+
return (
|
|
250
|
+
"UV_CACHE_DIR" in os.environ
|
|
251
|
+
or "uv" in sys.executable
|
|
252
|
+
or shutil.which("uv") is not None
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def _prepare_initial_environment(self):
|
|
256
|
+
r"""Prepare initial environment with Python 3.10, pip, and other
|
|
257
|
+
essential tools.
|
|
258
|
+
"""
|
|
259
|
+
try:
|
|
260
|
+
self.initial_env_path = os.path.join(
|
|
261
|
+
self.working_dir, ".initial_env"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if os.path.exists(self.initial_env_path):
|
|
265
|
+
self._update_terminal_output(
|
|
266
|
+
f"Using existing initial environment"
|
|
267
|
+
f": {self.initial_env_path}\n"
|
|
268
|
+
)
|
|
269
|
+
self.initial_env_prepared = True
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
self._update_terminal_output(
|
|
273
|
+
f"Preparing initial environment at: {self.initial_env_path}\n"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Create the initial environment directory
|
|
277
|
+
os.makedirs(self.initial_env_path, exist_ok=True)
|
|
278
|
+
|
|
279
|
+
# Check if we should use uv
|
|
280
|
+
if self._is_uv_environment():
|
|
281
|
+
self._setup_initial_env_with_uv()
|
|
282
|
+
else:
|
|
283
|
+
self._setup_initial_env_with_venv()
|
|
284
|
+
|
|
285
|
+
self.initial_env_prepared = True
|
|
286
|
+
self._update_terminal_output(
|
|
287
|
+
"Initial environment prepared successfully!\n"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
self._update_terminal_output(
|
|
292
|
+
f"Failed to prepare initial environment: {e!s}\n"
|
|
293
|
+
)
|
|
294
|
+
logger.error(f"Failed to prepare initial environment: {e}")
|
|
295
|
+
|
|
296
|
+
def _setup_initial_env_with_uv(self):
|
|
297
|
+
r"""Set up initial environment using uv."""
|
|
298
|
+
if self.initial_env_path is None:
|
|
299
|
+
raise Exception("Initial environment path not set")
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
# Create virtual environment with Python 3.10 using uv
|
|
303
|
+
subprocess.run(
|
|
304
|
+
["uv", "venv", "--python", "3.10", self.initial_env_path],
|
|
305
|
+
check=True,
|
|
306
|
+
capture_output=True,
|
|
307
|
+
cwd=self.working_dir,
|
|
308
|
+
timeout=300,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Get the python path from the new environment
|
|
312
|
+
if self.os_type == 'Windows':
|
|
313
|
+
python_path = os.path.join(
|
|
314
|
+
self.initial_env_path, "Scripts", "python.exe"
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
python_path = os.path.join(
|
|
318
|
+
self.initial_env_path, "bin", "python"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Install essential packages using uv
|
|
322
|
+
essential_packages = [
|
|
323
|
+
"pip",
|
|
324
|
+
"setuptools",
|
|
325
|
+
"wheel",
|
|
326
|
+
"pyautogui",
|
|
327
|
+
"plotly",
|
|
328
|
+
"ffmpeg",
|
|
329
|
+
]
|
|
330
|
+
subprocess.run(
|
|
331
|
+
[
|
|
332
|
+
"uv",
|
|
333
|
+
"pip",
|
|
334
|
+
"install",
|
|
335
|
+
"--python",
|
|
336
|
+
python_path,
|
|
337
|
+
*essential_packages,
|
|
338
|
+
],
|
|
339
|
+
check=True,
|
|
340
|
+
capture_output=True,
|
|
341
|
+
cwd=self.working_dir,
|
|
342
|
+
timeout=300,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Check if Node.js is available (but don't install it)
|
|
346
|
+
self._check_nodejs_availability()
|
|
347
|
+
|
|
348
|
+
self._update_terminal_output(
|
|
349
|
+
"[UV] Initial environment created with Python 3.10 "
|
|
350
|
+
"and essential packages\n"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
except subprocess.CalledProcessError as e:
|
|
354
|
+
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
355
|
+
raise Exception(f"UV setup failed: {error_msg}")
|
|
356
|
+
except subprocess.TimeoutExpired:
|
|
357
|
+
raise Exception("UV setup timed out after 5 minutes")
|
|
358
|
+
|
|
359
|
+
def _setup_initial_env_with_venv(self):
|
|
360
|
+
r"""Set up initial environment using standard venv."""
|
|
361
|
+
if self.initial_env_path is None:
|
|
362
|
+
raise Exception("Initial environment path not set")
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
# Create virtual environment with system Python
|
|
366
|
+
venv.create(
|
|
367
|
+
self.initial_env_path,
|
|
368
|
+
with_pip=True,
|
|
369
|
+
system_site_packages=False,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Get pip path
|
|
373
|
+
if self.os_type == 'Windows':
|
|
374
|
+
pip_path = os.path.join(
|
|
375
|
+
self.initial_env_path, "Scripts", "pip.exe"
|
|
376
|
+
)
|
|
377
|
+
else:
|
|
378
|
+
pip_path = os.path.join(self.initial_env_path, "bin", "pip")
|
|
379
|
+
|
|
380
|
+
# Upgrade pip and install essential packages
|
|
381
|
+
essential_packages = [
|
|
382
|
+
"pip",
|
|
383
|
+
"setuptools",
|
|
384
|
+
"wheel",
|
|
385
|
+
"pyautogui",
|
|
386
|
+
"plotly",
|
|
387
|
+
"ffmpeg",
|
|
388
|
+
]
|
|
389
|
+
subprocess.run(
|
|
390
|
+
[pip_path, "install", "--upgrade", *essential_packages],
|
|
391
|
+
check=True,
|
|
392
|
+
capture_output=True,
|
|
393
|
+
cwd=self.working_dir,
|
|
394
|
+
timeout=300,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Check if Node.js is available (but don't install it)
|
|
398
|
+
self._check_nodejs_availability()
|
|
399
|
+
|
|
400
|
+
self._update_terminal_output(
|
|
401
|
+
"Initial environment created with system Python and "
|
|
402
|
+
"essential packages\n"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
except subprocess.CalledProcessError as e:
|
|
406
|
+
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
407
|
+
raise Exception(f"Venv setup failed: {error_msg}")
|
|
408
|
+
except subprocess.TimeoutExpired:
|
|
409
|
+
raise Exception("Venv setup timed out after 5 minutes")
|
|
410
|
+
|
|
411
|
+
def _check_nodejs_availability(self):
|
|
412
|
+
r"""Check if Node.js is available without modifying the system."""
|
|
413
|
+
try:
|
|
414
|
+
# Check if Node.js is already available in the system
|
|
415
|
+
node_result = subprocess.run(
|
|
416
|
+
["node", "--version"],
|
|
417
|
+
check=False,
|
|
418
|
+
capture_output=True,
|
|
419
|
+
timeout=10,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
npm_result = subprocess.run(
|
|
423
|
+
["npm", "--version"],
|
|
424
|
+
check=False,
|
|
425
|
+
capture_output=True,
|
|
426
|
+
timeout=10,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
if node_result.returncode == 0 and npm_result.returncode == 0:
|
|
430
|
+
node_version = node_result.stdout.decode().strip()
|
|
431
|
+
npm_version = npm_result.stdout.decode().strip()
|
|
432
|
+
self._update_terminal_output(
|
|
433
|
+
f"Node.js {node_version} and npm {npm_version} "
|
|
434
|
+
"are available\n"
|
|
435
|
+
)
|
|
436
|
+
else:
|
|
437
|
+
self._update_terminal_output(
|
|
438
|
+
"Note: Node.js not found. If needed, please install it "
|
|
439
|
+
"manually.\n"
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
except Exception as e:
|
|
443
|
+
self._update_terminal_output(
|
|
444
|
+
f"Note: Could not check Node.js availability - {e}.\n"
|
|
445
|
+
)
|
|
446
|
+
logger.warning(f"Failed to check Node.js: {e}")
|
|
447
|
+
|
|
195
448
|
def _create_terminal(self):
|
|
196
449
|
r"""Create a terminal GUI. If GUI creation fails, fallback
|
|
197
450
|
to file output."""
|
|
@@ -335,108 +588,6 @@ class TerminalToolkit(BaseToolkit):
|
|
|
335
588
|
logger.error(f"Failed to copy file: {e}")
|
|
336
589
|
return None
|
|
337
590
|
|
|
338
|
-
def file_find_in_content(
|
|
339
|
-
self, file: str, regex: str, sudo: bool = False
|
|
340
|
-
) -> str:
|
|
341
|
-
r"""Search for text within a file's content using a regular expression.
|
|
342
|
-
|
|
343
|
-
This function is useful for finding specific patterns or lines of text
|
|
344
|
-
within a given file. It uses `grep` on Unix-like systems and `findstr`
|
|
345
|
-
on Windows.
|
|
346
|
-
|
|
347
|
-
Args:
|
|
348
|
-
file (str): The absolute path of the file to search within.
|
|
349
|
-
regex (str): The regular expression pattern to match.
|
|
350
|
-
sudo (bool, optional): Whether to use sudo privileges for the
|
|
351
|
-
search. Defaults to False. Note: Using sudo requires the
|
|
352
|
-
process to have appropriate permissions.
|
|
353
|
-
(default: :obj:`False`)
|
|
354
|
-
|
|
355
|
-
Returns:
|
|
356
|
-
str: The matching content found in the file. If no matches are
|
|
357
|
-
found, an empty string is returned. Returns an error message
|
|
358
|
-
if the file does not exist or another error occurs.
|
|
359
|
-
"""
|
|
360
|
-
|
|
361
|
-
if not os.path.exists(file):
|
|
362
|
-
return f"File not found: {file}"
|
|
363
|
-
|
|
364
|
-
if not os.path.isfile(file):
|
|
365
|
-
return f"The path provided is not a file: {file}"
|
|
366
|
-
|
|
367
|
-
command = []
|
|
368
|
-
if sudo:
|
|
369
|
-
error_msg = self._enforce_working_dir_for_execution(file)
|
|
370
|
-
if error_msg:
|
|
371
|
-
return error_msg
|
|
372
|
-
command.extend(["sudo"])
|
|
373
|
-
|
|
374
|
-
if self.os_type in ['Darwin', 'Linux']: # macOS or Linux
|
|
375
|
-
command.extend(["grep", "-E", regex, file])
|
|
376
|
-
else: # Windows
|
|
377
|
-
# For Windows, we could use PowerShell or findstr
|
|
378
|
-
command.extend(["findstr", "/R", regex, file])
|
|
379
|
-
|
|
380
|
-
try:
|
|
381
|
-
result = subprocess.run(
|
|
382
|
-
command, check=False, capture_output=True, text=True
|
|
383
|
-
)
|
|
384
|
-
return result.stdout.strip()
|
|
385
|
-
except subprocess.SubprocessError as e:
|
|
386
|
-
logger.error(f"Error searching in file content: {e}")
|
|
387
|
-
return f"Error: {e!s}"
|
|
388
|
-
|
|
389
|
-
def file_find_by_name(self, path: str, glob: str) -> str:
|
|
390
|
-
r"""Find files by name in a specified directory using a glob pattern.
|
|
391
|
-
|
|
392
|
-
This function recursively searches for files matching a given name or
|
|
393
|
-
pattern within a directory. It uses `find` on Unix-like systems and
|
|
394
|
-
`dir` on Windows.
|
|
395
|
-
|
|
396
|
-
Args:
|
|
397
|
-
path (str): The absolute path of the directory to search in.
|
|
398
|
-
glob (str): The filename pattern to search for, using glob syntax
|
|
399
|
-
(e.g., "*.py", "data*").
|
|
400
|
-
|
|
401
|
-
Returns:
|
|
402
|
-
str: A newline-separated string containing the paths of the files
|
|
403
|
-
that match the pattern. Returns an error message if the
|
|
404
|
-
directory does not exist or another error occurs.
|
|
405
|
-
"""
|
|
406
|
-
if not os.path.exists(path):
|
|
407
|
-
return f"Directory not found: {path}"
|
|
408
|
-
|
|
409
|
-
if not os.path.isdir(path):
|
|
410
|
-
return f"The path provided is not a directory: {path}"
|
|
411
|
-
|
|
412
|
-
command = []
|
|
413
|
-
if self.os_type in ['Darwin', 'Linux']: # macOS or Linux
|
|
414
|
-
command.extend(["find", path, "-name", glob])
|
|
415
|
-
else: # Windows
|
|
416
|
-
# For Windows, we use dir command with /s for recursive search
|
|
417
|
-
# and /b for bare format
|
|
418
|
-
|
|
419
|
-
pattern = glob
|
|
420
|
-
file_path = os.path.join(path, pattern).replace('/', '\\')
|
|
421
|
-
command.extend(["cmd", "/c", "dir", "/s", "/b", file_path])
|
|
422
|
-
|
|
423
|
-
try:
|
|
424
|
-
result = subprocess.run(
|
|
425
|
-
command,
|
|
426
|
-
check=False,
|
|
427
|
-
capture_output=True,
|
|
428
|
-
text=True,
|
|
429
|
-
shell=False,
|
|
430
|
-
)
|
|
431
|
-
|
|
432
|
-
output = result.stdout.strip()
|
|
433
|
-
if self.os_type == 'Windows':
|
|
434
|
-
output = output.replace('\\', '/')
|
|
435
|
-
return output
|
|
436
|
-
except subprocess.SubprocessError as e:
|
|
437
|
-
logger.error(f"Error finding files by name: {e}")
|
|
438
|
-
return f"Error: {e!s}"
|
|
439
|
-
|
|
440
591
|
def _sanitize_command(self, command: str, exec_dir: str) -> Tuple:
|
|
441
592
|
r"""Check and modify command to ensure safety.
|
|
442
593
|
|
|
@@ -721,24 +872,75 @@ class TerminalToolkit(BaseToolkit):
|
|
|
721
872
|
self._update_terminal_output(f"\n$ {command}\n")
|
|
722
873
|
|
|
723
874
|
if command.startswith('python') or command.startswith('pip'):
|
|
724
|
-
|
|
875
|
+
python_path = None
|
|
876
|
+
pip_path = None
|
|
877
|
+
|
|
878
|
+
# Try cloned environment first
|
|
879
|
+
if self.cloned_env_path and os.path.exists(
|
|
880
|
+
self.cloned_env_path
|
|
881
|
+
):
|
|
725
882
|
if self.os_type == 'Windows':
|
|
726
883
|
base_path = os.path.join(
|
|
727
884
|
self.cloned_env_path, "Scripts"
|
|
728
885
|
)
|
|
729
|
-
|
|
730
|
-
|
|
886
|
+
python_candidate = os.path.join(
|
|
887
|
+
base_path, "python.exe"
|
|
888
|
+
)
|
|
889
|
+
pip_candidate = os.path.join(base_path, "pip.exe")
|
|
731
890
|
else:
|
|
732
891
|
base_path = os.path.join(self.cloned_env_path, "bin")
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
892
|
+
python_candidate = os.path.join(base_path, "python")
|
|
893
|
+
pip_candidate = os.path.join(base_path, "pip")
|
|
894
|
+
|
|
895
|
+
# Verify the executables exist
|
|
896
|
+
if os.path.exists(python_candidate):
|
|
897
|
+
python_path = python_candidate
|
|
898
|
+
# For pip, use python -m pip if pip executable doesn't
|
|
899
|
+
# exist
|
|
900
|
+
if os.path.exists(pip_candidate):
|
|
901
|
+
pip_path = pip_candidate
|
|
902
|
+
else:
|
|
903
|
+
pip_path = f'"{python_path}" -m pip'
|
|
904
|
+
|
|
905
|
+
# Try initial environment if cloned environment failed
|
|
906
|
+
if (
|
|
907
|
+
python_path is None
|
|
908
|
+
and self.initial_env_prepared
|
|
909
|
+
and self.initial_env_path
|
|
910
|
+
and os.path.exists(self.initial_env_path)
|
|
911
|
+
):
|
|
912
|
+
if self.os_type == 'Windows':
|
|
913
|
+
base_path = os.path.join(
|
|
914
|
+
self.initial_env_path, "Scripts"
|
|
915
|
+
)
|
|
916
|
+
python_candidate = os.path.join(
|
|
917
|
+
base_path, "python.exe"
|
|
918
|
+
)
|
|
919
|
+
pip_candidate = os.path.join(base_path, "pip.exe")
|
|
920
|
+
else:
|
|
921
|
+
base_path = os.path.join(self.initial_env_path, "bin")
|
|
922
|
+
python_candidate = os.path.join(base_path, "python")
|
|
923
|
+
pip_candidate = os.path.join(base_path, "pip")
|
|
924
|
+
|
|
925
|
+
# Verify the executables exist
|
|
926
|
+
if os.path.exists(python_candidate):
|
|
927
|
+
python_path = python_candidate
|
|
928
|
+
# For pip, use python -m pip if pip executable doesn't
|
|
929
|
+
# exist
|
|
930
|
+
if os.path.exists(pip_candidate):
|
|
931
|
+
pip_path = pip_candidate
|
|
932
|
+
else:
|
|
933
|
+
pip_path = f'"{python_path}" -m pip'
|
|
934
|
+
|
|
935
|
+
# Fall back to system Python
|
|
936
|
+
if python_path is None:
|
|
736
937
|
python_path = self.python_executable
|
|
737
938
|
pip_path = f'"{python_path}" -m pip'
|
|
738
939
|
|
|
739
|
-
|
|
940
|
+
# Ensure we have valid paths before replacement
|
|
941
|
+
if python_path and command.startswith('python'):
|
|
740
942
|
command = command.replace('python', f'"{python_path}"', 1)
|
|
741
|
-
elif command.startswith('pip'):
|
|
943
|
+
elif pip_path and command.startswith('pip'):
|
|
742
944
|
command = command.replace('pip', pip_path, 1)
|
|
743
945
|
|
|
744
946
|
if not interactive:
|
|
@@ -1259,52 +1461,66 @@ class TerminalToolkit(BaseToolkit):
|
|
|
1259
1461
|
logger.info("TerminalToolkit cleanup initiated")
|
|
1260
1462
|
|
|
1261
1463
|
# Clean up all processes in shell sessions
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
logger.info(
|
|
1267
|
-
f"Terminating process in session '{session_id}'"
|
|
1268
|
-
)
|
|
1269
|
-
|
|
1270
|
-
# Close process input/output streams if open
|
|
1271
|
-
if (
|
|
1272
|
-
hasattr(process, 'stdin')
|
|
1273
|
-
and process.stdin
|
|
1274
|
-
and not process.stdin.closed
|
|
1275
|
-
):
|
|
1276
|
-
process.stdin.close()
|
|
1277
|
-
|
|
1278
|
-
# Terminate the process
|
|
1279
|
-
process.terminate()
|
|
1464
|
+
if hasattr(self, 'shell_sessions'):
|
|
1465
|
+
for session_id, session in self.shell_sessions.items():
|
|
1466
|
+
process = session.get("process")
|
|
1467
|
+
if process is not None and session.get("running", False):
|
|
1280
1468
|
try:
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
except subprocess.TimeoutExpired:
|
|
1284
|
-
# Force kill if the process doesn't terminate
|
|
1285
|
-
# gracefully
|
|
1286
|
-
logger.warning(
|
|
1287
|
-
f"Process in session '{session_id}' did not "
|
|
1288
|
-
f"terminate gracefully, forcing kill"
|
|
1469
|
+
logger.info(
|
|
1470
|
+
f"Terminating process in session '{session_id}'"
|
|
1289
1471
|
)
|
|
1290
|
-
process.kill()
|
|
1291
1472
|
|
|
1292
|
-
|
|
1293
|
-
|
|
1473
|
+
# Close process input/output streams if open
|
|
1474
|
+
if (
|
|
1475
|
+
hasattr(process, 'stdin')
|
|
1476
|
+
and process.stdin
|
|
1477
|
+
and not process.stdin.closed
|
|
1478
|
+
):
|
|
1479
|
+
process.stdin.close()
|
|
1294
1480
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1481
|
+
# Terminate the process
|
|
1482
|
+
process.terminate()
|
|
1483
|
+
try:
|
|
1484
|
+
# Give the process a short time to terminate
|
|
1485
|
+
# gracefully
|
|
1486
|
+
process.wait(timeout=3)
|
|
1487
|
+
except subprocess.TimeoutExpired:
|
|
1488
|
+
# Force kill if the process doesn't terminate
|
|
1489
|
+
# gracefully
|
|
1490
|
+
logger.warning(
|
|
1491
|
+
f"Process in session '{session_id}' did not "
|
|
1492
|
+
f"terminate gracefully, forcing kill"
|
|
1493
|
+
)
|
|
1494
|
+
process.kill()
|
|
1495
|
+
|
|
1496
|
+
# Mark the session as not running
|
|
1497
|
+
session["running"] = False
|
|
1498
|
+
|
|
1499
|
+
except Exception as e:
|
|
1500
|
+
logger.error(
|
|
1501
|
+
f"Error cleaning up process in session "
|
|
1502
|
+
f"'{session_id}': {e}"
|
|
1503
|
+
)
|
|
1300
1504
|
|
|
1301
|
-
#
|
|
1505
|
+
# Clean up file output if it exists
|
|
1302
1506
|
if hasattr(self, 'log_file') and self.is_macos:
|
|
1303
1507
|
try:
|
|
1304
1508
|
logger.info(f"Final terminal log saved to: {self.log_file}")
|
|
1305
1509
|
except Exception as e:
|
|
1306
1510
|
logger.error(f"Error logging file information: {e}")
|
|
1307
1511
|
|
|
1512
|
+
# Clean up initial environment if it exists
|
|
1513
|
+
if hasattr(self, 'initial_env_path') and self.initial_env_path:
|
|
1514
|
+
try:
|
|
1515
|
+
if os.path.exists(self.initial_env_path):
|
|
1516
|
+
shutil.rmtree(self.initial_env_path)
|
|
1517
|
+
logger.info(
|
|
1518
|
+
f"Cleaned up initial environment: "
|
|
1519
|
+
f"{self.initial_env_path}"
|
|
1520
|
+
)
|
|
1521
|
+
except Exception as e:
|
|
1522
|
+
logger.error(f"Error cleaning up initial environment: {e}")
|
|
1523
|
+
|
|
1308
1524
|
# Clean up GUI resources if they exist
|
|
1309
1525
|
if hasattr(self, 'root') and self.root:
|
|
1310
1526
|
try:
|
|
@@ -1325,8 +1541,6 @@ class TerminalToolkit(BaseToolkit):
|
|
|
1325
1541
|
functions in the toolkit.
|
|
1326
1542
|
"""
|
|
1327
1543
|
return [
|
|
1328
|
-
FunctionTool(self.file_find_in_content),
|
|
1329
|
-
FunctionTool(self.file_find_by_name),
|
|
1330
1544
|
FunctionTool(self.shell_exec),
|
|
1331
1545
|
FunctionTool(self.shell_view),
|
|
1332
1546
|
FunctionTool(self.shell_wait),
|