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.

Files changed (46) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +261 -489
  3. camel/memories/agent_memories.py +39 -0
  4. camel/memories/base.py +8 -0
  5. camel/models/gemini_model.py +30 -2
  6. camel/models/moonshot_model.py +36 -4
  7. camel/models/openai_model.py +29 -15
  8. camel/societies/workforce/prompts.py +25 -15
  9. camel/societies/workforce/role_playing_worker.py +1 -1
  10. camel/societies/workforce/single_agent_worker.py +9 -7
  11. camel/societies/workforce/worker.py +1 -1
  12. camel/societies/workforce/workforce.py +97 -34
  13. camel/storages/vectordb_storages/__init__.py +1 -0
  14. camel/storages/vectordb_storages/surreal.py +415 -0
  15. camel/tasks/task.py +9 -5
  16. camel/toolkits/__init__.py +10 -1
  17. camel/toolkits/base.py +57 -1
  18. camel/toolkits/human_toolkit.py +5 -1
  19. camel/toolkits/hybrid_browser_toolkit/config_loader.py +127 -414
  20. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +783 -1626
  21. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +489 -0
  22. camel/toolkits/markitdown_toolkit.py +2 -2
  23. camel/toolkits/message_integration.py +592 -0
  24. camel/toolkits/note_taking_toolkit.py +195 -26
  25. camel/toolkits/openai_image_toolkit.py +5 -5
  26. camel/toolkits/origene_mcp_toolkit.py +97 -0
  27. camel/toolkits/screenshot_toolkit.py +213 -0
  28. camel/toolkits/search_toolkit.py +161 -79
  29. camel/toolkits/terminal_toolkit.py +379 -165
  30. camel/toolkits/video_analysis_toolkit.py +13 -13
  31. camel/toolkits/video_download_toolkit.py +11 -11
  32. camel/toolkits/web_deploy_toolkit.py +1024 -0
  33. camel/types/enums.py +6 -3
  34. camel/types/unified_model_type.py +16 -4
  35. camel/utils/mcp_client.py +8 -0
  36. camel/utils/tool_result.py +1 -1
  37. {camel_ai-0.2.71a11.dist-info → camel_ai-0.2.72.dist-info}/METADATA +6 -3
  38. {camel_ai-0.2.71a11.dist-info → camel_ai-0.2.72.dist-info}/RECORD +40 -40
  39. camel/toolkits/hybrid_browser_toolkit/actions.py +0 -417
  40. camel/toolkits/hybrid_browser_toolkit/agent.py +0 -311
  41. camel/toolkits/hybrid_browser_toolkit/browser_session.py +0 -739
  42. camel/toolkits/hybrid_browser_toolkit/snapshot.py +0 -227
  43. camel/toolkits/hybrid_browser_toolkit/stealth_script.js +0 -0
  44. camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +0 -1002
  45. {camel_ai-0.2.71a11.dist-info → camel_ai-0.2.72.dist-info}/WHEEL +0 -0
  46. {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, an empty dictionary will be
46
- used. (default: :obj:`{}`)
47
- working_dir (str): The working directory for operations.
48
- If specified, all execution and write operations will be restricted
49
- to this directory. Read operations can access paths outside this
50
- directory.(default: :obj:`"./workspace"`)
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 execution.
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 operations.
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
- working_dir: str = "./workspace",
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 not os.path.exists(working_dir):
95
- os.makedirs(working_dir, exist_ok=True)
96
- self.working_dir = os.path.abspath(working_dir)
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
- "New Python environment created successfully!\n"
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
- if self.cloned_env_path:
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
- python_path = os.path.join(base_path, "python.exe")
730
- pip_path = os.path.join(base_path, "pip.exe")
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
- python_path = os.path.join(base_path, "python")
734
- pip_path = os.path.join(base_path, "pip")
735
- else:
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
- if command.startswith('python'):
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
- for session_id, session in self.shell_sessions.items():
1263
- process = session.get("process")
1264
- if process is not None and session.get("running", False):
1265
- try:
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
- # Give the process a short time to terminate gracefully
1282
- process.wait(timeout=3)
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
- # Mark the session as not running
1293
- session["running"] = False
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
- except Exception as e:
1296
- logger.error(
1297
- f"Error cleaning up process in session "
1298
- f"'{session_id}': {e}"
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
- # Close file output if it exists
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),