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.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +220 -7
- camel/memories/context_creators/score_based.py +11 -6
- camel/messages/base.py +2 -2
- camel/societies/role_playing.py +26 -28
- camel/societies/workforce/workforce.py +439 -139
- camel/societies/workforce/workforce_logger.py +37 -23
- camel/storages/__init__.py +4 -0
- camel/storages/vectordb_storages/__init__.py +4 -0
- camel/storages/vectordb_storages/chroma.py +731 -0
- camel/storages/vectordb_storages/pgvector.py +349 -0
- camel/tasks/task.py +30 -2
- camel/toolkits/__init__.py +2 -1
- camel/toolkits/excel_toolkit.py +814 -69
- camel/toolkits/file_write_toolkit.py +21 -7
- camel/toolkits/google_drive_mcp_toolkit.py +73 -0
- camel/toolkits/mcp_toolkit.py +31 -1
- camel/toolkits/terminal_toolkit.py +11 -4
- camel/types/enums.py +9 -6
- {camel_ai-0.2.69a6.dist-info → camel_ai-0.2.70.dist-info}/METADATA +8 -1
- {camel_ai-0.2.69a6.dist-info → camel_ai-0.2.70.dist-info}/RECORD +23 -20
- {camel_ai-0.2.69a6.dist-info → camel_ai-0.2.70.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.69a6.dist-info → camel_ai-0.2.70.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
(default: :obj:`None`
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
...
|
|
157
|
-
...
|
|
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.
|
|
172
|
+
... result = await workforce.process_task_async(task)
|
|
165
173
|
... return result
|
|
166
|
-
>>>
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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.
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
1168
|
-
|
|
1169
|
-
|
|
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"""
|
|
1280
|
+
r"""Call coordinator agent to assign tasks with optional validation
|
|
1281
|
+
feedback in the case of invalid worker IDs.
|
|
1172
1282
|
|
|
1173
|
-
|
|
1174
|
-
tasks (List[Task]):
|
|
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
|
|
1178
|
-
with their dependencies.
|
|
1289
|
+
TaskAssignResult: Assignment result from coordinator.
|
|
1179
1290
|
"""
|
|
1180
|
-
|
|
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 =
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
-
|
|
1262
|
-
|
|
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.
|
|
1298
|
-
return
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
if self.
|
|
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)
|