camel-ai 0.2.69a6__py3-none-any.whl → 0.2.70__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.

@@ -19,7 +19,7 @@ import time
19
19
  import uuid
20
20
  from collections import deque
21
21
  from enum import Enum
22
- from typing import Any, Coroutine, Deque, Dict, List, Optional
22
+ from typing import Any, Coroutine, Deque, Dict, List, Optional, Set, Tuple
23
23
 
24
24
  from colorama import Fore
25
25
 
@@ -37,6 +37,7 @@ from camel.societies.workforce.role_playing_worker import RolePlayingWorker
37
37
  from camel.societies.workforce.single_agent_worker import SingleAgentWorker
38
38
  from camel.societies.workforce.task_channel import TaskChannel
39
39
  from camel.societies.workforce.utils import (
40
+ TaskAssignment,
40
41
  TaskAssignResult,
41
42
  WorkerConf,
42
43
  check_if_running,
@@ -110,27 +111,24 @@ class Workforce(BaseNode):
110
111
  children (Optional[List[BaseNode]], optional): List of child nodes
111
112
  under this node. Each child node can be a worker node or
112
113
  another workforce node. (default: :obj:`None`)
113
- coordinator_agent_kwargs (Optional[Dict], optional): Keyword
114
- arguments passed directly to the coordinator :obj:`ChatAgent`
115
- constructor. The coordinator manages task assignment and failure
116
- handling strategies. See :obj:`ChatAgent` documentation
117
- for all available parameters.
118
- (default: :obj:`None` - uses ModelPlatformType.DEFAULT,
119
- ModelType.DEFAULT)
120
- task_agent_kwargs (Optional[Dict], optional): Keyword arguments
121
- passed directly to the task planning :obj:`ChatAgent` constructor.
122
- The task agent handles task decomposition into subtasks and result
123
- composition. See :obj:`ChatAgent` documentation for all
124
- available parameters.
125
- (default: :obj:`None` - uses ModelPlatformType.DEFAULT,
126
- ModelType.DEFAULT)
127
- new_worker_agent_kwargs (Optional[Dict], optional): Default keyword
128
- arguments passed to :obj:`ChatAgent` constructor for workers
129
- created dynamically at runtime when existing workers cannot handle
130
- failed tasks. See :obj:`ChatAgent` documentation for all
131
- available parameters.
132
- (default: :obj:`None` - creates workers with SearchToolkit,
133
- CodeExecutionToolkit, and ThinkingToolkit)
114
+ coordinator_agent (Optional[ChatAgent], optional): A custom coordinator
115
+ agent instance for task assignment and worker creation. If
116
+ provided, the workforce will create a new agent using this agent's
117
+ model configuration but with the required system message and
118
+ functionality.
119
+ If None, a default agent will be created using DEFAULT model
120
+ settings. (default: :obj:`None`)
121
+ task_agent (Optional[ChatAgent], optional): A custom task planning
122
+ agent instance for task decomposition and composition. If
123
+ provided, the workforce will create a new agent using this agent's
124
+ model configuration but with the required system message and tools
125
+ (TaskPlanningToolkit). If None, a default agent will be created
126
+ using DEFAULT model settings. (default: :obj:`None`)
127
+ new_worker_agent (Optional[ChatAgent], optional): A template agent for
128
+ workers created dynamically at runtime when existing workers cannot
129
+ handle failed tasks. If None, workers will be created with default
130
+ settings including SearchToolkit, CodeExecutionToolkit, and
131
+ ThinkingToolkit. (default: :obj:`None`)
134
132
  graceful_shutdown_timeout (float, optional): The timeout in seconds
135
133
  for graceful shutdown when a task fails 3 times. During this
136
134
  period, the workforce remains active for debugging.
@@ -146,40 +144,59 @@ class Workforce(BaseNode):
146
144
  (default: :obj:`False`)
147
145
 
148
146
  Example:
149
- >>> # Configure with custom model and shared memory
150
147
  >>> import asyncio
148
+ >>> from camel.agents import ChatAgent
149
+ >>> from camel.models import ModelFactory
150
+ >>> from camel.types import ModelPlatformType, ModelType
151
+ >>> from camel.tasks import Task
152
+ >>>
153
+ >>> # Simple workforce with default agents
154
+ >>> workforce = Workforce("Research Team")
155
+ >>>
156
+ >>> # Workforce with custom model configuration
151
157
  >>> model = ModelFactory.create(
152
- ... ModelPlatformType.OPENAI, ModelType.GPT_4O
158
+ ... ModelPlatformType.OPENAI, model_type=ModelType.GPT_4O
153
159
  ... )
160
+ >>> coordinator_agent = ChatAgent(model=model)
161
+ >>> task_agent = ChatAgent(model=model)
162
+ >>>
154
163
  >>> workforce = Workforce(
155
164
  ... "Research Team",
156
- ... coordinator_agent_kwargs={"model": model, "token_limit": 4000},
157
- ... task_agent_kwargs={"model": model, "token_limit": 8000},
158
- ... share_memory=True # Enable shared memory
165
+ ... coordinator_agent=coordinator_agent,
166
+ ... task_agent=task_agent,
159
167
  ... )
160
168
  >>>
161
169
  >>> # Process a task
162
170
  >>> async def main():
163
171
  ... task = Task(content="Research AI trends", id="1")
164
- ... result = workforce.process_task(task)
172
+ ... result = await workforce.process_task_async(task)
165
173
  ... return result
166
- >>> asyncio.run(main())
174
+ >>>
175
+ >>> result_task = asyncio.run(main())
176
+
177
+ Note:
178
+ When custom coordinator_agent or task_agent are provided, the workforce
179
+ will preserve the user's system message and append the required
180
+ workforce coordination or task planning instructions to it. This
181
+ ensures both the user's intent is preserved and proper workforce
182
+ functionality is maintained. All other agent configurations (model,
183
+ memory, tools, etc.) will also be preserved.
167
184
  """
168
185
 
169
186
  def __init__(
170
187
  self,
171
188
  description: str,
172
189
  children: Optional[List[BaseNode]] = None,
173
- coordinator_agent_kwargs: Optional[Dict] = None,
174
- task_agent_kwargs: Optional[Dict] = None,
175
- new_worker_agent_kwargs: Optional[Dict] = None,
190
+ coordinator_agent: Optional[ChatAgent] = None,
191
+ task_agent: Optional[ChatAgent] = None,
192
+ new_worker_agent: Optional[ChatAgent] = None, # TODO: use MCP Agent
176
193
  graceful_shutdown_timeout: float = 15.0,
177
194
  share_memory: bool = False,
178
195
  ) -> None:
179
196
  super().__init__(description)
180
197
  self._child_listening_tasks: Deque[asyncio.Task] = deque()
181
198
  self._children = children or []
182
- self.new_worker_agent_kwargs = new_worker_agent_kwargs
199
+ self.new_worker_agent = new_worker_agent
183
200
  self.graceful_shutdown_timeout = graceful_shutdown_timeout
184
201
  self.share_memory = share_memory
185
202
  self.metrics_logger = WorkforceLogger(workforce_id=self.node_id)
@@ -213,58 +230,72 @@ class Workforce(BaseNode):
213
230
  role=role_or_desc,
214
231
  )
215
232
 
216
- # Warning messages for default model usage
217
- if coordinator_agent_kwargs is None:
218
- logger.warning(
219
- "No coordinator_agent_kwargs provided. Using default "
220
- "ChatAgent settings (ModelPlatformType.DEFAULT, "
221
- "ModelType.DEFAULT). To customize the coordinator agent "
222
- "that assigns tasks and handles failures, pass a dictionary "
223
- "with ChatAgent parameters, e.g.: {'model': your_model, "
224
- "'tools': your_tools, 'token_limit': 8000}. See ChatAgent "
225
- "documentation for all available options."
226
- )
227
- if task_agent_kwargs is None:
228
- logger.warning(
229
- "No task_agent_kwargs provided. Using default ChatAgent "
230
- "settings (ModelPlatformType.DEFAULT, ModelType.DEFAULT). "
231
- "To customize the task planning agent that "
232
- "decomposes/composes tasks, pass a dictionary with "
233
- "ChatAgent parameters, e.g.: {'model': your_model, "
234
- "'token_limit': 16000}. See ChatAgent documentation for "
235
- "all available options."
236
- )
237
- if new_worker_agent_kwargs is None:
238
- logger.warning(
239
- "No new_worker_agent_kwargs provided. Workers created at "
240
- "runtime will use default ChatAgent settings with "
241
- "SearchToolkit, CodeExecutionToolkit, and ThinkingToolkit. "
242
- "To customize runtime worker creation, pass a dictionary "
243
- "with ChatAgent parameters, e.g.: {'model': your_model, "
244
- "'tools': your_tools}. See ChatAgent documentation for all "
245
- "available options."
246
- )
247
-
248
- if self.share_memory:
249
- logger.info(
250
- "Shared memory enabled. All agents will share their complete "
251
- "conversation history and function-calling trajectory for "
252
- "better context continuity during task handoffs."
253
- )
254
-
233
+ # Set up coordinator agent with default system message
255
234
  coord_agent_sys_msg = BaseMessage.make_assistant_message(
256
235
  role_name="Workforce Manager",
257
- content="You are coordinating a group of workers. A worker can be "
258
- "a group of agents or a single agent. Each worker is "
236
+ content="You are coordinating a group of workers. A worker "
237
+ "can be a group of agents or a single agent. Each worker is "
259
238
  "created to solve a specific kind of task. Your job "
260
239
  "includes assigning tasks to a existing worker, creating "
261
240
  "a new worker for a task, etc.",
262
241
  )
263
- self.coordinator_agent = ChatAgent(
264
- coord_agent_sys_msg,
265
- **(coordinator_agent_kwargs or {}),
266
- )
267
242
 
243
+ if coordinator_agent is None:
244
+ logger.warning(
245
+ "No coordinator_agent provided. Using default "
246
+ "ChatAgent settings (ModelPlatformType.DEFAULT, "
247
+ "ModelType.DEFAULT) with default system message."
248
+ )
249
+ self.coordinator_agent = ChatAgent(coord_agent_sys_msg)
250
+ else:
251
+ logger.info(
252
+ "Custom coordinator_agent provided. Preserving user's "
253
+ "system message and appending workforce coordination "
254
+ "instructions to ensure proper functionality."
255
+ )
256
+
257
+ if coordinator_agent.system_message is not None:
258
+ user_sys_msg_content = coordinator_agent.system_message.content
259
+ combined_content = (
260
+ f"{user_sys_msg_content}\n\n"
261
+ f"{coord_agent_sys_msg.content}"
262
+ )
263
+ combined_sys_msg = BaseMessage.make_assistant_message(
264
+ role_name=coordinator_agent.system_message.role_name,
265
+ content=combined_content,
266
+ )
267
+ else:
268
+ combined_sys_msg = coord_agent_sys_msg
269
+
270
+ # Create a new agent with the provided agent's configuration
271
+ # but with the combined system message
272
+ self.coordinator_agent = ChatAgent(
273
+ system_message=combined_sys_msg,
274
+ model=coordinator_agent.model_backend,
275
+ memory=coordinator_agent.memory,
276
+ message_window_size=getattr(
277
+ coordinator_agent.memory, "window_size", None
278
+ ),
279
+ token_limit=getattr(
280
+ coordinator_agent.memory.get_context_creator(),
281
+ "token_limit",
282
+ None,
283
+ ),
284
+ output_language=coordinator_agent.output_language,
285
+ tools=[
286
+ tool.func
287
+ for tool in coordinator_agent._internal_tools.values()
288
+ ],
289
+ external_tools=[
290
+ schema
291
+ for schema in coordinator_agent._external_tool_schemas.values() # noqa: E501
292
+ ],
293
+ response_terminators=coordinator_agent.response_terminators,
294
+ max_iteration=coordinator_agent.max_iteration,
295
+ stop_event=coordinator_agent.stop_event,
296
+ )
297
+
298
+ # Set up task agent with default system message and required tools
268
299
  task_sys_msg = BaseMessage.make_assistant_message(
269
300
  role_name="Task Planner",
270
301
  content="You are going to compose and decompose tasks. Keep "
@@ -274,13 +305,83 @@ class Workforce(BaseNode):
274
305
  "of agents. This ensures efficient execution by minimizing "
275
306
  "context switching between agents.",
276
307
  )
277
- _task_agent_kwargs = dict(task_agent_kwargs or {})
278
- extra_tools = TaskPlanningToolkit().get_tools()
279
- _task_agent_kwargs["tools"] = [
280
- *_task_agent_kwargs.get("tools", []),
281
- *extra_tools,
282
- ]
283
- self.task_agent = ChatAgent(task_sys_msg, **_task_agent_kwargs)
308
+ task_planning_tools = TaskPlanningToolkit().get_tools()
309
+
310
+ if task_agent is None:
311
+ logger.warning(
312
+ "No task_agent provided. Using default ChatAgent "
313
+ "settings (ModelPlatformType.DEFAULT, ModelType.DEFAULT) "
314
+ "with default system message and TaskPlanningToolkit."
315
+ )
316
+ self.task_agent = ChatAgent(
317
+ task_sys_msg,
318
+ tools=TaskPlanningToolkit().get_tools(), # type: ignore[arg-type]
319
+ )
320
+ else:
321
+ logger.info(
322
+ "Custom task_agent provided. Preserving user's "
323
+ "system message and appending task planning "
324
+ "instructions to ensure proper functionality."
325
+ )
326
+
327
+ if task_agent.system_message is not None:
328
+ user_task_sys_msg_content = task_agent.system_message.content
329
+ combined_task_content = (
330
+ f"{user_task_sys_msg_content}\n\n"
331
+ f"{task_sys_msg.content}"
332
+ )
333
+ combined_task_sys_msg = BaseMessage.make_assistant_message(
334
+ role_name=task_agent.system_message.role_name,
335
+ content=combined_task_content,
336
+ )
337
+ else:
338
+ combined_task_sys_msg = task_sys_msg
339
+
340
+ # Since ChatAgent constructor uses a dictionary with
341
+ # function names as keys, we don't need to manually deduplicate.
342
+ combined_tools = [
343
+ tool.func for tool in task_agent._internal_tools.values()
344
+ ] + [tool.func for tool in task_planning_tools]
345
+
346
+ # Create a new agent with the provided agent's configuration
347
+ # but with the combined system message and tools
348
+ self.task_agent = ChatAgent(
349
+ system_message=combined_task_sys_msg,
350
+ model=task_agent.model_backend,
351
+ memory=task_agent.memory,
352
+ message_window_size=getattr(
353
+ task_agent.memory, "window_size", None
354
+ ),
355
+ token_limit=getattr(
356
+ task_agent.memory.get_context_creator(),
357
+ "token_limit",
358
+ None,
359
+ ),
360
+ output_language=task_agent.output_language,
361
+ tools=combined_tools,
362
+ external_tools=[
363
+ schema
364
+ for schema in task_agent._external_tool_schemas.values()
365
+ ],
366
+ response_terminators=task_agent.response_terminators,
367
+ max_iteration=task_agent.max_iteration,
368
+ stop_event=task_agent.stop_event,
369
+ )
370
+
371
+ if new_worker_agent is None:
372
+ logger.info(
373
+ "No new_worker_agent provided. Workers created at runtime "
374
+ "will use default ChatAgent settings with SearchToolkit, "
375
+ "CodeExecutionToolkit, and ThinkingToolkit. To customize "
376
+ "runtime worker creation, pass a ChatAgent instance."
377
+ )
378
+
379
+ if self.share_memory:
380
+ logger.info(
381
+ "Shared memory enabled. All agents will share their complete "
382
+ "conversation history and function-calling trajectory for "
383
+ "better context continuity during task handoffs."
384
+ )
284
385
 
285
386
  def __repr__(self):
286
387
  return (
@@ -1164,22 +1265,30 @@ class Workforce(BaseNode):
1164
1265
  )
1165
1266
  return info
1166
1267
 
1167
- def _find_assignee(
1168
- self,
1169
- tasks: List[Task],
1268
+ def _get_valid_worker_ids(self) -> set:
1269
+ r"""Get all valid worker IDs from child nodes.
1270
+
1271
+ Returns:
1272
+ set: Set of valid worker IDs that can be assigned tasks.
1273
+ """
1274
+ valid_worker_ids = {child.node_id for child in self._children}
1275
+ return valid_worker_ids
1276
+
1277
+ def _call_coordinator_for_assignment(
1278
+ self, tasks: List[Task], invalid_ids: Optional[List[str]] = None
1170
1279
  ) -> TaskAssignResult:
1171
- r"""Assigns multiple tasks to worker nodes with the best capabilities.
1280
+ r"""Call coordinator agent to assign tasks with optional validation
1281
+ feedback in the case of invalid worker IDs.
1172
1282
 
1173
- Parameters:
1174
- tasks (List[Task]): The tasks to be assigned.
1283
+ Args:
1284
+ tasks (List[Task]): Tasks to assign.
1285
+ invalid_ids (List[str], optional): Invalid worker IDs from previous
1286
+ attempt (if any).
1175
1287
 
1176
1288
  Returns:
1177
- TaskAssignResult: Assignment result containing task assignments
1178
- with their dependencies.
1289
+ TaskAssignResult: Assignment result from coordinator.
1179
1290
  """
1180
- self.coordinator_agent.reset()
1181
-
1182
- # Format tasks information for the prompt
1291
+ # format tasks information for the prompt
1183
1292
  tasks_info = ""
1184
1293
  for task in tasks:
1185
1294
  tasks_info += f"Task ID: {task.id}\n"
@@ -1188,29 +1297,220 @@ class Workforce(BaseNode):
1188
1297
  tasks_info += f"Additional Info: {task.additional_info}\n"
1189
1298
  tasks_info += "---\n"
1190
1299
 
1191
- prompt = ASSIGN_TASK_PROMPT.format(
1192
- tasks_info=tasks_info,
1193
- child_nodes_info=self._get_child_nodes_info(),
1300
+ prompt = str(
1301
+ ASSIGN_TASK_PROMPT.format(
1302
+ tasks_info=tasks_info,
1303
+ child_nodes_info=self._get_child_nodes_info(),
1304
+ )
1194
1305
  )
1195
1306
 
1196
- logger.debug(
1197
- f"Sending batch assignment request to coordinator "
1198
- f"for {len(tasks)} tasks."
1199
- )
1307
+ # add feedback if this is a retry
1308
+ if invalid_ids:
1309
+ valid_worker_ids = list(self._get_valid_worker_ids())
1310
+ feedback = (
1311
+ f"VALIDATION ERROR: The following worker IDs are invalid: "
1312
+ f"{invalid_ids}. "
1313
+ f"VALID WORKER IDS: {valid_worker_ids}. "
1314
+ f"Please reassign ONLY the above tasks using these valid IDs."
1315
+ )
1316
+ prompt = prompt + f"\n\n{feedback}"
1200
1317
 
1201
1318
  response = self.coordinator_agent.step(
1202
1319
  prompt, response_format=TaskAssignResult
1203
1320
  )
1321
+
1204
1322
  if response.msg is None or response.msg.content is None:
1205
1323
  logger.error(
1206
1324
  "Coordinator agent returned empty response for task assignment"
1207
1325
  )
1208
- # Return empty result as fallback
1209
1326
  return TaskAssignResult(assignments=[])
1210
1327
 
1211
- result_dict = json.loads(response.msg.content, parse_int=str)
1212
- task_assign_result = TaskAssignResult(**result_dict)
1213
- return task_assign_result
1328
+ try:
1329
+ result_dict = json.loads(response.msg.content, parse_int=str)
1330
+ return TaskAssignResult(**result_dict)
1331
+ except json.JSONDecodeError as e:
1332
+ logger.error(
1333
+ f"JSON parsing error in task assignment: Invalid response "
1334
+ f"format - {e}. Response content: "
1335
+ f"{response.msg.content[:50]}..."
1336
+ )
1337
+ return TaskAssignResult(assignments=[])
1338
+
1339
+ def _validate_assignments(
1340
+ self, assignments: List[TaskAssignment], valid_ids: Set[str]
1341
+ ) -> Tuple[List[TaskAssignment], List[TaskAssignment]]:
1342
+ r"""Validate task assignments against valid worker IDs.
1343
+
1344
+ Args:
1345
+ assignments (List[TaskAssignment]): Assignments to validate.
1346
+ valid_ids (Set[str]): Set of valid worker IDs.
1347
+
1348
+ Returns:
1349
+ Tuple[List[TaskAssignment], List[TaskAssignment]]:
1350
+ (valid_assignments, invalid_assignments)
1351
+ """
1352
+ valid_assignments: List[TaskAssignment] = []
1353
+ invalid_assignments: List[TaskAssignment] = []
1354
+
1355
+ for assignment in assignments:
1356
+ if assignment.assignee_id in valid_ids:
1357
+ valid_assignments.append(assignment)
1358
+ else:
1359
+ invalid_assignments.append(assignment)
1360
+
1361
+ return valid_assignments, invalid_assignments
1362
+
1363
+ def _handle_task_assignment_fallbacks(self, tasks: List[Task]) -> List:
1364
+ r"""Create new workers for unassigned tasks as fallback.
1365
+
1366
+ Args:
1367
+ tasks (List[Task]): Tasks that need new workers.
1368
+
1369
+ Returns:
1370
+ List[TaskAssignment]: Assignments for newly created workers.
1371
+ """
1372
+ fallback_assignments = []
1373
+
1374
+ for task in tasks:
1375
+ logger.info(f"Creating new worker for unassigned task {task.id}")
1376
+ new_worker = self._create_worker_node_for_task(task)
1377
+
1378
+ assignment = TaskAssignment(
1379
+ task_id=task.id,
1380
+ assignee_id=new_worker.node_id,
1381
+ dependencies=[],
1382
+ )
1383
+ fallback_assignments.append(assignment)
1384
+
1385
+ return fallback_assignments
1386
+
1387
+ def _handle_assignment_retry_and_fallback(
1388
+ self,
1389
+ invalid_assignments: List[TaskAssignment],
1390
+ tasks: List[Task],
1391
+ valid_worker_ids: Set[str],
1392
+ ) -> List[TaskAssignment]:
1393
+ r"""Called if Coordinator agent fails to assign tasks to valid worker
1394
+ IDs. Handles retry assignment and fallback worker creation for invalid
1395
+ assignments.
1396
+
1397
+ Args:
1398
+ invalid_assignments (List[TaskAssignment]): Invalid assignments to
1399
+ retry.
1400
+ tasks (List[Task]): Original tasks list for task lookup.
1401
+ valid_worker_ids (set): Set of valid worker IDs.
1402
+
1403
+ Returns:
1404
+ List[TaskAssignment]: Final assignments for the invalid tasks.
1405
+ """
1406
+ invalid_ids = [a.assignee_id for a in invalid_assignments]
1407
+ invalid_tasks = [
1408
+ task
1409
+ for task in tasks
1410
+ if any(a.task_id == task.id for a in invalid_assignments)
1411
+ ]
1412
+
1413
+ # handle cases where coordinator returned no assignments at all
1414
+ if not invalid_assignments:
1415
+ invalid_tasks = tasks # all tasks need assignment
1416
+ logger.warning(
1417
+ f"Coordinator returned no assignments. "
1418
+ f"Retrying assignment for all {len(invalid_tasks)} tasks."
1419
+ )
1420
+ else:
1421
+ logger.warning(
1422
+ f"Invalid worker IDs detected: {invalid_ids}. "
1423
+ f"Retrying assignment for {len(invalid_tasks)} tasks."
1424
+ )
1425
+
1426
+ # retry assignment with feedback
1427
+ retry_result = self._call_coordinator_for_assignment(
1428
+ invalid_tasks, invalid_ids
1429
+ )
1430
+ final_assignments = []
1431
+
1432
+ if retry_result.assignments:
1433
+ retry_valid, retry_invalid = self._validate_assignments(
1434
+ retry_result.assignments, valid_worker_ids
1435
+ )
1436
+ final_assignments.extend(retry_valid)
1437
+
1438
+ # collect tasks that are still unassigned for fallback
1439
+ if retry_invalid:
1440
+ unassigned_tasks = [
1441
+ task
1442
+ for task in invalid_tasks
1443
+ if any(a.task_id == task.id for a in retry_invalid)
1444
+ ]
1445
+ else:
1446
+ unassigned_tasks = []
1447
+ else:
1448
+ # retry failed completely, all invalid tasks need fallback
1449
+ logger.warning("Retry assignment failed")
1450
+ unassigned_tasks = invalid_tasks
1451
+
1452
+ # handle fallback for any remaining unassigned tasks
1453
+ if unassigned_tasks:
1454
+ logger.warning(
1455
+ f"Creating fallback workers for {len(unassigned_tasks)} "
1456
+ f"unassigned tasks"
1457
+ )
1458
+ fallback_assignments = self._handle_task_assignment_fallbacks(
1459
+ unassigned_tasks
1460
+ )
1461
+ final_assignments.extend(fallback_assignments)
1462
+
1463
+ return final_assignments
1464
+
1465
+ def _find_assignee(
1466
+ self,
1467
+ tasks: List[Task],
1468
+ ) -> TaskAssignResult:
1469
+ r"""Assigns multiple tasks to worker nodes with the best capabilities.
1470
+
1471
+ Parameters:
1472
+ tasks (List[Task]): The tasks to be assigned.
1473
+
1474
+ Returns:
1475
+ TaskAssignResult: Assignment result containing task assignments
1476
+ with their dependencies.
1477
+ """
1478
+ self.coordinator_agent.reset()
1479
+ valid_worker_ids = self._get_valid_worker_ids()
1480
+
1481
+ logger.debug(
1482
+ f"Sending batch assignment request to coordinator "
1483
+ f"for {len(tasks)} tasks."
1484
+ )
1485
+
1486
+ assignment_result = self._call_coordinator_for_assignment(tasks)
1487
+
1488
+ # validate assignments
1489
+ valid_assignments, invalid_assignments = self._validate_assignments(
1490
+ assignment_result.assignments, valid_worker_ids
1491
+ )
1492
+
1493
+ # check if we have assignments for all tasks
1494
+ assigned_task_ids = {
1495
+ a.task_id for a in valid_assignments + invalid_assignments
1496
+ }
1497
+ unassigned_tasks = [t for t in tasks if t.id not in assigned_task_ids]
1498
+
1499
+ # if all assignments are valid and all tasks are assigned, return early
1500
+ if not invalid_assignments and not unassigned_tasks:
1501
+ return TaskAssignResult(assignments=valid_assignments)
1502
+
1503
+ # handle retry and fallback for
1504
+ # invalid assignments and unassigned tasks
1505
+ all_problem_assignments = invalid_assignments
1506
+ retry_and_fallback_assignments = (
1507
+ self._handle_assignment_retry_and_fallback(
1508
+ all_problem_assignments, tasks, valid_worker_ids
1509
+ )
1510
+ )
1511
+ valid_assignments.extend(retry_and_fallback_assignments)
1512
+
1513
+ return TaskAssignResult(assignments=valid_assignments)
1214
1514
 
1215
1515
  async def _post_task(self, task: Task, assignee_id: str) -> None:
1216
1516
  # Record the start time when a task is posted
@@ -1258,8 +1558,19 @@ class Workforce(BaseNode):
1258
1558
  "with various tasks.",
1259
1559
  )
1260
1560
  else:
1261
- result_dict = json.loads(response.msg.content)
1262
- new_node_conf = WorkerConf(**result_dict)
1561
+ try:
1562
+ result_dict = json.loads(response.msg.content)
1563
+ new_node_conf = WorkerConf(**result_dict)
1564
+ except json.JSONDecodeError as e:
1565
+ logger.error(
1566
+ f"JSON parsing error in worker creation: Invalid response "
1567
+ f"format - {e}. Response content: "
1568
+ f"{response.msg.content[:100]}..."
1569
+ )
1570
+ raise RuntimeError(
1571
+ f"Failed to create worker for task {task.id}: "
1572
+ f"Coordinator agent returned malformed JSON response. "
1573
+ )
1263
1574
 
1264
1575
  new_agent = self._create_new_agent(
1265
1576
  new_node_conf.role,
@@ -1294,23 +1605,23 @@ class Workforce(BaseNode):
1294
1605
  content=sys_msg,
1295
1606
  )
1296
1607
 
1297
- if self.new_worker_agent_kwargs is not None:
1298
- return ChatAgent(worker_sys_msg, **self.new_worker_agent_kwargs)
1299
-
1300
- # Default tools for a new agent
1301
- function_list = [
1302
- SearchToolkit().search_duckduckgo,
1303
- *CodeExecutionToolkit().get_tools(),
1304
- *ThinkingToolkit().get_tools(),
1305
- ]
1608
+ if self.new_worker_agent is not None:
1609
+ return self.new_worker_agent
1610
+ else:
1611
+ # Default tools for a new agent
1612
+ function_list = [
1613
+ SearchToolkit().search_duckduckgo,
1614
+ *CodeExecutionToolkit().get_tools(),
1615
+ *ThinkingToolkit().get_tools(),
1616
+ ]
1306
1617
 
1307
- model = ModelFactory.create(
1308
- model_platform=ModelPlatformType.DEFAULT,
1309
- model_type=ModelType.DEFAULT,
1310
- model_config_dict={"temperature": 0},
1311
- )
1618
+ model = ModelFactory.create(
1619
+ model_platform=ModelPlatformType.DEFAULT,
1620
+ model_type=ModelType.DEFAULT,
1621
+ model_config_dict={"temperature": 0},
1622
+ )
1312
1623
 
1313
- return ChatAgent(worker_sys_msg, model=model, tools=function_list) # type: ignore[arg-type]
1624
+ return ChatAgent(worker_sys_msg, model=model, tools=function_list) # type: ignore[arg-type]
1314
1625
 
1315
1626
  async def _get_returned_task(self) -> Task:
1316
1627
  r"""Get the task that's published by this node and just get returned
@@ -1796,28 +2107,17 @@ class Workforce(BaseNode):
1796
2107
  """
1797
2108
 
1798
2109
  # Create a new instance with the same configuration
1799
- # Extract the original kwargs from the agents to properly clone them
1800
- coordinator_kwargs = (
1801
- getattr(self.coordinator_agent, 'init_kwargs', {}) or {}
1802
- )
1803
- task_kwargs = getattr(self.task_agent, 'init_kwargs', {}) or {}
1804
-
1805
2110
  new_instance = Workforce(
1806
2111
  description=self.description,
1807
- coordinator_agent_kwargs=coordinator_kwargs.copy(),
1808
- task_agent_kwargs=task_kwargs.copy(),
1809
- new_worker_agent_kwargs=self.new_worker_agent_kwargs.copy()
1810
- if self.new_worker_agent_kwargs
2112
+ coordinator_agent=self.coordinator_agent.clone(with_memory),
2113
+ task_agent=self.task_agent.clone(with_memory),
2114
+ new_worker_agent=self.new_worker_agent.clone(with_memory)
2115
+ if self.new_worker_agent
1811
2116
  else None,
1812
2117
  graceful_shutdown_timeout=self.graceful_shutdown_timeout,
1813
2118
  share_memory=self.share_memory,
1814
2119
  )
1815
2120
 
1816
- new_instance.task_agent = self.task_agent.clone(with_memory)
1817
- new_instance.coordinator_agent = self.coordinator_agent.clone(
1818
- with_memory
1819
- )
1820
-
1821
2121
  for child in self._children:
1822
2122
  if isinstance(child, SingleAgentWorker):
1823
2123
  cloned_worker = child.worker.clone(with_memory)