camel-ai 0.2.41__py3-none-any.whl → 0.2.42__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 CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  from camel.logger import disable_logging, enable_logging, set_log_level
16
16
 
17
- __version__ = '0.2.41'
17
+ __version__ = '0.2.42'
18
18
 
19
19
  __all__ = [
20
20
  '__version__',
@@ -327,7 +327,10 @@ class ChatAgent(BaseAgent):
327
327
  return False
328
328
 
329
329
  def update_memory(
330
- self, message: BaseMessage, role: OpenAIBackendRole
330
+ self,
331
+ message: BaseMessage,
332
+ role: OpenAIBackendRole,
333
+ timestamp: Optional[float] = None,
331
334
  ) -> None:
332
335
  r"""Updates the agent memory with a new message.
333
336
 
@@ -335,12 +338,19 @@ class ChatAgent(BaseAgent):
335
338
  message (BaseMessage): The new message to add to the stored
336
339
  messages.
337
340
  role (OpenAIBackendRole): The backend role type.
341
+ timestamp (Optional[float], optional): Custom timestamp for the
342
+ memory record. If None, current timestamp will be used.
343
+ (default: :obj:`None`)
338
344
  """
345
+ from datetime import timezone
346
+
339
347
  self.memory.write_record(
340
348
  MemoryRecord(
341
349
  message=message,
342
350
  role_at_backend=role,
343
- timestamp=datetime.now().timestamp(),
351
+ timestamp=timestamp
352
+ if timestamp is not None
353
+ else datetime.now(timezone.utc).timestamp(),
344
354
  agent_id=self.agent_id,
345
355
  )
346
356
  )
@@ -1331,8 +1341,18 @@ class ChatAgent(BaseAgent):
1331
1341
  tool_call_id=tool_call_id,
1332
1342
  )
1333
1343
 
1334
- self.update_memory(assist_msg, OpenAIBackendRole.ASSISTANT)
1335
- self.update_memory(func_msg, OpenAIBackendRole.FUNCTION)
1344
+ # Use slightly different timestamps to ensure correct ordering
1345
+ # This ensures the assistant message (tool call) always appears before
1346
+ # the function message (tool result) in the conversation context
1347
+ current_time = datetime.now().timestamp()
1348
+ self.update_memory(
1349
+ assist_msg, OpenAIBackendRole.ASSISTANT, timestamp=current_time
1350
+ )
1351
+ self.update_memory(
1352
+ func_msg,
1353
+ OpenAIBackendRole.FUNCTION,
1354
+ timestamp=current_time + 0.001,
1355
+ )
1336
1356
 
1337
1357
  # Record information about this tool call
1338
1358
  tool_record = ToolCallingRecord(
@@ -224,7 +224,7 @@ class SingleStepEnv:
224
224
  raise TypeError(f"Unsupported dataset type: {type(self.dataset)}")
225
225
 
226
226
  async def step(
227
- self, action: Union[Action, List[Action], str]
227
+ self, action: Union[Action, List[Action], str, Dict[int, str]]
228
228
  ) -> Union[
229
229
  Tuple[Observation, float, bool, Dict[str, Any]],
230
230
  List[Tuple[Observation, float, bool, Dict[str, Any]]],
@@ -242,13 +242,15 @@ class SingleStepEnv:
242
242
  the observation will not change.
243
243
 
244
244
  Args:
245
- action (Union[Action, List[Action], str]):
245
+ action (Union[Action, List[Action], str, Dict[int, str]]):
246
246
  The action(s) taken by the agent,
247
247
  which should contain the response(s)
248
248
  to the observation(s). Can be:
249
249
  - A single `Action` object (for batch size 1),
250
250
  - A list of `Action` objects (for batched evaluation),
251
251
  - A raw string (only allowed when batch size is 1).
252
+ - A dict that maps indices to their `llm_response`
253
+ (for batched evaluation)
252
254
 
253
255
  Returns:
254
256
  Union[Tuple[Observation, float, bool, Dict[str, Any]], List[...]]:
@@ -293,6 +295,7 @@ class SingleStepEnv:
293
295
  f"total batch size ({self.current_batch_size})"
294
296
  )
295
297
 
298
+ indices = [act.index for act in actions]
296
299
  proposed_solutions = [act.llm_response for act in actions]
297
300
  ground_truths: List[str] = []
298
301
  for idx in indices:
@@ -334,21 +337,22 @@ class SingleStepEnv:
334
337
  ).as_tuple()
335
338
  for i in range(len(actions))
336
339
  ]
340
+
337
341
  for _, idx in enumerate(indices):
338
342
  self._states_done[idx] = True
339
343
 
340
344
  return step_results[0] if len(step_results) == 1 else step_results
341
345
 
342
346
  def _normalize_actions(
343
- self, action: Union[Action, List[Action], str]
347
+ self, action: Union[Action, List[Action], str, Dict[int, str]]
344
348
  ) -> List[Action]:
345
349
  r"""Normalize the user-provided action(s) into a validated list
346
350
  of `Action` objects.
347
351
 
348
352
  This method handles flexibility in input format by converting
349
- raw strings (only allowed when batch size is 1) and ensuring
350
- all necessary structure and integrity checks on actions
351
- (e.g., index bounds, duplicates).
353
+ raw strings (only allowed when batch size is 1) and dictionaries,
354
+ ensuring all necessary structure and integrity checks on
355
+ actions (e.g., index bounds, duplicates).
352
356
 
353
357
  Args:
354
358
  action (Union[Action, List[Action], str]):
@@ -357,6 +361,7 @@ class SingleStepEnv:
357
361
  - A list of `Action` objects.
358
362
  - A raw string (if `batch_size == 1`), auto-wrapped
359
363
  in an `Action`.
364
+ - A dict mapping int indices to str responses
360
365
 
361
366
  Returns:
362
367
  List[Action]: A list of validated `Action` instances
@@ -368,8 +373,9 @@ class SingleStepEnv:
368
373
  - Action list is empty,
369
374
  - Index mismatches expected values
370
375
  (e.g., 0 for batch size 1),
371
- - Wrong structure is used
372
- (e.g., string used with batch size > 1).
376
+ - Wrong structure is used (e.g.,
377
+ string used with batch size > 1,
378
+ dict used with batch size == 1).
373
379
  TypeError: If the action is of an unsupported type.
374
380
  """
375
381
 
@@ -380,9 +386,20 @@ class SingleStepEnv:
380
386
  " when batch_size == 1"
381
387
  )
