camel-ai 0.2.68__py3-none-any.whl → 0.2.69a1__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 (36) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +170 -11
  3. camel/configs/vllm_config.py +2 -0
  4. camel/datagen/self_improving_cot.py +1 -1
  5. camel/memories/context_creators/score_based.py +129 -87
  6. camel/runtimes/configs.py +11 -11
  7. camel/runtimes/daytona_runtime.py +4 -4
  8. camel/runtimes/docker_runtime.py +6 -6
  9. camel/runtimes/remote_http_runtime.py +5 -5
  10. camel/societies/workforce/prompts.py +13 -12
  11. camel/societies/workforce/single_agent_worker.py +252 -22
  12. camel/societies/workforce/utils.py +10 -2
  13. camel/societies/workforce/worker.py +21 -45
  14. camel/societies/workforce/workforce.py +36 -15
  15. camel/tasks/task.py +18 -12
  16. camel/toolkits/__init__.py +2 -0
  17. camel/toolkits/aci_toolkit.py +19 -19
  18. camel/toolkits/arxiv_toolkit.py +6 -6
  19. camel/toolkits/dappier_toolkit.py +5 -5
  20. camel/toolkits/file_write_toolkit.py +10 -10
  21. camel/toolkits/github_toolkit.py +3 -3
  22. camel/toolkits/non_visual_browser_toolkit/__init__.py +18 -0
  23. camel/toolkits/non_visual_browser_toolkit/actions.py +196 -0
  24. camel/toolkits/non_visual_browser_toolkit/agent.py +278 -0
  25. camel/toolkits/non_visual_browser_toolkit/browser_non_visual_toolkit.py +363 -0
  26. camel/toolkits/non_visual_browser_toolkit/nv_browser_session.py +175 -0
  27. camel/toolkits/non_visual_browser_toolkit/snapshot.js +188 -0
  28. camel/toolkits/non_visual_browser_toolkit/snapshot.py +164 -0
  29. camel/toolkits/pptx_toolkit.py +4 -4
  30. camel/toolkits/sympy_toolkit.py +1 -1
  31. camel/toolkits/task_planning_toolkit.py +3 -3
  32. camel/toolkits/thinking_toolkit.py +1 -1
  33. {camel_ai-0.2.68.dist-info → camel_ai-0.2.69a1.dist-info}/METADATA +1 -1
  34. {camel_ai-0.2.68.dist-info → camel_ai-0.2.69a1.dist-info}/RECORD +36 -29
  35. {camel_ai-0.2.68.dist-info → camel_ai-0.2.69a1.dist-info}/WHEEL +0 -0
  36. {camel_ai-0.2.68.dist-info → camel_ai-0.2.69a1.dist-info}/licenses/LICENSE +0 -0
@@ -36,9 +36,9 @@ class RemoteHttpRuntime(BaseRuntime):
36
36
 
37
37
  Args:
38
38
  host (str): The host of the remote server.
39
- port (int): The port of the remote server. (default: :obj: `8000`)
39
+ port (int): The port of the remote server. (default: :obj:`8000`)
40
40
  python_exec (str): The python executable to run the API server.
41
- (default: :obj: `python3`)
41
+ (default: :obj:`python3`)
42
42
  """
43
43
 
44
44
  def __init__(
@@ -90,9 +90,9 @@ class RemoteHttpRuntime(BaseRuntime):
90
90
  list of functions to add.
91
91
  entrypoint (str): The entrypoint for the function.
92
92
  redirect_stdout (bool): Whether to return the stdout of
93
- the function. (default: :obj: `False`)
93
+ the function. (default: :obj:`False`)
94
94
  arguments (Optional[Dict[str, Any]]): The arguments for the
95
- function. (default: :obj: `None`)
95
+ function. (default: :obj:`None`)
96
96
 
97
97
  Returns:
98
98
  RemoteHttpRuntime: The current runtime.
@@ -162,7 +162,7 @@ class RemoteHttpRuntime(BaseRuntime):
162
162
  r"""Wait for the API Server to be ready.
