camel-ai 0.2.23a0__py3-none-any.whl → 0.2.25__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.

@@ -0,0 +1,421 @@
1
+ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
+
15
+ import os
16
+ import subprocess
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ from camel.logger import get_logger
20
+ from camel.toolkits.base import BaseToolkit
21
+ from camel.toolkits.function_tool import FunctionTool
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ class TerminalToolkit(BaseToolkit):
27
+ r"""A toolkit for terminal operations across multiple operating systems.
28
+
29
+ This toolkit provides a set of functions for terminal operations such as
30
+ searching for files by name or content, executing shell commands, and
31
+ managing terminal sessions.
32
+
33
+ Args:
34
+ timeout (Optional[float]): The timeout for terminal operations.
35
+ shell_sessions (Optional[Dict[str, Any]]): A dictionary to store
36
+ shell session information. If None, an empty dictionary will be
37
+ used.
38
+
39
+ Note:
40
+ Most functions are compatible with Unix-based systems (macOS, Linux).
41
+ For Windows compatibility, additional implementation details are
42
+ needed.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ timeout: Optional[float] = None,
48
+ shell_sessions: Optional[Dict[str, Any]] = None,
49
+ ):
50
+ import platform
51
+
52
+ super().__init__(timeout=timeout)
53
+ self.shell_sessions = shell_sessions or {}
54
+ self.os_type = (
55
+ platform.system()
56
+ ) # 'Windows', 'Darwin' (macOS), 'Linux'
57
+
58
+ def file_find_in_content(
59
+ self, file: str, regex: str, sudo: bool = False
60
+ ) -> str:
61
+ r"""Search for matching text within file content.
62
+
63
+ Args:
64
+ file (str): Absolute path of the file to search within.
65
+ regex (str): Regular expression pattern to match.
66
+ sudo (bool, optional): Whether to use sudo privileges. Defaults to
67
+ False. Note: Using sudo requires the process to have
68
+ appropriate permissions.
69
+
70
+ Returns:
71
+ str: Matching content found in the file.
72
+ """
73
+ if not os.path.exists(file):
74
+ return f"File not found: {file}"
75
+
76
+ if not os.path.isfile(file):
77
+ return f"The path provided is not a file: {file}"
78
+
79
+ command = []
80
+ if sudo:
81
+ command.extend(["sudo"])
82
+
83
+ if self.os_type in ['Darwin', 'Linux']: # macOS or Linux
84
+ command.extend(["grep", "-E", regex, file])
85
+ else: # Windows
86
+ # For Windows, we could use PowerShell or findstr
87
+ command.extend(["findstr", "/R", regex, file])
88
+
89
+ try:
90
+ result = subprocess.run(
91
+ command, check=False, capture_output=True, text=True
92
+ )
93
+ return result.stdout.strip()
94
+ except subprocess.SubprocessError as e:
95
+ logger.error(f"Error searching in file content: {e}")
96
+ return f"Error: {e!s}"
97
+
98
+ def file_find_by_name(self, path: str, glob: str) -> str:
99
+ r"""Find files by name pattern in specified directory.
100
+
101
+ Args:
102
+ path (str): Absolute path of directory to search.
103
+ glob (str): Filename pattern using glob syntax wildcards.
104
+
105
+ Returns:
106
+ str: List of files matching the pattern.
107
+ """
108
+ if not os.path.exists(path):
109
+ return f"Directory not found: {path}"
110
+
111
+ if not os.path.isdir(path):
112
+ return f"The path provided is not a directory: {path}"
113
+
114
+ command = []
115
+ if self.os_type in ['Darwin', 'Linux']: # macOS or Linux
116
+ command.extend(["find", path, "-name", glob])
117
+ else: # Windows
118
+ # For Windows, we use dir command with /s for recursive search
119
+ # and /b for bare format
120
+
121
+ pattern = glob
122
+ command.extend(["dir", "/s", "/b", os.path.join(path, pattern)])
123
+
124
+ try:
125
+ result = subprocess.run(
126
+ command, check=False, capture_output=True, text=True
127
+ )
128
+ return result.stdout.strip()
129
+ except subprocess.SubprocessError as e:
130
+ logger.error(f"Error finding files by name: {e}")
131
+ return f"Error: {e!s}"
132
+
133
+ def shell_exec(self, id: str, exec_dir: str, command: str) -> str:
134
+ r"""Execute commands in a specified shell session.
135
+
136
+ Args:
137
+ id (str): Unique identifier of the target shell session.
138
+ exec_dir (str): Working directory for command execution (must use
139
+ absolute path).
140
+ command (str): Shell command to execute.
141
+
142
+ Returns:
143
+ str: Output of the command execution or error message.
144
+ """
145
+ if not os.path.isabs(exec_dir):
146
+ return f"exec_dir must be an absolute path: {exec_dir}"
147
+
148
+ if not os.path.exists(exec_dir):
149
+ return f"Directory not found: {exec_dir}"
150
+
151
+ # If the session doesn't exist, create a new one
152
+ if id not in self.shell_sessions:
153
+ self.shell_sessions[id] = {
154
+ "process": None,
155
+ "output": "",
156
+ "running": False,
157
+ }
158
+
159
+ try:
160
+ # Execute the command in the specified directory
161
+ process = subprocess.Popen(
162
+ command,
163
+ shell=True,
164
+ cwd=exec_dir,
165
+ stdout=subprocess.PIPE,
166
+ stderr=subprocess.PIPE,
167
+ stdin=subprocess.PIPE,
168
+ text=False,
169
+ )
170
+
171
+ # Store the process and mark as running
172
+ self.shell_sessions[id]["process"] = process
173
+ self.shell_sessions[id]["running"] = True
174
+ self.shell_sessions[id]["output"] = ""
175
+
176
+ # Get initial output (non-blocking)
177
+ stdout, stderr = "", ""
178
+ try:
179
+ if process.stdout:
180
+ stdout = process.stdout.read().decode('utf-8')
181
+ if process.stderr:
182
+ stderr = process.stderr.read().decode('utf-8')
183
+ except Exception as e:
184
+ logger.error(f"Error reading initial output: {e}")
185
+ return f"Error: {e!s}"
186
+
187
+ output = stdout
188
+ if stderr:
189
+ output += f"\nErrors:\n{stderr}"
190
+
191
+ self.shell_sessions[id]["output"] = output
192
+ return (
193
+ f"Command started in session '{id}'. Initial output: {output}"
194
+ )
195
+
196
+ except subprocess.SubprocessError as e:
197
+ self.shell_sessions[id]["running"] = False
198
+ error_msg = f"Error executing command: {e}"
199
+ self.shell_sessions[id]["output"] = error_msg
200
+ logger.error(error_msg)
201
+ return error_msg
202
+
203
+ def shell_view(self, id: str) -> str:
204
+ r"""View the content of a specified shell session.
205
+
206
+ Args:
207
+ id (str): Unique identifier of the target shell session.
208
+
209
+ Returns:
210
+ str: Current output content of the shell session.
211
+ """
212
+ if id not in self.shell_sessions:
213
+ return f"Shell session not found: {id}"
214
+
215
+ session = self.shell_sessions[id]
216
+ process = session.get("process")
217
+
218
+ if process is None:
219
+ return f"No active process in session '{id}'"
220
+
221
+ # Try to get any new output
222
+ if session["running"] and process.poll() is None:
223
+ try:
224
+ # Non-blocking read from stdout/stderr
225
+ stdout_data, stderr_data = "", ""
226
+ if process.stdout and process.stdout.readable():
227
+ stdout_data = process.stdout.read1().decode('utf-8')
228
+ if process.stderr and process.stderr.readable():
229
+ stderr_data = process.stderr.read1().decode('utf-8')
230
+
231
+ if stdout_data:
232
+ session["output"] += stdout_data
233
+ if stderr_data:
234
+ session["output"] += f"\nErrors:\n{stderr_data}"
235
+ except Exception as e:
236
+ logger.error(f"Error getting process output: {e}")
237
+ return f"Error: {e!s}"
238
+
239
+ # Check if the process has completed
240
+ if process.poll() is not None and session["running"]:
241
+ try:
242
+ # Get remaining output if any
243
+ stdout_data, stderr_data = "", ""
244
+ if process.stdout and process.stdout.readable():
245
+ stdout_data = process.stdout.read().decode('utf-8')
246
+ if process.stderr and process.stderr.readable():
247
+ stderr_data = process.stderr.read().decode('utf-8')
248
+
249
+ if stdout_data:
250
+ session["output"] += stdout_data
251
+ if stderr_data:
252
+ session["output"] += f"\nErrors:\n{stderr_data}"
253
+ except Exception as e:
254
+ logger.error(f"Error getting final process output: {e}")
255
+ return f"Error: {e!s}"
256
+ finally:
257
+ session["running"] = False
258
+
259
+ return session["output"]
260
+
261
+ def shell_wait(self, id: str, seconds: Optional[int] = None) -> str:
262
+ r"""Wait for the running process in a specified shell session to
263
+ return.
264
+
265
+ Args:
266
+ id (str): Unique identifier of the target shell session.
267
+ seconds (Optional[int], optional): Wait duration in seconds.
268
+ If None, wait indefinitely. Defaults to None.
269
+
270
+ Returns:
271
+ str: Final output content after waiting.
272
+ """
273
+ if id not in self.shell_sessions:
274
+ return f"Shell session not found: {id}"
275
+
276
+ session = self.shell_sessions[id]
277
+ process = session.get("process")
278
+
279
+ if process is None:
280
+ return f"No active process in session '{id}'"
281
+
282
+ if not session["running"]:
283
+ return f"Process in session '{id}' is not running"
284
+
285
+ try:
286
+ # Use communicate with timeout
287
+ stdout, stderr = process.communicate(timeout=seconds)
288
+
289
+ if stdout:
290
+ stdout_str = (
291
+ stdout.decode('utf-8')
292
+ if isinstance(stdout, bytes)
293
+ else stdout
294
+ )
295
+ session["output"] += stdout_str
296
+ if stderr:
297
+ stderr_str = (
298
+ stderr.decode('utf-8')
299
+ if isinstance(stderr, bytes)
300
+ else stderr
301
+ )
302
+ session["output"] += f"\nErrors:\n{stderr_str}"
303
+
304
+ session["running"] = False
305
+ return (
306
+ f"Process completed in session '{id}'. "
307
+ f"Output: {session['output']}"
308
+ )
309
+
310
+ except subprocess.TimeoutExpired:
311
+ return (
312
+ f"Process in session '{id}' is still running "
313
+ f"after {seconds} seconds"
314
+ )
315
+ except Exception as e:
316
+ logger.error(f"Error waiting for process: {e}")
317
+ return f"Error waiting for process: {e!s}"
318
+
319
+ def shell_write_to_process(
320
+ self, id: str, input: str, press_enter: bool
321
+ ) -> str:
322
+ r"""Write input to a running process in a specified shell session.
323
+
324
+ Args:
325
+ id (str): Unique identifier of the target shell session.
326
+ input (str): Input content to write to the process.
327
+ press_enter (bool): Whether to press Enter key after input.
328
+
329
+ Returns:
330
+ str: Status message indicating whether the input was sent.
331
+ """
332
+ if id not in self.shell_sessions:
333
+ return f"Shell session not found: {id}"
334
+
335
+ session = self.shell_sessions[id]
336
+ process = session.get("process")
337
+
338
+ if process is None:
339
+ return f"No active process in session '{id}'"
340
+
341
+ if not session["running"] or process.poll() is not None:
342
+ return f"Process in session '{id}' is not running"
343
+
344
+ try:
345
+ if not process.stdin or process.stdin.closed:
346
+ return (
347
+ f"Cannot write to process in session '{id}': "
348
+ f"stdin is closed"
349
+ )
350
+
351
+ if press_enter:
352
+ input = input + "\n"
353
+
354
+ # Write bytes to stdin
355
+ process.stdin.write(input.encode('utf-8'))
356
+ process.stdin.flush()
357
+
358
+ return f"Input sent to process in session '{id}'"
359
+ except Exception as e:
360
+ logger.error(f"Error writing to process: {e}")
361
+ return f"Error writing to process: {e!s}"
362
+
363
+ def shell_kill_process(self, id: str) -> str:
364
+ r"""Terminate a running process in a specified shell session.
365
+
366
+ Args:
367
+ id (str): Unique identifier of the target shell session.
368
+
369
+ Returns:
370
+ str: Status message indicating whether the process was terminated.
371
+ """
372
+ if id not in self.shell_sessions:
373
+ return f"Shell session not found: {id}"
374
+
375
+ session = self.shell_sessions[id]
376
+ process = session.get("process")
377
+
378
+ if process is None:
379
+ return f"No active process in session '{id}'"
380
+
381
+ if not session["running"] or process.poll() is not None:
382
+ return f"Process in session '{id}' is not running"
383
+
384
+ try:
385
+ # Clean up process resources before termination
386
+ if process.stdin and not process.stdin.closed:
387
+ process.stdin.close()
388
+
389
+ process.terminate()
390
+ try:
391
+ process.wait(timeout=5)
392
+ except subprocess.TimeoutExpired:
393
+ logger.warning(
394
+ f"Process in session '{id}' did not terminate gracefully"
395
+ f", forcing kill"
396
+ )
397
+ process.kill()
398
+
399
+ session["running"] = False
400
+ return f"Process in session '{id}' has been terminated"
401
+ except Exception as e:
402
+ logger.error(f"Error killing process: {e}")
403
+ return f"Error killing process: {e!s}"
404
+
405
+ def get_tools(self) -> List[FunctionTool]:
406
+ r"""Returns a list of FunctionTool objects representing the functions
407
+ in the toolkit.
408
+
409
+ Returns:
410
+ List[FunctionTool]: A list of FunctionTool objects representing the
411
+ functions in the toolkit.
412
+ """
413
+ return [
414
+ FunctionTool(self.file_find_in_content),
415
+ FunctionTool(self.file_find_by_name),
416
+ FunctionTool(self.shell_exec),
417
+ FunctionTool(self.shell_view),
418
+ FunctionTool(self.shell_wait),
419
+ FunctionTool(self.shell_write_to_process),
420
+ FunctionTool(self.shell_kill_process),
421
+ ]