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.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +16 -2
- camel/configs/anthropic_config.py +45 -11
- camel/configs/sglang_config.py +7 -5
- camel/datagen/self_improving_cot.py +2 -2
- camel/datagen/self_instruct/self_instruct.py +46 -2
- camel/interpreters/subprocess_interpreter.py +187 -46
- camel/models/__init__.py +2 -0
- camel/models/anthropic_model.py +5 -1
- camel/models/base_audio_model.py +92 -0
- camel/models/fish_audio_model.py +18 -8
- camel/models/model_manager.py +9 -0
- camel/models/openai_audio_models.py +80 -1
- camel/models/sglang_model.py +35 -5
- camel/societies/role_playing.py +119 -0
- camel/toolkits/__init__.py +17 -1
- camel/toolkits/audio_analysis_toolkit.py +238 -0
- camel/toolkits/excel_toolkit.py +172 -0
- camel/toolkits/file_write_toolkit.py +371 -0
- camel/toolkits/image_analysis_toolkit.py +202 -0
- camel/toolkits/mcp_toolkit.py +251 -0
- camel/toolkits/page_script.js +376 -0
- camel/toolkits/terminal_toolkit.py +421 -0
- camel/toolkits/video_analysis_toolkit.py +407 -0
- camel/toolkits/{video_toolkit.py → video_download_toolkit.py} +19 -25
- camel/toolkits/web_toolkit.py +1306 -0
- camel/types/enums.py +3 -0
- {camel_ai-0.2.23a0.dist-info → camel_ai-0.2.25.dist-info}/METADATA +241 -106
- {camel_ai-0.2.23a0.dist-info → camel_ai-0.2.25.dist-info}/RECORD +60 -50
- {camel_ai-0.2.23a0.dist-info → camel_ai-0.2.25.dist-info}/WHEEL +1 -1
- {camel_ai-0.2.23a0.dist-info → camel_ai-0.2.25.dist-info/licenses}/LICENSE +0 -0
|
@@ -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
|
+
]
|