163
163
 
164
164
  Args:
165
- timeout (int): The number of seconds to wait. (default: :obj: `10`)
165
+ timeout (int): The number of seconds to wait. (default: :obj:`10`)
166
166
 
167
167
  Returns:
168
168
  bool: Whether the API Server is ready.
@@ -49,17 +49,6 @@ The information returned should be concise and clear.
49
49
  ASSIGN_TASK_PROMPT = TextPrompt(
50
50
  """You need to assign multiple tasks to worker nodes based on the information below.
51
51
 
52
- Here are the tasks to be assigned:
53
- ==============================
54
- {tasks_info}
55
- ==============================
56
-
57
- Following is the information of the existing worker nodes. The format is <ID>:<description>:<additional_info>. Choose the most capable worker node ID for each task.
58
-
59
- ==============================
60
- {child_nodes_info}
61
- ==============================
62
-
63
52
  For each task, you need to:
64
53
  1. Choose the most capable worker node ID for that task
65
54
  2. Identify any dependencies between tasks (if task B requires results from task A, then task A is a dependency of task B)
@@ -80,9 +69,21 @@ Example valid response:
80
69
  ]
81
70
  }}
82
71
 
83
- IMPORTANT: Only add dependencies when one task truly needs the output/result of another task to complete successfully. Don't add dependencies unless they are logically necessary.
72
+ ***CRITICAL: DEPENDENCY MANAGEMENT IS YOUR IMPORTANT RESPONSIBILITY.***
73
+ Carefully analyze the sequence of tasks. A task's dependencies MUST include the IDs of all prior tasks whose outputs are necessary for its execution. For example, a task to 'Summarize Paper X' MUST depend on the task that 'Finds/Retrieves Paper X'. Similarly, a task that 'Compiles a report from summaries' MUST depend on all 'Summarize Paper X' tasks. **Incorrect or missing dependencies will lead to critical operational failures and an inability to complete the overall objective.** Be meticulous in defining these relationships.
84
74
 
85
75
  Do not include any other text, explanations, justifications, or conversational filler before or after the JSON object. Return ONLY the JSON object.
76
+
77
+ Here are the tasks to be assigned:
78
+ ==============================
79
+ {tasks_info}
80
+ ==============================
81
+
82
+ Following is the information of the existing worker nodes. The format is <ID>:<description>:<additional_info>. Choose the most capable worker node ID for each task.
83
+
84
+ ==============================
85
+ {child_nodes_info}
86
+ ==============================
86
87
  """
87
88
  )
88
89
 
@@ -13,9 +13,12 @@
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
  from __future__ import annotations
15
15
 
16
+ import asyncio
16
17
  import datetime
17
18
  import json
18
- from typing import Any, List
19
+ import time
20
+ from collections import deque
21
+ from typing import Any, List, Optional
19
22
 
20
23
  from colorama import Fore
21
24
 
@@ -26,44 +29,237 @@ from camel.societies.workforce.worker import Worker
26
29
  from camel.tasks.task import Task, TaskState, validate_task_content
27
30
 
28
31
 
