camel-ai 0.2.76a14__py3-none-any.whl → 0.2.78__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.

@@ -495,6 +495,11 @@ class WorkforceLogger:
495
495
 
496
496
  tasks_handled_by_worker: Dict[str, int] = {}
497
497
 
498
+ # Track unique task final states to avoid double-counting
499
+ task_final_states: Dict[
500
+ str, str
501
+ ] = {} # task_id -> 'completed' or 'failed'
502
+
498
503
  # Helper function to check if a task is the main task (has no parent)
499
504
  def is_main_task(task_id: str) -> bool:
500
505
  return (
@@ -528,7 +533,9 @@ class WorkforceLogger:
528
533
  elif event_type == 'task_completed':
529
534
  # Exclude main task from total count
530
535
  if not is_main_task(task_id):
531
- kpis['total_tasks_completed'] += 1
536
+ # Track final state - a completed task overwrites any
537
+ # previous failed state
538
+ task_final_states[task_id] = 'completed'
532
539
  # Count tasks handled by worker (only for non-main tasks)
533
540
  if 'worker_id' in entry and entry['worker_id'] is not None:
534
541
  worker_id = entry['worker_id']
@@ -550,7 +557,11 @@ class WorkforceLogger:
550
557
  elif event_type == 'task_failed':
551
558
  # Exclude main task from total count
552
559
  if not is_main_task(task_id):
553
- kpis['total_tasks_failed'] += 1
560
+ # Only track as failed if not already completed
561
+ # (in case of retries, the final completion overwrites
562
+ # failed state)
563
+ if task_final_states.get(task_id) != 'completed':
564
+ task_final_states[task_id] = 'failed'
554
565
  # Count tasks handled by worker (only for non-main tasks)
555
566
  if 'worker_id' in entry and entry['worker_id'] is not None:
556
567
  worker_id = entry['worker_id']
@@ -565,6 +576,14 @@ class WorkforceLogger:
565
576
  kpis['total_workforce_running_time_seconds'] = (
566
577
  last_timestamp - first_timestamp
567
578
  ).total_seconds()
579
+
580
+ # Count unique tasks by final state
581
+ for _task_id, state in task_final_states.items():
582
+ if state == 'completed':
583
+ kpis['total_tasks_completed'] += 1
584
+ elif state == 'failed':
585
+ kpis['total_tasks_failed'] += 1
586
+
568
587
  # Calculate worker utilization based on proportion of tasks handled
569
588
  total_tasks_processed_for_utilization = (
570
589
  kpis['total_tasks_completed'] + kpis['total_tasks_failed']
@@ -610,9 +629,9 @@ class WorkforceLogger:
610
629
 
611
630
  kpis['total_workers_created'] = len(self._worker_information)
612
631
 
613
- # Current pending tasks (simplified)
614
- kpis['current_pending_tasks'] = kpis['total_tasks_created'] - (
615
- kpis['total_tasks_completed'] + kpis['total_tasks_failed']
632
+ # Current pending tasks - tasks created but not yet completed or failed
633
+ kpis['current_pending_tasks'] = kpis['total_tasks_created'] - len(
634
+ task_final_states
616
635
  )
617
636
 
618
637
  return kpis
@@ -191,7 +191,7 @@ class ContextSummarizerToolkit(BaseToolkit):
191
191
  summary (str): The summary text to save.
192
192
 
193
193
  Returns:
194
- str: Success message or error message.
194
+ str: "success" or error message starting with "Error:".
195
195
  """
196
196
  try:
197
197
  # prepare metadata for unified markdown saving
@@ -221,7 +221,7 @@ class ContextSummarizerToolkit(BaseToolkit):
221
221
  to save.
222
222
 
223
223
  Returns:
224
- str: Success message or error message.
224
+ str: "success" or error message starting with "Error:".
225
225
  """
226
226
  try:
227
227
  # prepare metadata for markdown saving
@@ -872,7 +872,7 @@ class ExcelToolkit(BaseToolkit):
872
872
  import csv
873
873
 
874
874
  with open(
875
- resolved_csv_path, 'w', newline='', encoding='utf-8'
875
+ resolved_csv_path, 'w', newline='', encoding='utf-8-sig'
876
876
  ) as csvfile:
877
877
  writer = csv.writer(csvfile)
878
878
  writer.writerows(data)
@@ -906,7 +906,7 @@ class FileToolkit(BaseToolkit):
906
906
  self,
907
907
  file_path: Path,
908
908
  content: Union[str, List[List]],
909
- encoding: str = "utf-8",
909
+ encoding: str = "utf-8-sig",
910
910
  ) -> None:
911
911
  r"""Write CSV content to a file.
912
912
 
@@ -914,7 +914,8 @@ class FileToolkit(BaseToolkit):
914
914
  file_path (Path): The target file path.
915
915
  content (Union[str, List[List]]): The CSV content as a string or
916
916
  list of lists.
917
- encoding (str): Character encoding to use. (default: :obj:`utf-8`)
917
+ encoding (str): Character encoding to use.
918
+ (default: :obj:`utf-8-sig`)
918
919
  """
919
920
  import csv
920
921
 
@@ -15,7 +15,6 @@
15
15
  import os
16
16
  import platform
17
17
  import re
18
- import shlex
19
18
  import shutil
20
19
  import subprocess
21
20
  import sys
@@ -27,38 +26,99 @@ from camel.logger import get_logger
27
26
  logger = get_logger(__name__)
28
27
 
29
28
 
30
- def contains_command_chaining(command: str) -> bool:
31
- r"""Check if command contains chaining operators that could be used to
32
- bypass security.
29
+ def check_command_safety(
30
+ command: str,
31
+ allowed_commands: Optional[Set[str]] = None,
32
+ ) -> Tuple[bool, str]:
33
+ r"""Check if a command (potentially with chaining) is safe to execute.
34
+
35
+ Args:
36
+ command (str): The command string to check
37
+ allowed_commands (Optional[Set[str]]): Set of allowed commands
38
+ (whitelist mode)
39
+
40
+ Returns:
41
+ Tuple[bool, str]: (is_safe, reason)
33
42
  """
34
- # Pattern to match command chaining operators: ;, &&, ||, |
35
- # But exclude cases where they are inside quotes or escaped
36
- chaining_pattern = r'''
37
- (?<!\\) # Not preceded by backslash (not escaped)
38
- (?: # Group for alternation
39
- ; # Semicolon
40
- | # OR
41
- \|\| # Logical OR
42
- | # OR
43
- && # Logical AND
44
- | # OR
45
- (?<!\|) # Not preceded by pipe (to avoid matching ||)
46
- \| # Single pipe
47
- (?!\|) # Not followed by pipe (to avoid matching ||)
48
- )
49
- (?= # Positive lookahead
50
- (?: # Group
51
- [^"'] # Not a quote
52
- | # OR
53
- "[^"]*" # Content in double quotes
54
- | # OR
55
- '[^']*' # Content in single quotes
56
- )* # Zero or more times
57
- $ # End of string
58
- )
59
- '''
43
+ if not command.strip():
44
+ return False, "Empty command is not allowed."
60
45
 
61
- return bool(re.search(chaining_pattern, command, re.VERBOSE))
46
+ # Dangerous commands list - including ALL rm operations
47
+ dangerous_commands = [
48
+ # System administration
49
+ 'sudo',
50
+ 'su',
51
+ 'reboot',
52
+ 'shutdown',
53
+ 'halt',
54
+ 'poweroff',
55
+ 'init',
56
+ # File system manipulation
57
+ 'rm',
58
+ 'chown',
59
+ 'chgrp',
60
+ 'umount',
61
+ 'mount',
62
+ # Disk operations
63
+ 'dd',
64
+ 'mkfs',
65
+ 'fdisk',
66
+ 'parted',
67
+ 'fsck',
68
+ 'mkswap',
69
+ 'swapon',
70
+ 'swapoff',
71
+ # Process management
72
+ 'service',
73
+ 'systemctl',
74
+ 'systemd',
75
+ # Network configuration
76
+ 'iptables',
77
+ 'ip6tables',
78
+ 'ifconfig',
79
+ 'route',
80
+ 'iptables-save',
81
+ # Cron and scheduling
82
+ 'crontab',
83
+ 'at',
84
+ 'batch',
85
+ # User management
86
+ 'useradd',
87
+ 'userdel',
88
+ 'usermod',
89
+ 'passwd',
90
+ 'chpasswd',
91
+ 'newgrp',
92
+ # Kernel modules
93
+ 'modprobe',
94
+ 'rmmod',
95
+ 'insmod',
96
+ 'lsmod',
97
+ ]
98
+
99
+ # Remove quoted strings to avoid false positives
100
+ clean_command = re.sub(r'''["'][^"']*["']''', ' ', command)
101
+
102
+ # If whitelist mode, check ALL commands against the whitelist
103
+ if allowed_commands is not None:
104
+ # Extract all command words (at start or after operators)
105
+ cmd_pattern = r'(?:^|;|\||&&)\s*\b([a-zA-Z_/][\w\-/]*)'
106
+ found_commands = re.findall(cmd_pattern, clean_command, re.IGNORECASE)
107
+ for cmd in found_commands:
108
+ if cmd.lower() not in allowed_commands:
109
+ return (
110
+ False,
111
+ f"Command '{cmd}' is not in the allowed commands list.",
112
+ )
113
+ return True, ""
114
+
115
+ # Check for dangerous commands
116
+ for cmd in dangerous_commands:
117
+ pattern = rf'(?:^|;|\||&&)\s*\b{re.escape(cmd)}\b'
118
+ if re.search(pattern, clean_command, re.IGNORECASE):
119
+ return False, f"Command '{cmd}' is blocked for safety."
120
+
121
+ return True, ""
62
122
 
63
123
 
64
124
  def sanitize_command(
@@ -80,133 +140,25 @@ def sanitize_command(
80
140
  Returns:
81
141
  Tuple[bool, str]: (is_safe, message_or_command)
82
142
  """
83
- # Apply security checks to both backends - security should be consistent
84
143
  if not safe_mode:
85
144
  return True, command # Skip all checks if safe_mode is disabled
86
145
 
87
- # First check for command chaining and pipes
88
- if contains_command_chaining(command):
89
- return (
90
- False,
91
- "Command chaining (;, &&, ||, |) is not allowed "
92
- "for security reasons.",
93
- )
94
-
95
- parts = shlex.split(command)
96
- if not parts:
97
- return False, "Empty command is not allowed."
98
- base_cmd = parts[0].lower()
99
-
100
- # If whitelist is defined, only allow whitelisted commands
101
- if allowed_commands is not None:
102
- if base_cmd not in allowed_commands:
103
- return (
104
- False,
105
- f"Command '{base_cmd}' is not in the allowed commands list.",
146
+ # Use safety checker
147
+ is_safe, reason = check_command_safety(command, allowed_commands)
148
+ if not is_safe:
149
+ return False, reason
150
+
151
+ # Additional check for Docker backend: prevent cd outside working directory
152
+ if not use_docker_backend and working_dir and 'cd ' in command:
153
+ # Extract cd commands and check their targets
154
+ cd_pattern = r'\bcd\s+([^\s;|&]+)'
155
+ for match in re.finditer(cd_pattern, command):
156
+ target_path = match.group(1).strip('\'"')
157
+ target_dir = os.path.abspath(
158
+ os.path.join(working_dir, target_path)
106
159
  )
107
- # If command is whitelisted, skip the dangerous commands check
108
- # but still apply other safety checks
109
- else:
110
- # Block dangerous commands (only when no whitelist is defined)
111
- dangerous_commands = [
112
- # System administration
113
- 'sudo',
114
- 'su',
115
- 'reboot',
116
- 'shutdown',
117
- 'halt',
118
- 'poweroff',
119
- 'init',
120
- # File system manipulation
121
- 'rm',
122
- 'mv',
123
- 'chmod',
124
- 'chown',
125
- 'chgrp',
126
- 'umount',
127
- 'mount',
128
- # Disk operations
129
- 'dd',
130
- 'mkfs',
131
- 'fdisk',
132
- 'parted',
133
- 'fsck',
134
- 'mkswap',
135
- 'swapon',
136
- 'swapoff',
137
- # Process management
138
- 'kill',
139
- 'killall',
140
- 'pkill',
141
- 'service',
142
- 'systemctl',
143
- 'systemd',
144
- # Network configuration
145
- 'iptables',
146
- 'ip6tables',
147
- 'ifconfig',
148
- 'route',
149
- 'iptables-save',
150
- # Cron and scheduling
151
- 'crontab',
152
- 'at',
153
- 'batch',
154
- # User management
155
- 'useradd',
156
- 'userdel',
157
- 'usermod',
158
- 'passwd',
159
- 'chpasswd',
160
- 'newgrp',
161
- # Kernel modules
162
- 'modprobe',
163
- 'rmmod',
164
- 'insmod',
165
- 'lsmod',
166
- # System information that could leak sensitive data
167
- 'dmesg',
168
- 'last',
169
- 'lastlog',
170
- 'who',
171
- 'w',
172
- ]
173
- if base_cmd in dangerous_commands:
174
- # Special handling for rm command - use regex for precise checking
175
- if base_cmd == 'rm':
176
- # Check for dangerous rm options using regex
177
- dangerous_rm_pattern = (
178
- r'\s-[^-\s]*[rf][^-\s]*\s|\s--force\s|'
179
- r'\s--recursive\s|\s-rf\s|\s-fr\s'
180
- )
181
- if re.search(dangerous_rm_pattern, command, re.IGNORECASE):
182
- return (
183
- False,
184
- f"Command '{base_cmd}' with forceful or "
185
- f"recursive options is blocked for safety.",
186
- )
187
- # Also block rm without any target (could be dangerous)
188
- if len(parts) < 2:
189
- return (
190
- False,
191
- "rm command requires target "
192
- "file/directory specification.",
193
- )
194
- else:
195
- return False, f"Command '{base_cmd}' is blocked for safety."
196
-
197
- # For local backend only: prevent changing
198
- # directory outside the workspace
199
- # Docker containers are already sandboxed,
200
- # so this check is not needed there
201
- if (
202
- not use_docker_backend
203
- and base_cmd == 'cd'
204
- and len(parts) > 1
205
- and working_dir
206
- ):
207
- target_dir = os.path.abspath(os.path.join(working_dir, parts[1]))
208
- if not target_dir.startswith(working_dir):
209
- return False, "Cannot 'cd' outside of the working directory."
160
+ if not target_dir.startswith(working_dir):
161
+ return False, "Cannot 'cd' outside of the working directory."
210
162
 
211
163
  return True, command
212
164
 
camel/types/enums.py CHANGED
@@ -30,7 +30,7 @@ class RoleType(Enum):
30
30
 
31
31
 
32
32
  class ModelType(UnifiedModelType, Enum):
33
- DEFAULT = os.getenv("DEFAULT_MODEL_TYPE", "gpt-5-mini")
33
+ DEFAULT = os.getenv("DEFAULT_MODEL_TYPE", "gpt-4.1-mini-2025-04-14")
34
34
 
35
35
  GPT_3_5_TURBO = "gpt-3.5-turbo"
36
36
  GPT_4 = "gpt-4"
@@ -207,7 +207,7 @@ class ModelType(UnifiedModelType, Enum):
207
207
  CLAUDE_3_5_SONNET = "claude-3-5-sonnet-latest"
208
208
  CLAUDE_3_5_HAIKU = "claude-3-5-haiku-latest"
209
209
  CLAUDE_3_7_SONNET = "claude-3-7-sonnet-latest"
210
- CLAUDE_4_5_SONNET = "claude-4-5-sonnet-latest"
210
+ CLAUDE_SONNET_4_5 = "claude-sonnet-4-5"
211
211
  CLAUDE_SONNET_4 = "claude-sonnet-4-20250514"
212
212
  CLAUDE_OPUS_4 = "claude-opus-4-20250514"
213
213
  CLAUDE_OPUS_4_1 = "claude-opus-4-1-20250805"
@@ -642,7 +642,7 @@ class ModelType(UnifiedModelType, Enum):
642
642
  ModelType.CLAUDE_3_5_SONNET,
643
643
  ModelType.CLAUDE_3_5_HAIKU,
644
644
  ModelType.CLAUDE_3_7_SONNET,
645
- ModelType.CLAUDE_4_5_SONNET,
645
+ ModelType.CLAUDE_SONNET_4_5,
646
646
  ModelType.CLAUDE_SONNET_4,
647
647
  ModelType.CLAUDE_OPUS_4,
648
648
  ModelType.CLAUDE_OPUS_4_1,
@@ -1423,7 +1423,7 @@ class ModelType(UnifiedModelType, Enum):
1423
1423
  ModelType.CLAUDE_3_5_SONNET,
1424
1424
  ModelType.CLAUDE_3_5_HAIKU,
1425
1425
  ModelType.CLAUDE_3_7_SONNET,
1426
- ModelType.CLAUDE_4_5_SONNET,
1426
+ ModelType.CLAUDE_SONNET_4_5,
1427
1427
  ModelType.CLAUDE_SONNET_4,
1428
1428
  ModelType.CLAUDE_OPUS_4,
1429
1429
  ModelType.CLAUDE_OPUS_4_1,