382
388
  logger.warning("Auto-converting from str to Action", stacklevel=2)
383
- action = Action(index=0, llm_response=action)
389
+ actions = [Action(index=0, llm_response=action)]
390
+
391
+ elif isinstance(action, dict):
392
+ if not all(isinstance(k, int) for k in action.keys()):
393
+ raise ValueError("All dictionary keys must be integers")
384
394
 
385
- if isinstance(action, Action):
395
+ if self.current_batch_size == 1 and list(action.keys()) != [0]:
396
+ raise ValueError(
397
+ "For batch_size=1, dict input must have exactly one key: 0"
398
+ )
399
+ actions = [
400
+ Action(index=k, llm_response=v) for k, v in action.items()
401
+ ]
402
+ elif isinstance(action, Action):
386
403
  actions = [action]
387
404
  elif isinstance(action, list):
388
405
  if not action:
@@ -397,7 +414,7 @@ class SingleStepEnv:
397
414
 
398
415
  if self.current_batch_size == 1 and len(actions) != 1:
399
416
  raise ValueError(
400
- "For batch_size=1, expect a single Action or a "
417
+ "For batch_size=1, expect a single Action, a dictionary or a "
401
418
  "list containing exactly one Action"
402
419
  )
403
420
 
@@ -162,6 +162,14 @@ class VLLMModel(BaseModelBackend):
162
162
  if response_format:
163
163
  kwargs["response_format"] = {"type": "json_object"}
164
164
 