32
+ class AgentPool:
33
+ r"""A pool of agent instances for efficient reuse.
34
+
35
+ This pool manages a collection of pre-cloned agents. It supports
36
+ auto-scaling based ondemand and intelligent reuse of existing agents.
37
+
38
+ Args:
39
+ base_agent (ChatAgent): The base agent to clone from.
40
+ initial_size (int): Initial number of agents in the pool.
41
+ (default: :obj:`1`)
42
+ max_size (int): Maximum number of agents in the pool.
43
+ (default: :obj:`10`)
44
+ auto_scale (bool): Whether to automatically scale the pool size.
45
+ (default: :obj:`True`)
46
+ scale_factor (float): Factor by which to scale the pool when needed.
47
+ (default: :obj:`1.5`)
48
+ idle_timeout (float): Time in seconds after which idle agents are
49
+ removed. (default: :obj:`180.0`)
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ base_agent: ChatAgent,
55
+ initial_size: int = 1,
56
+ max_size: int = 10,
57
+ auto_scale: bool = True,
58
+ scale_factor: float = 1.5,
59
+ idle_timeout: float = 180.0, # 3 minutes
60
+ ):
61
+ self.base_agent = base_agent
62
+ self.max_size = max_size
63
+ self.auto_scale = auto_scale
64
+ self.scale_factor = scale_factor
65
+ self.idle_timeout = idle_timeout
66
+
67
+ # Pool management
68
+ self._available_agents: deque = deque()
69
+ self._in_use_agents: set = set()
70
+ self._agent_last_used: dict = {}
71
+ self._lock = asyncio.Lock()
72
+
73
+ # Statistics
74
+ self._total_borrows = 0
75
+ self._total_clones_created = 0
76
+ self._pool_hits = 0
77
+
78
+ # Initialize pool
79
+ self._initialize_pool(initial_size)
80
+
81
+ def _initialize_pool(self, size: int) -> None:
82
+ r"""Initialize the pool with the specified number of agents."""
83
+ for _ in range(min(size, self.max_size)):
84
+ agent = self._create_fresh_agent()
85
+ self._available_agents.append(agent)
86
+
87
+ def _create_fresh_agent(self) -> ChatAgent:
88
+ r"""Create a fresh agent instance."""
89
+ agent = self.base_agent.clone(with_memory=False)
90
+ self._total_clones_created += 1
91
+ return agent
92
+
93
+ async def get_agent(self) -> ChatAgent:
94
+ r"""Get an agent from the pool, creating one if necessary."""
95
+ async with self._lock:
96
+ self._total_borrows += 1
97
+
98
+ # Try to get from available agents first
99
+ if self._available_agents:
100
+ agent = self._available_agents.popleft()
101
+ self._in_use_agents.add(id(agent))
102
+ self._pool_hits += 1
103
+
104
+ # Reset the agent state
105
+ agent.reset()
106
+ return agent
107
+
108
+ # Check if we can create new agents
109
+ total_agents = len(self._available_agents) + len(
110
+ self._in_use_agents
111
+ )
112
+ if total_agents < self.max_size:
113
+ agent = self._create_fresh_agent()
114
+ self._in_use_agents.add(id(agent))
115
+ return agent
116
+
117
+ # Pool exhausted, wait and retry or create temporary agent
118
+ if self.auto_scale:
119
+ # Create a temporary agent that won't be returned to pool
120
+ return self._create_fresh_agent()
121
+ else:
122
+ # Wait for an agent to become available
123
+ while not self._available_agents:
124
+ await asyncio.sleep(0.1)
125
+
126
+ agent = self._available_agents.popleft()
127
+ self._in_use_agents.add(id(agent))
128
+ agent.reset()
129
+ return agent
130
+
131
+ async def return_agent(self, agent: ChatAgent) -> None:
132
+ r"""Return an agent to the pool."""
133
+ async with self._lock:
134
+ agent_id = id(agent)
135
+
136
+ if agent_id in self._in_use_agents:
137
+ self._in_use_agents.remove(agent_id)
138
+
139
+ # Only return to pool if we're under max size
140
+ if len(self._available_agents) < self.max_size:
141
+ # Reset agent state before returning to pool
142
+ agent.reset()
143
+ self._available_agents.append(agent)
144
+ self._agent_last_used[agent_id] = time.time()
145
+
146
+ async def cleanup_idle_agents(self) -> None:
147
+ r"""Remove idle agents from the pool to free memory."""
148
+ if not self.auto_scale:
149
+ return
150
+
151
+ async with self._lock:
152
+ current_time = time.time()
153
+ agents_to_remove = []
154
+
155
+ for agent in list(self._available_agents):
156
+ agent_id = id(agent)
157
+ last_used = self._agent_last_used.get(agent_id, current_time)
158
+
159
+ if current_time - last_used > self.idle_timeout:
160
+ agents_to_remove.append(agent)
161
+
162
+ for agent in agents_to_remove:
163
+ self._available_agents.remove(agent)
164
+ agent_id = id(agent)
165
+ self._agent_last_used.pop(agent_id, None)
166
+
167
+ def get_stats(self) -> dict:
168
+ r"""Get pool statistics."""
169
+ return {
170
+ "available_agents": len(self._available_agents),
171
+ "in_use_agents": len(self._in_use_agents),
172
+ "total_borrows": self._total_borrows,
173
+ "total_clones_created": self._total_clones_created,
174
+ "pool_hits": self._pool_hits,
175
+ "hit_rate": self._pool_hits / max(self._total_borrows, 1),
176
+ }
177
+
178
+
29
179
  class SingleAgentWorker(Worker):
30
180
  r"""A worker node that consists of a single agent.
31
181
 
32
182
  Args:
33
183
  description (str): Description of the node.
34
184
  worker (ChatAgent): Worker of the node. A single agent.
35
- max_concurrent_tasks (int): Maximum number of tasks this worker can
36
- process concurrently. (default: :obj:`10`)
185
+ use_agent_pool (bool): Whether to use agent pool for efficiency.
186
+ (default: :obj:`True`)
187
+ pool_initial_size (int): Initial size of the agent pool.
188
+ (default: :obj:`1`)
189
+ pool_max_size (int): Maximum size of the agent pool.
190
+ (default: :obj:`10`)
191
+ auto_scale_pool (bool): Whether to auto-scale the agent pool.
192
+ (default: :obj:`True`)
37
193
  """
38
194
 
39
195
  def __init__(
40
196
  self,
41
197
  description: str,
42
198
  worker: ChatAgent,
43
- max_concurrent_tasks: int = 10,
199
+ use_agent_pool: bool = True,
200
+ pool_initial_size: int = 1,
201
+ pool_max_size: int = 10,
202
+ auto_scale_pool: bool = True,
44
203
  ) -> None:
45
204
  node_id = worker.agent_id
46
205
  super().__init__(
47
206
  description,
48
207
  node_id=node_id,
49
- max_concurrent_tasks=max_concurrent_tasks,
50
208
  )
51
209
  self.worker = worker
210
+ self.use_agent_pool = use_agent_pool
211
+
212
+ self.agent_pool: Optional[AgentPool] = None
213
+ self._cleanup_task: Optional[asyncio.Task] = None
214
+ # Initialize agent pool if enabled
215
+ if self.use_agent_pool:
216
+ self.agent_pool = AgentPool(
217
+ base_agent=worker,
218
+ initial_size=pool_initial_size,
219
+ max_size=pool_max_size,
220
+ auto_scale=auto_scale_pool,
221
+ )
52
222
 
53
223
  def reset(self) -> Any:
54
224
  r"""Resets the worker to its initial state."""
55
225
  super().reset()
56
226
  self.worker.reset()
57
227
 
228
+ # Reset agent pool if it exists
229
+ if self.agent_pool:
230
+ # Stop cleanup task
231
+ if self._cleanup_task and not self._cleanup_task.done():
232
+ self._cleanup_task.cancel()
233
+
234
+ # Reinitialize pool
235
+ self.agent_pool = AgentPool(
236
+ base_agent=self.worker,
237
+ )
238
+
239
+ async def _get_worker_agent(self) -> ChatAgent:
240
+ r"""Get a worker agent, either from pool or by cloning."""
241
+ if self.use_agent_pool and self.agent_pool:
242
+ return await self.agent_pool.get_agent()
243
+ else:
244
+ # Fallback to original cloning approach
245
+ return self.worker.clone(with_memory=False)
246
+
247
+ async def _return_worker_agent(self, agent: ChatAgent) -> None:
248
+ r"""Return a worker agent to the pool if pooling is enabled."""
249
+ if self.use_agent_pool and self.agent_pool:
250
+ await self.agent_pool.return_agent(agent)
251
+ # If not using pool, agent will be garbage collected
252
+
58
253
  async def _process_task(
59
254
  self, task: Task, dependencies: List[Task]
60
255
  ) -> TaskState:
61
- r"""Processes a task with its dependencies using a cloned agent.
256
+ r"""Processes a task with its dependencies using an efficient agent
257
+ management system.
62
258
 
63
259
  This method asynchronously processes a given task, considering its
64
- dependencies, by sending a generated prompt to a cloned worker agent.
65
- Using a cloned agent ensures that concurrent tasks don't interfere
66
- with each other's state.
260
+ dependencies, by sending a generated prompt to a worker agent.
261
+ Uses an agent pool for efficiency when enabled, or falls back to
262
+ cloning when pool is disabled.
67
263
 
68
264
  Args:
69
265
  task (Task): The task to process, which includes necessary details
@@ -74,17 +270,17 @@ class SingleAgentWorker(Worker):
74
270
  TaskState: `TaskState.DONE` if processed successfully, otherwise
75
271
  `TaskState.FAILED`.
76
272
  """