165
+ # Remove additionalProperties from each tool's function parameters
166
+ if tools and "tools" in kwargs:
167
+ for tool in kwargs["tools"]:
168
+ if "function" in tool and "parameters" in tool["function"]:
169
+ tool["function"]["parameters"].pop(
170
+ "additionalProperties", None
171
+ )
172
+
165
173
  response = await self._async_client.chat.completions.create(
166
174
  messages=messages,
167
175
  model=self.model_type,
@@ -197,6 +205,14 @@ class VLLMModel(BaseModelBackend):
197
205
  if response_format:
198
206
  kwargs["response_format"] = {"type": "json_object"}
199
207
 
208
+ # Remove additionalProperties from each tool's function parameters
209
+ if tools and "tools" in kwargs:
210
+ for tool in kwargs["tools"]:
211
+ if "function" in tool and "parameters" in tool["function"]:
212
+ tool["function"]["parameters"].pop(
213
+ "additionalProperties", None
214
+ )
215
+
200
216
  response = self._client.chat.completions.create(
201
217
  messages=messages,
202
218
  model=self.model_type,
@@ -12,9 +12,16 @@
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
 
15
+ import atexit
15
16
  import os
17
+ import platform
18
+ import queue
16
19
  import subprocess
17
- from typing import Any, Dict, List, Optional
20
+ import sys
21
+ import threading
22
+ import venv
23
+ from queue import Queue
24
+ from typing import Any, Dict, List, Optional, Tuple
18
25
 
19
26
  from camel.logger import get_logger
20
27
  from camel.toolkits.base import BaseToolkit
@@ -36,7 +43,19 @@ class TerminalToolkit(BaseToolkit):
36
43
  timeout (Optional[float]): The timeout for terminal operations.
37
44
  shell_sessions (Optional[Dict[str, Any]]): A dictionary to store
38
45
  shell session information. If None, an empty dictionary will be
39
- used.
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"`)
51
+ need_terminal (bool): Whether to create a terminal interface.
52
+ (default: :obj:`True`)
53
+ use_shell_mode (bool): Whether to use shell mode for command execution.
54
+ (default: :obj:`True`)
55
+ 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`)
40
59
 
41
60
  Note:
42
61
  Most functions are compatible with Unix-based systems (macOS, Linux).
@@ -48,14 +67,260 @@ class TerminalToolkit(BaseToolkit):
48
67
  self,
49
68
  timeout: Optional[float] = None,
50
69
  shell_sessions: Optional[Dict[str, Any]] = None,
70
+ working_dir: str = "./workspace",
71
+ need_terminal: bool = True,
72
+ use_shell_mode: bool = True,
73
+ clone_current_env: bool = False,
74
+ safe_mode: bool = True,
51
75
  ):
52
- import platform
53
-
54
76
  super().__init__(timeout=timeout)
55
77
  self.shell_sessions = shell_sessions or {}
56
- self.os_type = (
57
- platform.system()
58
- ) # 'Windows', 'Darwin' (macOS), 'Linux'
78
+ self.os_type = platform.system()
79
+ self.output_queue: Queue[str] = Queue()
80
+ self.agent_queue: Queue[str] = Queue()
81
+ self.terminal_ready = threading.Event()
82
+ self.gui_thread = None
83
+ self.safe_mode = safe_mode
84
+
85
+ self.cloned_env_path = None
86
+ self.use_shell_mode = use_shell_mode
87
+
88
+ self.python_executable = sys.executable
89
+ self.is_macos = platform.system() == 'Darwin'
90
+
91
+ atexit.register(self.__del__)
92
+
93
+ if not os.path.exists(working_dir):
94
+ os.makedirs(working_dir, exist_ok=True)
95
+ self.working_dir = os.path.abspath(working_dir)
96
+ self._update_terminal_output(
97
+ f"Working directory set to: {self.working_dir}\n"
98
+ )
99
+ if self.safe_mode:
100
+ self._update_terminal_output(
101
+ "Safe mode enabled: Write operations can only "
102
+ "be performed within the working directory\n"
103
+ )
104
+
105
+ if clone_current_env:
106
+ self.cloned_env_path = os.path.join(self.working_dir, ".venv")
107
+ self._clone_current_environment()
108
+ else:
109
+ self.cloned_env_path = None
110
+
111
+ if need_terminal:
112
+ if self.is_macos:
113
+ # macOS uses non-GUI mode
114
+ logger.info("Detected macOS environment, using non-GUI mode")
115
+ self._setup_file_output()
116
+ self.terminal_ready.set()
117
+ else:
118
+ # Other platforms use normal GUI
119
+ self.gui_thread = threading.Thread(
120
+ target=self._create_terminal, daemon=True
121
+ )
122
+ self.gui_thread.start()
123
+ self.terminal_ready.wait(timeout=5)
124
+
125
+ def _setup_file_output(self):
126
+ r"""Set up file output to replace GUI, using a fixed file to simulate
127
+ terminal.
128
+ """
129
+
130
+ self.log_file = os.path.join(os.getcwd(), "camel_terminal.txt")
131
+
132
+ if os.path.exists(self.log_file):
133
+ with open(self.log_file, "w") as f:
134
+ f.truncate(0)
135
+ f.write("CAMEL Terminal Session\n")
136
+ f.write("=" * 50 + "\n")
137
+ f.write(f"Working Directory: {os.getcwd()}\n")
138
+ f.write("=" * 50 + "\n\n")
139
+ else:
140
+ with open(self.log_file, "w") as f:
141
+ f.write("CAMEL Terminal Session\n")
142
+ f.write("=" * 50 + "\n")
143
+ f.write(f"Working Directory: {os.getcwd()}\n")
144
+ f.write("=" * 50 + "\n\n")
145
+
146
+ # Inform the user
147
+ logger.info(f"Terminal output redirected to: {self.log_file}")
148
+
149
+ def file_update(output: str):
150
+ try:
151
+ # Directly append to the end of the file
152
+ with open(self.log_file, "a") as f:
153
+ f.write(output)
154
+ # If the output does not end with a newline, add one
155
+ if output and not output.endswith('\n'):
156
+ f.write('\n')
157
+ # Ensure the agent also receives the output
158
+ self.agent_queue.put(output)
159
+ except Exception as e:
160
+ logger.error(f"Failed to write to terminal: {e}")
161
+
162
+ # Replace the update method
163
+ self._update_terminal_output = file_update
164
+
165
+ def _clone_current_environment(self):
166
+ r"""Create a new Python virtual environment."""
167
+ try:
168
+ if os.path.exists(self.cloned_env_path):
169
+ self._update_terminal_output(
170
+ f"Using existing environment: {self.cloned_env_path}\n"
171
+ )
172
+ return
173
+
174
+ self._update_terminal_output(
175
+ f"Creating new Python environment at:{self.cloned_env_path}\n"
176
+ )
177
+
178
+ venv.create(self.cloned_env_path, with_pip=True)
179
+ self._update_terminal_output(
180
+ "New Python environment created successfully!\n"
181
+ )
182
+
183
+ except Exception as e:
184
+ self._update_terminal_output(
185
+ f"Failed to create environment: {e!s}\n"
186
+ )
187
+ logger.error(f"Failed to create environment: {e}")
188
+
189
+ def _create_terminal(self):
190
+ r"""Create a terminal GUI."""
191
+
192
+ try:
193
+ import tkinter as tk
194
+ from tkinter import scrolledtext
195
+
196
+ def update_terminal():
197
+ try:
198
+ while True:
199
+ output = self.output_queue.get_nowait()
200
+ if isinstance(output, bytes):
201
+ output = output.decode('utf-8', errors='replace')
202
+ self.terminal.insert(tk.END, output)
203
+ self.terminal.see(tk.END)
204
+ except queue.Empty:
205
+ if hasattr(self, 'root') and self.root:
206
+ self.root.after(100, update_terminal)
207
+
208
+ self.root = tk.Tk()
209
+ self.root.title(f"{self.os_type} Terminal")
210
+
211
+ self.root.geometry("800x600")
212
+ self.root.minsize(400, 300)
213
+
214
+ self.terminal = scrolledtext.ScrolledText(
215
+ self.root,
216
+ wrap=tk.WORD,
217
+ bg='black',
218
+ fg='white',
219
+ font=('Consolas', 10),
220
+ insertbackground='white', # Cursor color
221
+ )
222
+ self.terminal.pack(fill=tk.BOTH, expand=True)
223
+
224
+ # Set the handling for closing the window
225
+ def on_closing():
226
+ self.root.quit()
227
+ self.root.destroy()
228
+ self.root = None
229
+
230
+ self.root.protocol("WM_DELETE_WINDOW", on_closing)
231
+
232
+ # Start updating
233
+ update_terminal()
234
+
235
+ # Mark the terminal as ready
236
+ self.terminal_ready.set()
237
+
238
+ # Start the main loop
239
+ self.root.mainloop()
240
+
241
+ except Exception as e:
242
+ logger.error(f"Failed to create terminal: {e}")
243
+ self.terminal_ready.set()
244
+
245
+ def _update_terminal_output(self, output: str):
246
+ r"""Update terminal output and send to agent.
247
+
248
+ Args:
249
+ output (str): The output to be sent to the agent
250
+ """
251
+ try:
252
+ # If it is macOS , only write to file
253
+ if self.is_macos:
254
+ if hasattr(self, 'log_file'):
255
+ with open(self.log_file, "a") as f:
256
+ f.write(output)
257
+ # Ensure the agent also receives the output
258
+ self.agent_queue.put(output)
259
+ return
260
+
261
+ # For other cases, try to update the GUI (if it exists)
262
+ if hasattr(self, 'root') and self.root:
263
+ self.output_queue.put(output)
264
+
265
+ # Always send to agent queue
266
+ self.agent_queue.put(output)
267
+
268
+ except Exception as e:
269
+ logger.error(f"Failed to update terminal output: {e}")
270
+
271
+ def _is_path_within_working_dir(self, path: str) -> bool:
272
+ r"""Check if the path is within the working directory.
273
+
274
+ Args:
275
+ path (str): The path to check
276
+
277
+ Returns:
278
+ bool: Returns True if the path is within the working directory,
279
+ otherwise returns False
280
+ """
281
+ abs_path = os.path.abspath(path)
282
+ return abs_path.startswith(self.working_dir)
283
+
284
+ def _enforce_working_dir_for_execution(self, path: str) -> Optional[str]:
285
+ r"""Enforce working directory restrictions, return error message
286
+ if execution path is not within the working directory.
287
+
288
+ Args:
289
+ path (str): The path to be used for executing operations
290
+
291
+ Returns:
292
+ Optional[str]: Returns error message if the path is not within
293
+ the working directory, otherwise returns None
294
+ """
295
+ if not self._is_path_within_working_dir(path):
296
+ return (
297
+ f"Operation restriction: Execution path {path} must "
298
+ f"be within working directory {self.working_dir}"
299
+ )
300
+ return None
301
+
302
+ def _copy_external_file_to_workdir(
303
+ self, external_file: str
304
+ ) -> Optional[str]:
305
+ r"""Copy external file to working directory.
306
+
307
+ Args:
308
+ external_file (str): The path of the external file
309
+
310
+ Returns:
311
+ Optional[str]: New path after copying to the working directory,
312
+ returns None on failure
313
+ """
314
+ try:
315
+ import shutil
316
+
317
+ filename = os.path.basename(external_file)
318
+ new_path = os.path.join(self.working_dir, filename)
319
+ shutil.copy2(external_file, new_path)
320
+ return new_path
321
+ except Exception as e:
322
+ logger.error(f"Failed to copy file: {e}")
323
+ return None
59
324
 
60
325
  def file_find_in_content(
61
326
  self, file: str, regex: str, sudo: bool = False
@@ -72,6 +337,7 @@ class TerminalToolkit(BaseToolkit):
72
337
  Returns:
73
338
  str: Matching content found in the file.
74
339
  """
340
+
75
341
  if not os.path.exists(file):
76
342
  return f"File not found: {file}"
77
343
 
@@ -80,6 +346,9 @@ class TerminalToolkit(BaseToolkit):
80
346
 
81
347
  command = []
82
348
  if sudo:
349
+ error_msg = self._enforce_working_dir_for_execution(file)
350
+ if error_msg:
351
+ return error_msg
83
352
  command.extend(["sudo"])
84
353
 
85
354
  if self.os_type in ['Darwin', 'Linux']: # macOS or Linux
@@ -119,38 +388,283 @@ class TerminalToolkit(BaseToolkit):
119
388
  else: # Windows
120
389
  # For Windows, we use dir command with /s for recursive search
121
390
  # and /b for bare format
391
+
122
392
  pattern = glob
123
393
  file_path = os.path.join(path, pattern).replace('/', '\\')
124
394
  command.extend(["cmd", "/c", "dir", "/s", "/b", file_path])
125
395
 
126
396
  try:
127
397
  result = subprocess.run(
128
- command, check=False, capture_output=True, text=True
398
+ command,
399
+ check=False,
400
+ capture_output=True,
401
+ text=True,
402
+ shell=False,
129
403
  )
130
- return result.stdout.strip()
404
+
405
+ output = result.stdout.strip()
406
+ if self.os_type == 'Windows':
407
+ output = output.replace('\\', '/')
408
+ return output
131
409
  except subprocess.SubprocessError as e:
132
410
  logger.error(f"Error finding files by name: {e}")
133
411
  return f"Error: {e!s}"
134
412
 
135
- def shell_exec(self, id: str, exec_dir: str, command: str) -> str:
136
- r"""Execute commands in a specified shell session.
413
+ def _sanitize_command(self, command: str, exec_dir: str) -> Tuple:
414
+ r"""Check and modify command to ensure safety.
415
+
416
+ Args:
417
+ command (str): The command to check
418
+ exec_dir (str): The directory to execute the command in
419
+
420
+ Returns:
421
+ Tuple: (is safe, modified command or error message)
422
+ """
423
+ if not self.safe_mode:
424
+ return True, command
425
+
426
+ if not command or command.strip() == "":
427
+ return False, "Empty command"
428
+
429
+ # Use shlex for safer command parsing
430
+ import shlex
431
+
432
+ try:
433
+ parts = shlex.split(command)
434
+ except ValueError as e:
435
+ # Handle malformed commands (e.g., unbalanced quotes)
436
+ return False, f"Invalid command format: {e}"
437
+
438
+ if not parts:
439
+ return False, "Empty command"
440
+
441
+ # Get base command
442
+ base_cmd = parts[0].lower()
443
+
444
+ # Handle special commands
445
+ if base_cmd in ['cd', 'chdir']:
446
+ # Check if cd command attempts to leave the working directory
447
+ if len(parts) > 1:
448
+ target_dir = parts[1].strip('"\'')
449
+ if (
450
+ target_dir.startswith('/')
451
+ or target_dir.startswith('\\')
452
+ or ':' in target_dir
453
+ ):
454
+ # Absolute path
455
+ abs_path = os.path.abspath(target_dir)
456
+ else:
457
+ # Relative path
458
+ abs_path = os.path.abspath(
459
+ os.path.join(exec_dir, target_dir)
460
+ )
461
+
462
+ if not self._is_path_within_working_dir(abs_path):
463
+ return False, (
464
+ f"Safety restriction: Cannot change to directory "
465
+ f"outside of working directory {self.working_dir}"
466
+ )
467
+
468
+ # Check file operation commands
469
+ elif base_cmd in [
470
+ 'rm',
471
+ 'del',
472
+ 'rmdir',
473
+ 'rd',
474
+ 'deltree',
475
+ 'erase',
476
+ 'unlink',
477
+ 'shred',
478
+ 'srm',
479
+ 'wipe',
480
+ 'remove',
481
+ ]:
482
+ # Check targets of delete commands
483
+ for _, part in enumerate(parts[1:], 1):
484
+ if part.startswith('-') or part.startswith(
485
+ '/'
486
+ ): # Skip options
487
+ continue
488
+
489
+ target = part.strip('"\'')
490
+ if (
491
+ target.startswith('/')
492
+ or target.startswith('\\')
493
+ or ':' in target
494
+ ):
495
+ # Absolute path
496
+ abs_path = os.path.abspath(target)
497
+ else:
498
+ # Relative path
499
+ abs_path = os.path.abspath(os.path.join(exec_dir, target))
500
+
501
+ if not self._is_path_within_working_dir(abs_path):
502
+ return False, (
503
+ f"Safety restriction: Cannot delete files outside "
504
+ f"of working directory {self.working_dir}"
505
+ )
506
+
507
+ # Check write/modify commands
508
+ elif base_cmd in [
509
+ 'touch',
510
+ 'mkdir',
511
+ 'md',
512
+ 'echo',
513
+ 'cat',
514
+ 'cp',
515
+ 'copy',
516
+ 'mv',
517
+ 'move',
518
+ 'rename',
519
+ 'ren',
520
+ 'write',
521
+ 'output',
522
+ ]:
523
+ # Check for redirection symbols
524
+ full_cmd = command.lower()
525
+ if '>' in full_cmd:
526
+ # Find the file path after redirection
527
+ redirect_parts = command.split('>')
528
+ if len(redirect_parts) > 1:
529
+ output_file = (
530
+ redirect_parts[1].strip().split()[0].strip('"\'')
531
+ )
532
+ if (
533
+ output_file.startswith('/')
534
+ or output_file.startswith('\\')
535
+ or ':' in output_file
536
+ ):
537
+ # Absolute path
538
+ abs_path = os.path.abspath(output_file)
539
+ else:
540
+ # Relative path
541
+ abs_path = os.path.abspath(
542
+ os.path.join(exec_dir, output_file)
543
+ )
544
+
545
+ if not self._is_path_within_working_dir(abs_path):
546
+ return False, (
547
+ f"Safety restriction: Cannot write to file "
548
+ f"outside of working directory {self.working_dir}"
549
+ )
550
+
551
+ # For cp/mv commands, check target paths
552
+ if base_cmd in ['cp', 'copy', 'mv', 'move']:
553
+ # Simple handling, assuming the last parameter is the target
554
+ if len(parts) > 2:
555
+ target = parts[-1].strip('"\'')
556
+ if (
557
+ target.startswith('/')
558
+ or target.startswith('\\')
559
+ or ':' in target
560
+ ):
561
+ # Absolute path
562
+ abs_path = os.path.abspath(target)
563
+ else:
564
+ # Relative path
565
+ abs_path = os.path.abspath(
566
+ os.path.join(exec_dir, target)
567
+ )
568
+
569
+ if not self._is_path_within_working_dir(abs_path):
570
+ return False, (
571
+ f"Safety restriction: Cannot write to file "
572
+ f"outside of working directory {self.working_dir}"
573
+ )
574
+
575
+ # Check dangerous commands
576
+ elif base_cmd in [
577
+ 'sudo',
578
+ 'su',
579
+ 'chmod',
580
+ 'chown',
581
+ 'chgrp',
582
+ 'passwd',
583
+ 'mkfs',
584
+ 'fdisk',
585
+ 'dd',
586
+ 'shutdown',
587
+ 'reboot',
588
+ 'halt',
589
+ 'poweroff',
590
+ 'init',
591
+ ]:
592
+ return False, (
593
+ f"Safety restriction: Command '{base_cmd}' may affect system "
594
+ f"security and is prohibited"
595
+ )
596
+
597
+ # Check network commands
598
+ elif base_cmd in ['ssh', 'telnet', 'ftp', 'sftp', 'nc', 'netcat']:
599
+ return False, (
600
+ f"Safety restriction: Network command '{base_cmd}' "
601
+ f"is prohibited"
602
+ )
603
+
604
+ # Add copy functionality - copy from external to working directory
605
+ elif base_cmd == 'safecopy':
606
+ # Custom command: safecopy <source file> <target file>
607
+ if len(parts) != 3:
608
+ return False, "Usage: safecopy <source file> <target file>"
609
+
610
+ source = parts[1].strip('\'"')
611
+ target = parts[2].strip('\'"')
612
+
613
+ # Check if source file exists
614
+ if not os.path.exists(source):
615
+ return False, f"Source file does not exist: {source}"
616
+
617
+ # Ensure target is within working directory
618
+ if (
619
+ target.startswith('/')
620
+ or target.startswith('\\')
621
+ or ':' in target
622
+ ):
623
+ # Absolute path
624
+ abs_target = os.path.abspath(target)
625
+ else:
626
+ # Relative path
627
+ abs_target = os.path.abspath(os.path.join(exec_dir, target))
628
+
629
+ if not self._is_path_within_working_dir(abs_target):
630
+ return False, (
631
+ f"Safety restriction: Target file must be within "
632
+ f"working directory {self.working_dir}"
633
+ )
634
+
635
+ # Replace with safe copy command
636
+ if self.os_type == 'Windows':
637
+ return True, f"copy \"{source}\" \"{abs_target}\""
638
+ else:
639
+ return True, f"cp \"{source}\" \"{abs_target}\""
640
+
641
+ return True, command
642
+
643
+ def shell_exec(self, id: str, command: str) -> str:
644
+ r"""Execute commands. This can be used to execute various commands,
645
+ such as writing code, executing code, and running commands.
137
646
 
138
647
  Args:
139
648
  id (str): Unique identifier of the target shell session.
140
- exec_dir (str): Working directory for command execution (must use
141
- absolute path).
142
649
  command (str): Shell command to execute.
143
650
 
144
651
  Returns:
145
652
  str: Output of the command execution or error message.
146
653
  """
147
- if not os.path.isabs(exec_dir):
148
- return f"exec_dir must be an absolute path: {exec_dir}"
654
+ # Command execution must be within the working directory
655
+ error_msg = self._enforce_working_dir_for_execution(self.working_dir)
656
+ if error_msg:
657
+ return error_msg
149
658
 
150
- if not os.path.exists(exec_dir):
151
- return f"Directory not found: {exec_dir}"
659
+ if self.safe_mode:
660
+ is_safe, sanitized_command = self._sanitize_command(
661
+ command, self.working_dir
662
+ )
663
+ if not is_safe:
664
+ return f"Command rejected: {sanitized_command}"
665
+ command = sanitized_command
152
666
 
153
- # If the session doesn't exist, create a new one
667
+ # If the session does not exist, create a new session
154
668
  if id not in self.shell_sessions:
155
669
  self.shell_sessions[id] = {
156
670
  "process": None,
@@ -159,48 +673,97 @@ class TerminalToolkit(BaseToolkit):
159
673
  }
160
674
 
161
675
  try:
162
- # Execute the command in the specified directory
163
- process = subprocess.Popen(
164
- command,
165
- shell=True,
166
- cwd=exec_dir,
167
- stdout=subprocess.PIPE,
168
- stderr=subprocess.PIPE,
169
- stdin=subprocess.PIPE,
170
- text=False,
171
- )
172
-
173
- # Store the process and mark as running
174
- self.shell_sessions[id]["process"] = process
175
- self.shell_sessions[id]["running"] = True
176
- self.shell_sessions[id]["output"] = ""
676
+ # First, log the command to be executed
677
+ self._update_terminal_output(f"\n$ {command}\n")
678
+
679
+ if command.startswith('python') or command.startswith('pip'):
680
+ if self.cloned_env_path:
681
+ if self.os_type == 'Windows':
682
+ base_path = os.path.join(
683
+ self.cloned_env_path, "Scripts"
684
+ )
685
+ python_path = os.path.join(base_path, "python.exe")
686
+ pip_path = os.path.join(base_path, "pip.exe")
687
+ else:
688
+ base_path = os.path.join(self.cloned_env_path, "bin")
689
+ python_path = os.path.join(base_path, "python")
690
+ pip_path = os.path.join(base_path, "pip")
691
+ else:
692
+ python_path = self.python_executable
693
+ pip_path = f'"{python_path}" -m pip'
694
+
695
+ if command.startswith('python'):
696
+ command = command.replace('python', f'"{python_path}"', 1)
697
+ elif command.startswith('pip'):
698
+ command = command.replace('pip', pip_path, 1)
699
+
700
+ if self.is_macos:
701
+ # Type safe version - macOS uses subprocess.run
702
+ process = subprocess.run(
703
+ command,
704
+ shell=True,
705
+ cwd=self.working_dir,
706
+ capture_output=True,
707
+ text=True,
708
+ env=os.environ.copy(),
709
+ )
177
710
 
178
- # Get initial output (non-blocking)
179
- stdout, stderr = "", ""
180
- try:
181
- if process.stdout:
182
- stdout = process.stdout.read().decode('utf-8')
711
+ # Process the output
712
+ output = process.stdout or ""
183
713
  if process.stderr:
184
- stderr = process.stderr.read().decode('utf-8')
185
- except Exception as e:
186
- logger.error(f"Error reading initial output: {e}")
187
- return f"Error: {e!s}"
714
+ output += f"\nStderr Output:\n{process.stderr}"
715
+
716
+ # Update session information and terminal
717
+ self.shell_sessions[id]["output"] = output
718
+ self._update_terminal_output(output + "\n")
719
+
720
+ return output
721
+
722
+ else:
723
+ # Non-macOS systems use the Popen method
724
+ proc = subprocess.Popen(
725
+ command,
726
+ shell=True,
727
+ cwd=self.working_dir,
728
+ stdout=subprocess.PIPE,
729
+ stderr=subprocess.PIPE,
730
+ stdin=subprocess.PIPE,
731
+ text=True,
732
+ bufsize=1,
733
+ universal_newlines=True,
734
+ env=os.environ.copy(),
735
+ )
188
736
 
189
- output = stdout
190
- if stderr:
191
- output += f"\nErrors:\n{stderr}"
737
+ # Store the process and mark it as running
738
+ self.shell_sessions[id]["process"] = proc
739
+ self.shell_sessions[id]["running"] = True
192
740
 
193
- self.shell_sessions[id]["output"] = output
194
- return (
195
- f"Command started in session '{id}'. Initial output: {output}"
196
- )
741
+ # Get output
742
+ stdout, stderr = proc.communicate()
197
743
 
198
- except subprocess.SubprocessError as e:
199
- self.shell_sessions[id]["running"] = False
200
- error_msg = f"Error executing command: {e}"
201
- self.shell_sessions[id]["output"] = error_msg
744
+ output = stdout or ""
745
+ if stderr:
746
+ output += f"\nStderr Output:\n{stderr}"
747
+
748
+ # Update session information and terminal
749
+ self.shell_sessions[id]["output"] = output
750
+ self._update_terminal_output(output + "\n")
751
+
752
+ return output
753
+
754
+ except Exception as e:
755
+ error_msg = f"Command execution error: {e!s}"
202
756
  logger.error(error_msg)
203
- return error_msg
757
+ self._update_terminal_output(f"\nError: {error_msg}\n")
758
+
759
+ # More detailed error information
760
+ import traceback
761
+
762
+ detailed_error = traceback.format_exc()
763
+ return (
764
+ f"Error: {error_msg}\n\n"
765
+ f"Detailed information: {detailed_error}"
766
+ )
204
767
 
205
768
  def shell_view(self, id: str) -> str:
206
769
  r"""View the content of a specified shell session.
@@ -215,50 +778,29 @@ class TerminalToolkit(BaseToolkit):
215
778
  return f"Shell session not found: {id}"
216
779
 
217
780
  session = self.shell_sessions[id]
218
- process = session.get("process")
219
781
 
220
- if process is None:
221
- return f"No active process in session '{id}'"
782
+ try:
783
+ # Check process status
784
+ if session["process"].poll() is not None:
785
+ session["running"] = False
222
786
 
223
- # Try to get any new output
224
- if session["running"] and process.poll() is None:
787
+ # Collect all new output from agent queue
788
+ new_output = ""
225
789
  try:
226
- # Non-blocking read from stdout/stderr
227
- stdout_data, stderr_data = "", ""
228
- if process.stdout and process.stdout.readable():
229
- stdout_data = process.stdout.read1().decode('utf-8')
230
- if process.stderr and process.stderr.readable():
231
- stderr_data = process.stderr.read1().decode('utf-8')
232
-
233
- if stdout_data:
234
- session["output"] += stdout_data
235
- if stderr_data:
236
- session["output"] += f"\nErrors:\n{stderr_data}"
237
- except Exception as e:
238
- logger.error(f"Error getting process output: {e}")
239
- return f"Error: {e!s}"
790
+ while True:
791
+ output = self.agent_queue.get_nowait()
792
+ new_output += output
793
+ session["output"] += output
794
+ except queue.Empty:
795
+ pass
240
796
 
241
- # Check if the process has completed
242
- if process.poll() is not None and session["running"]:
243
- try:
244
- # Get remaining output if any
245
- stdout_data, stderr_data = "", ""
246
- if process.stdout and process.stdout.readable():
247
- stdout_data = process.stdout.read().decode('utf-8')
248
- if process.stderr and process.stderr.readable():
249
- stderr_data = process.stderr.read().decode('utf-8')
250
-
251
- if stdout_data:
252
- session["output"] += stdout_data
253
- if stderr_data:
254
- session["output"] += f"\nErrors:\n{stderr_data}"
255
- except Exception as e:
256
- logger.error(f"Error getting final process output: {e}")
257
- return f"Error: {e!s}"
258
- finally:
259
- session["running"] = False
797
+ return new_output or session["output"]
260
798
 
261
- return session["output"]
799
+ except Exception as e:
800
+ error_msg = f"Error reading terminal output: {e}"
801
+ self._update_terminal_output(f"\nError: {error_msg}\n")
802
+ logger.error(error_msg)
803
+ return f"Error: {e!s}"
262
804
 
263
805
  def shell_wait(self, id: str, seconds: Optional[int] = None) -> str:
264
806
  r"""Wait for the running process in a specified shell session to
@@ -281,33 +823,40 @@ class TerminalToolkit(BaseToolkit):
281
823
  if process is None:
282
824
  return f"No active process in session '{id}'"
283
825
 
284
- if not session["running"]:
826
+ if not session["running"] or process.poll() is not None:
285
827
  return f"Process in session '{id}' is not running"
286
828
 
287
829
  try:
288
- # Use communicate with timeout
289
- stdout, stderr = process.communicate(timeout=seconds)
290
-
291
- if stdout:
292
- stdout_str = (
293
- stdout.decode('utf-8')
294
- if isinstance(stdout, bytes)
295
- else stdout
830
+ if hasattr(process, 'communicate'):
831
+ # Use communicate with timeout
832
+ stdout, stderr = process.communicate(timeout=seconds)
833
+
834
+ if stdout:
835
+ stdout_str = (
836
+ stdout.decode('utf-8')
837
+ if isinstance(stdout, bytes)
838
+ else stdout
839
+ )
840
+ session["output"] += stdout_str
841
+ if stderr:
842
+ stderr_str = (
843
+ stderr.decode('utf-8')
844
+ if isinstance(stderr, bytes)
845
+ else stderr
846
+ )
847
+ if stderr_str:
848
+ session["output"] += f"\nStderr Output:\n{stderr_str}"
849
+
850
+ session["running"] = False
851
+ return (
852
+ f"Process completed in session '{id}'. "
853
+ f"Output: {session['output']}"
296
854
  )
297
- session["output"] += stdout_str
298
- if stderr:
299
- stderr_str = (
300
- stderr.decode('utf-8')
301
- if isinstance(stderr, bytes)
302
- else stderr
855
+ else:
856
+ return (
857
+ f"Process already completed in session '{id}'. "
858
+ f"Output: {session['output']}"
303
859
  )
304
- session["output"] += f"\nErrors:\n{stderr_str}"
305
-
306
- session["running"] = False
307
- return (
308
- f"Process completed in session '{id}'. "
309
- f"Output: {session['output']}"
310
- )
311
860
 
312
861
  except subprocess.TimeoutExpired:
313
862
  return (
@@ -404,6 +953,71 @@ class TerminalToolkit(BaseToolkit):
404
953
  logger.error(f"Error killing process: {e}")
405
954
  return f"Error killing process: {e!s}"
406
955
 
956
+ def __del__(self):
957
+ r"""Clean up resources when the object is being destroyed.
958
+ Terminates all running processes and closes any open file handles.
959
+ """
960
+ # Log that cleanup is starting
961
+ logger.info("TerminalToolkit cleanup initiated")
962
+
963
+ # Clean up all processes in shell sessions
964
+ for session_id, session in self.shell_sessions.items():
965
+ process = session.get("process")
966
+ if process is not None and session.get("running", False):
967
+ try:
968
+ logger.info(
969
+ f"Terminating process in session '{session_id}'"
970
+ )
971
+
972
+ # Close process input/output streams if open
973
+ if (
974
+ hasattr(process, 'stdin')
975
+ and process.stdin
976
+ and not process.stdin.closed
977
+ ):
978
+ process.stdin.close()
979
+
980
+ # Terminate the process
981
+ process.terminate()
982
+ try:
983
+ # Give the process a short time to terminate gracefully
984
+ process.wait(timeout=3)
985
+ except subprocess.TimeoutExpired:
986
+ # Force kill if the process doesn't terminate
987
+ # gracefully
988
+ logger.warning(
989
+ f"Process in session '{session_id}' did not "
990
+ f"terminate gracefully, forcing kill"
991
+ )
992
+ process.kill()
993
+
994
+ # Mark the session as not running
995
+ session["running"] = False
996
+
997
+ except Exception as e:
998
+ logger.error(
999
+ f"Error cleaning up process in session "
1000
+ f"'{session_id}': {e}"
1001
+ )
1002
+
1003
+ # Close file output if it exists
1004
+ if hasattr(self, 'log_file') and self.is_macos:
1005
+ try:
1006
+ logger.info(f"Final terminal log saved to: {self.log_file}")
1007
+ except Exception as e:
1008
+ logger.error(f"Error logging file information: {e}")
1009
+
1010
+ # Clean up GUI resources if they exist
1011
+ if hasattr(self, 'root') and self.root:
1012
+ try:
1013
+ logger.info("Closing terminal GUI")
1014
+ self.root.quit()
1015
+ self.root.destroy()
1016
+ except Exception as e:
1017
+ logger.error(f"Error closing terminal GUI: {e}")
1018
+
1019
+ logger.info("TerminalToolkit cleanup completed")
1020
+
407
1021
  def get_tools(self) -> List[FunctionTool]:
408
1022
  r"""Returns a list of FunctionTool objects representing the functions
409
1023
  in the toolkit.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: camel-ai
3
- Version: 0.2.41
3
+ Version: 0.2.42
4
4
  Summary: Communicative Agents for AI Society Study
5
5
  Project-URL: Homepage, https://www.camel-ai.org/
6
6
  Project-URL: Repository, https://github.com/camel-ai/camel
@@ -1,4 +1,4 @@
1
- camel/__init__.py,sha256=Pvojrkcw9OdbaLHL6wyoc5VwLbd_tJsRg58wVSrP2cs,912
1
+ camel/__init__.py,sha256=OFoHvtZQPjUxj6ga19ljZ11Bxul4-gtgGIZnBFLOBvU,912
2
2
  camel/generators.py,sha256=JRqj9_m1PF4qT6UtybzTQ-KBT9MJQt18OAAYvQ_fr2o,13844
3
3
  camel/human.py,sha256=9X09UmxI2JqQnhrFfnZ3B9EzFmVfdSWQcjLWTIXKXe0,4962
4
4
  camel/logger.py,sha256=rZVeOVYuQ9RYJ5Tqyv0usqy0g4zaVEq4qSfZ9nd2640,5755
@@ -7,7 +7,7 @@ camel/agents/__init__.py,sha256=OEWDfxima22hjSZWgIu6sn9O_QKBr6cpS8b1Jywou0A,1596
7
7
  camel/agents/_types.py,sha256=ryPRmEXnpNtbFT23GoAcwK-zxWWsIOqYu64mxMx_PhI,1430
8
8
  camel/agents/_utils.py,sha256=AR7Qqgbkmn4X2edYUQf1rdksGUyV5hm3iK1z-Dn0Mcg,6266
9
9
  camel/agents/base.py,sha256=c4bJYL3G3Z41SaFdMPMn8ZjLdFiFaVOFO6EQIfuCVR8,1124
10
- camel/agents/chat_agent.py,sha256=WfHK6ZNVxnI-AaseWVNORAUlinCobHsByGu0ZSYIr2M,51267
10
+ camel/agents/chat_agent.py,sha256=74Wd9ESQqepLrGcJIo0lU-5QAHCHIB7zS0-NiqiC7HA,52035
11
11
  camel/agents/critic_agent.py,sha256=qFVlHlQo0CVgmPWfWYLT8_oP_KyzCLFsQw_nN_vu5Bs,7487
12
12
  camel/agents/deductive_reasoner_agent.py,sha256=6BZGaq1hR6hKJuQtOfoYQnk_AkZpw_Mr7mUy2MspQgs,13540
13
13
  camel/agents/embodied_agent.py,sha256=XBxBu5ZMmSJ4B2U3Z7SMwvLlgp6yNpaBe8HNQmY9CZA,7536
@@ -106,7 +106,7 @@ camel/embeddings/vlm_embedding.py,sha256=HZFdcz1YzkFPzMj45_jaCVmDQJyccoXN561aLWl
106
106
  camel/environments/__init__.py,sha256=nWFEYK-QcRX0WskLVsXn4iP_pK2liL-iHKG7DSO0zTU,969
107
107
  camel/environments/models.py,sha256=jVcCyU7xObKoWPnkshmPqyyKi3AOiMVVtUZA-tWEYUU,4194
108
108
  camel/environments/multi_step.py,sha256=rEEMMHip5ZVEWpNj7miws5wALlujtUbFZkWDsy7ofHM,8360
109
- camel/environments/single_step.py,sha256=JtFH17v3KM5aRjP_WzNXGCvRx1N5ryTZm0voZgPFAkw,18855
109
+ camel/environments/single_step.py,sha256=ELZ5GViw9eFCjQMytPMaLWHbAZZAqUCmNsk9h6qbggU,19713
110
110
  camel/extractors/__init__.py,sha256=lgtDl8zWvN826fJVKqRv05w556YZ-EdrHwdzKphywgA,1097
111
111
  camel/extractors/base.py,sha256=3jvuZpq27nlADDCX3GfubOpeb_zt-E9rzxF3x4lYm8s,10404
112
112
  camel/extractors/python_strategies.py,sha256=k8q4BIAhPZnCSN2LqPaZVrhF56y3Y4cZ6ddn79jcIXE,7825
@@ -180,7 +180,7 @@ camel/models/sglang_model.py,sha256=kdF-qShOH4j5lkuC2JEzUByuSoKtCFHdrIpAdbpR2Gg,
180
180
  camel/models/siliconflow_model.py,sha256=c40e0SQjHUNjr1ttJTTRTylRiNsPK_idP7Pa2iZr36g,6041
181
181
  camel/models/stub_model.py,sha256=JvjeEkXS7RMcR_UA_64a3T6S0QALUhOaMQs-aI7Etug,5955
182
182
  camel/models/togetherai_model.py,sha256=nV6ZqOrwEK6oNU1gTJlPfJDQbd9Mcl4GWkbqYnGPCQ0,7049
183
- camel/models/vllm_model.py,sha256=QZU7K6exbvEVVG8s50vxkSBQ-ppYLe-gF23-a6_XbnI,8098
183
+ camel/models/vllm_model.py,sha256=lbcflI19mnvMt62hvpdDpfIJqYo0fTsd-pEZC_H-akA,8826
184
184
  camel/models/volcano_model.py,sha256=inYDiKOfGvq8o3XW4KVQIrXiZOhXQfB4HfCHGCWHPKs,3792
185
185
  camel/models/yi_model.py,sha256=sg7qOzvWZlGeKmlvA4kvZSWwMxTBo0-qgvEVjBalXcE,6572
186
186
  camel/models/zhipuai_model.py,sha256=JtMOTDsJIteZBPdIAwkeyUoAoh3O0dseaqjikiwjIfM,6909
@@ -312,7 +312,7 @@ camel/toolkits/semantic_scholar_toolkit.py,sha256=Rh7eA_YPxV5pvPIzhjjvpr3vtlaCni
312
312
  camel/toolkits/slack_toolkit.py,sha256=F1Xn2_Jmnv-1SdBnCNg3MI3RGwjZ7ZDWiNZjFJJS6x8,10791
313
313
  camel/toolkits/stripe_toolkit.py,sha256=07swo5znGTnorafC1uYLKB4NRcJIOPOx19J7tkpLYWk,10102
314
314
  camel/toolkits/sympy_toolkit.py,sha256=dkzGp7C7Oy-qP1rVziEk_ZOPRb37d5LoI7JKCLiTEo4,33758
315
- camel/toolkits/terminal_toolkit.py,sha256=kU6eOX9Ngysjh42jlgNyWhHYtDqf2CfZJH6bdwhSw-s,15228
315
+ camel/toolkits/terminal_toolkit.py,sha256=gupuTvNkwnFzcFwDB_irSJ9-dXRr8yEAsYq5ChEkkHg,37230
316
316
  camel/toolkits/thinking_toolkit.py,sha256=NyA6rDFG-WbCNt7NFODBTpqOIDtP6he6GhnZpPlA2To,8001
317
317
  camel/toolkits/twitter_toolkit.py,sha256=j14Hxpt4XTEOF6dWpGm6Ot_vkYT7WOiLrUa1d2JT05U,15850
318
318
  camel/toolkits/video_analysis_toolkit.py,sha256=EqZnut9p0W5bgF1YpdGoaRJFDLHM2Ls8i9ApTQz44tA,15088
@@ -368,7 +368,7 @@ camel/verifiers/base.py,sha256=SQGZPP6p08q4Qmpr1vD-eb0UxBwkl1hpZSm19yV2wWo,14866
368
368
  camel/verifiers/math_verifier.py,sha256=tA1D4S0sm8nsWISevxSN0hvSVtIUpqmJhzqfbuMo0y4,6875
369
369
  camel/verifiers/models.py,sha256=GdxYPr7UxNrR1577yW4kyroRcLGfd-H1GXgv8potDWU,2471
370
370
  camel/verifiers/python_verifier.py,sha256=o1VVINq4YQqNPz8h1FiZ7v2dMBN3wlH6AsRWPqN0iGQ,18037
371
- camel_ai-0.2.41.dist-info/METADATA,sha256=nZHt9rZqnrURzdvTIMWAPIGpKbo60IVfGFu7Za_F8Jo,42033
372
- camel_ai-0.2.41.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
373
- camel_ai-0.2.41.dist-info/licenses/LICENSE,sha256=id0nB2my5kG0xXeimIu5zZrbHLS6EQvxvkKkzIHaT2k,11343
374
- camel_ai-0.2.41.dist-info/RECORD,,
371
+ camel_ai-0.2.42.dist-info/METADATA,sha256=Yi_gfzN4krlk4P1Ek1XKLMuXldj0Xwu5TdddMd-pQqc,42033
372
+ camel_ai-0.2.42.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
373
+ camel_ai-0.2.42.dist-info/licenses/LICENSE,sha256=id0nB2my5kG0xXeimIu5zZrbHLS6EQvxvkKkzIHaT2k,11343
374
+ camel_ai-0.2.42.dist-info/RECORD,,