77
- # Clone the agent for this specific task to avoid state conflicts
78
- # when processing multiple tasks concurrently
79
- worker_agent = self.worker.clone(with_memory=False)
80
-
81
- dependency_tasks_info = self._get_dep_tasks_info(dependencies)
82
- prompt = PROCESS_TASK_PROMPT.format(
83
- content=task.content,
84
- dependency_tasks_info=dependency_tasks_info,
85
- additional_info=task.additional_info,
86
- )
273
+ # Get agent efficiently (from pool or by cloning)
274
+ worker_agent = await self._get_worker_agent()
275
+
87
276
  try:
277
+ dependency_tasks_info = self._get_dep_tasks_info(dependencies)
278
+ prompt = PROCESS_TASK_PROMPT.format(
279
+ content=task.content,
280
+ dependency_tasks_info=dependency_tasks_info,
281
+ additional_info=task.additional_info,
282
+ )
283
+
88
284
  response = await worker_agent.astep(
89
285
  prompt, response_format=TaskResult
90
286
  )
@@ -94,8 +290,11 @@ class SingleAgentWorker(Worker):
94
290
  f"\n{e}{Fore.RESET}"
95
291
  )
96
292
  return TaskState.FAILED
293
+ finally:
294
+ # Return agent to pool or let it be garbage collected
295
+ await self._return_worker_agent(worker_agent)
97
296
 
98
- # Get actual token usage from the cloned agent that processed this task
297
+ # Get actual token usage from the agent that processed this task
99
298
  try:
100
299
  _, total_token_count = worker_agent.memory.get_context()
101
300
  except Exception:
@@ -117,11 +316,11 @@ class SingleAgentWorker(Worker):
117
316
  "timestamp": str(datetime.datetime.now()),
118
317
  "description": f"Attempt by "
119
318
  f"{getattr(worker_agent, 'agent_id', worker_agent.role_name)} "
120
- f"(cloned from "
319
+ f"(from pool/clone of "
121
320
  f"{getattr(self.worker, 'agent_id', self.worker.role_name)}) "
122
321
  f"to process task {task.content}",
123
322
  "response_content": response.msg.content,
124
- "tool_calls": response.info["tool_calls"],
323
+ "tool_calls": response.info.get("tool_calls"),
125
324
  "total_token_count": total_token_count,
126
325
  }
127
326
 
@@ -157,3 +356,34 @@ class SingleAgentWorker(Worker):
157
356
 
158
357
  task.result = task_result.content
159
358
  return TaskState.DONE
359
+
360
+ async def _listen_to_channel(self):
361
+ r"""Override to start cleanup task when pool is enabled."""
362
+ # Start cleanup task for agent pool
363
+ if self.use_agent_pool and self.agent_pool:
364
+ self._cleanup_task = asyncio.create_task(self._periodic_cleanup())
365
+
366
+ # Call parent implementation
367
+ await super()._listen_to_channel()
368
+
369
+ # Stop cleanup task
370
+ if self._cleanup_task and not self._cleanup_task.done():
371
+ self._cleanup_task.cancel()
372
+
373
+ async def _periodic_cleanup(self):
374
+ r"""Periodically clean up idle agents from the pool."""
375
+ while True:
376
+ try:
377
+ await asyncio.sleep(60) # Cleanup every minute
378
+ if self.agent_pool:
379
+ await self.agent_pool.cleanup_idle_agents()
380
+ except asyncio.CancelledError:
381
+ break
382
+ except Exception as e:
383
+ print(f"Error in pool cleanup: {e}")
384
+
385
+ def get_pool_stats(self) -> Optional[dict]:
386
+ r"""Get agent pool statistics if pool is enabled."""
387
+ if self.use_agent_pool and self.agent_pool:
388
+ return self.agent_pool.get_stats()
389
+ return None
@@ -50,7 +50,8 @@ class TaskAssignment(BaseModel):
50
50
  )
51
51
  dependencies: List[str] = Field(
52
52
  default_factory=list,
53
- description="List of task IDs that must complete before this task.",
53
+ description="List of task IDs that must complete before this task. "
54
+ "This is critical for the task decomposition and execution.",
54
55
  )
55
56
 
56
57
 
@@ -156,7 +157,14 @@ def check_if_running(
156
157
  )
157
158
  return None
158
159
  else:
159
- raise last_exception
160
+ raise (
161
+ last_exception
162
+ if last_exception
163
+ else RuntimeError(
164
+ f"Unexpected failure in {func.__name__} "
165
+ "with no exception captured."
166
+ )
167
+ )
160
168
 
161
169
  return wrapper
162
170
 
@@ -36,18 +36,14 @@ class Worker(BaseNode, ABC):
36
36
  description (str): Description of the node.
37
37
  node_id (Optional[str]): ID of the node. If not provided, it will
38
38
  be generated automatically. (default: :obj:`None`)
39
- max_concurrent_tasks (int): Maximum number of tasks this worker can
40
- process concurrently. (default: :obj:`10`)
41
39
  """
42
40
 
43
41
  def __init__(
44
42
  self,
45
43
  description: str,
46
44
  node_id: Optional[str] = None,
47
- max_concurrent_tasks: int = 10,
48
45
  ) -> None:
49
46
  super().__init__(description, node_id=node_id)
50
- self.max_concurrent_tasks = max_concurrent_tasks
51
47
  self._active_task_ids: Set[str] = set()
52
48
 
53
49
  def __repr__(self):
@@ -114,17 +110,12 @@ class Worker(BaseNode, ABC):
114
110
 
115
111
  @check_if_running(False)
116
112
  async def _listen_to_channel(self):
117
- r"""Continuously listen to the channel, process tasks that are
118
- assigned to this node concurrently up to max_concurrent_tasks limit.
113
+ r"""Continuously listen to the channel and process assigned tasks.
119
114
 
120
- This method supports parallel task execution when multiple tasks
121
- are assigned to the same worker.
115
+ This method supports parallel task execution without artificial limits.
122
116
  """
123
117
  self._running = True
124
- logger.info(
125
- f"{self} started with max {self.max_concurrent_tasks} "
126
- f"concurrent tasks."
127
- )
118
+ logger.info(f"{self} started.")
128
119
 
129
120
  # Keep track of running task coroutines
130
121
  running_tasks: Set[asyncio.Task] = set()
@@ -141,39 +132,24 @@ class Worker(BaseNode, ABC):
141
132
  except Exception as e:
142
133
  logger.error(f"Task processing failed: {e}")
143
134
 
144
- # Check if we can accept more tasks
145
- if len(running_tasks) < self.max_concurrent_tasks:
146
- try:
147
- # Try to get a new task (with short timeout to avoid
148
- # blocking)
149
- task = await asyncio.wait_for(
150
- self._get_assigned_task(), timeout=1.0
151
- )
152
-
153
- # Create and start processing task
154
- task_coroutine = asyncio.create_task(
155
- self._process_single_task(task)
156
- )
157
- running_tasks.add(task_coroutine)
158
-
159
- except asyncio.TimeoutError:
160
- # No tasks available, continue loop
161
- if not running_tasks:
162
- # No tasks running and none available, short sleep
163
- await asyncio.sleep(0.1)
164
- continue
165
- else:
166
- # At max capacity, wait for at least one task to complete
167
- if running_tasks:
168
- done, running_tasks = await asyncio.wait(
169
- running_tasks, return_when=asyncio.FIRST_COMPLETED
170
- )
171
- # Process completed tasks
172
- for completed_task in done:
173
- try:
174
- await completed_task
175
- except Exception as e:
176
- logger.error(f"Task processing failed: {e}")
135
+ # Try to get a new task (with short timeout to avoid blocking)
136
+ try:
137
+ task = await asyncio.wait_for(
138
+ self._get_assigned_task(), timeout=1.0
139
+ )
140
+
141
+ # Create and start processing task
142
+ task_coroutine = asyncio.create_task(
143
+ self._process_single_task(task)
144
+ )
145
+ running_tasks.add(task_coroutine)
146
+
147
+ except asyncio.TimeoutError:
148
+ # No tasks available, continue loop
149
+ if not running_tasks:
150
+ # No tasks running and none available, short sleep
151
+ await asyncio.sleep(0.1)
152
+ continue
177
153
 
178
154
  except Exception as e:
179
155
  logger.error(
@@ -674,8 +674,8 @@ class Workforce(BaseNode):
674
674
  # Reset state for tasks being moved back to pending
675
675
  for task in tasks_to_move_back:
676
676
  # Handle all possible task states
677
- if task.state in [TaskState.DONE, TaskState.FAILED]:
678
- task.state = TaskState.OPEN
677
+ if task.state in [TaskState.DONE, TaskState.OPEN]:
678
+ task.state = TaskState.FAILED # TODO: Add logic for OPEN
679
679
  # Clear result to avoid confusion
680
680
  task.result = None
681
681
  # Reset failure count to give task a fresh start
@@ -881,7 +881,7 @@ class Workforce(BaseNode):
881
881
  self.reset()
882
882
  self._task = task
883
883
  self._state = WorkforceState.RUNNING
884
- task.state = TaskState.OPEN
884
+ task.state = TaskState.FAILED # TODO: Add logic for OPEN
885
885
  self._pending_tasks.append(task)
886
886
 
887
887
  # Decompose the task into subtasks first
@@ -998,21 +998,23 @@ class Workforce(BaseNode):
998
998
  self,
999
999
  description: str,
1000
1000
  worker: ChatAgent,
1001
- max_concurrent_tasks: int = 10,
1001
+ pool_max_size: int = 10,
1002
1002
  ) -> Workforce:
1003
1003
  r"""Add a worker node to the workforce that uses a single agent.
1004
1004
 
1005
1005
  Args:
1006
1006
  description (str): Description of the worker node.
1007
1007
  worker (ChatAgent): The agent to be added.
1008
- max_concurrent_tasks (int): Maximum number of tasks this worker can
1009
- process concurrently. (default: :obj:`10`)
1008
+ pool_max_size (int): Maximum size of the agent pool.
1009
+ (default: :obj:`10`)
1010
1010
 
1011
1011
  Returns:
1012
1012
  Workforce: The workforce node itself.
1013
1013
  """
1014
1014
  worker_node = SingleAgentWorker(
1015
- description, worker, max_concurrent_tasks
1015
+ description=description,
1016
+ worker=worker,
1017
+ pool_max_size=pool_max_size,
1016
1018
  )
1017
1019
  self._children.append(worker_node)
1018
1020
  if self.metrics_logger:
@@ -1194,6 +1196,13 @@ class Workforce(BaseNode):
1194
1196
  response = self.coordinator_agent.step(
1195
1197
  prompt, response_format=TaskAssignResult
1196
1198
  )
1199
+ if response.msg is None or response.msg.content is None:
1200
+ logger.error(
1201
+ "Coordinator agent returned empty response for task assignment"
1202
+ )
1203
+ # Return empty result as fallback
1204
+ return TaskAssignResult(assignments=[])
1205
+
1197
1206
  result_dict = json.loads(response.msg.content, parse_int=str)
1198
1207
  task_assign_result = TaskAssignResult(**result_dict)
1199
1208
  return task_assign_result
@@ -1231,8 +1240,21 @@ class Workforce(BaseNode):
1231
1240
  response = self.coordinator_agent.step(
1232
1241
  prompt, response_format=WorkerConf
1233
1242
  )
1234
- result_dict = json.loads(response.msg.content)
1235
- new_node_conf = WorkerConf(**result_dict)
1243
+ if response.msg is None or response.msg.content is None:
1244
+ logger.error(
1245
+ "Coordinator agent returned empty response for worker creation"
1246
+ )
1247
+ # Create a fallback worker configuration
1248
+ new_node_conf = WorkerConf(
1249
+ description=f"Fallback worker for "
1250
+ f"task: {task.content[:50]}...",
1251
+ role="General Assistant",
1252
+ sys_msg="You are a general assistant that can help "
1253
+ "with various tasks.",
1254
+ )
1255
+ else:
1256
+ result_dict = json.loads(response.msg.content)
1257
+ new_node_conf = WorkerConf(**result_dict)
1236
1258
 
1237
1259
  new_agent = self._create_new_agent(
1238
1260
  new_node_conf.role,
@@ -1242,7 +1264,7 @@ class Workforce(BaseNode):
1242
1264
  new_node = SingleAgentWorker(
1243
1265
  description=new_node_conf.description,
1244
1266
  worker=new_agent,
1245
- max_concurrent_tasks=10, # TODO: make this configurable
1267
+ pool_max_size=10, # TODO: make this configurable
1246
1268
  )
1247
1269
  new_node.set_channel(self._channel)
1248
1270
 
@@ -1384,10 +1406,10 @@ class Workforce(BaseNode):
1384
1406
  metadata={'failure_count': task.failure_count},
1385
1407
  )
1386
1408
 
1387
- if task.failure_count >= 3:
1409
+ if task.failure_count > 3:
1388
1410
  return True
1389
1411
 
1390
- if task.get_depth() >= 3:
1412
+ if task.get_depth() > 3:
1391
1413
  # Create a new worker node and reassign
1392
1414
  assignee = self._create_worker_node_for_task(task)
1393
1415
 
@@ -1685,7 +1707,7 @@ class Workforce(BaseNode):
1685
1707
  await self._graceful_shutdown(returned_task)
1686
1708
  break
1687
1709
  elif returned_task.state == TaskState.OPEN:
1688
- # TODO: multi-layer workforce
1710
+ # TODO: Add logic for OPEN
1689
1711
  pass
1690
1712
  else:
1691
1713
  raise ValueError(
@@ -1797,7 +1819,7 @@ class Workforce(BaseNode):
1797
1819
  new_instance.add_single_agent_worker(
1798
1820
  child.description,
1799
1821
  cloned_worker,
1800
- child.max_concurrent_tasks,
1822
+ pool_max_size=10,
1801
1823
  )
1802
1824
  elif isinstance(child, RolePlayingWorker):
1803
1825
  new_instance.add_role_playing_worker(
@@ -1808,7 +1830,6 @@ class Workforce(BaseNode):
1808
1830
  child.user_agent_kwargs,
1809
1831
  child.summarize_agent_kwargs,
1810
1832
  child.chat_turn_limit,
1811
- child.max_concurrent_tasks,
1812
1833
  )
1813
1834
  elif isinstance(child, Workforce):
1814
1835
  new_instance.add_workforce(child.clone(with_memory))