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.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +170 -11
- camel/configs/vllm_config.py +2 -0
- camel/datagen/self_improving_cot.py +1 -1
- camel/memories/context_creators/score_based.py +129 -87
- camel/runtimes/configs.py +11 -11
- camel/runtimes/daytona_runtime.py +4 -4
- camel/runtimes/docker_runtime.py +6 -6
- camel/runtimes/remote_http_runtime.py +5 -5
- camel/societies/workforce/prompts.py +13 -12
- camel/societies/workforce/single_agent_worker.py +252 -22
- camel/societies/workforce/utils.py +10 -2
- camel/societies/workforce/worker.py +21 -45
- camel/societies/workforce/workforce.py +36 -15
- camel/tasks/task.py +18 -12
- camel/toolkits/__init__.py +2 -0
- camel/toolkits/aci_toolkit.py +19 -19
- camel/toolkits/arxiv_toolkit.py +6 -6
- camel/toolkits/dappier_toolkit.py +5 -5
- camel/toolkits/file_write_toolkit.py +10 -10
- camel/toolkits/github_toolkit.py +3 -3
- camel/toolkits/non_visual_browser_toolkit/__init__.py +18 -0
- camel/toolkits/non_visual_browser_toolkit/actions.py +196 -0
- camel/toolkits/non_visual_browser_toolkit/agent.py +278 -0
- camel/toolkits/non_visual_browser_toolkit/browser_non_visual_toolkit.py +363 -0
- camel/toolkits/non_visual_browser_toolkit/nv_browser_session.py +175 -0
- camel/toolkits/non_visual_browser_toolkit/snapshot.js +188 -0
- camel/toolkits/non_visual_browser_toolkit/snapshot.py +164 -0
- camel/toolkits/pptx_toolkit.py +4 -4
- camel/toolkits/sympy_toolkit.py +1 -1
- camel/toolkits/task_planning_toolkit.py +3 -3
- camel/toolkits/thinking_toolkit.py +1 -1
- {camel_ai-0.2.68.dist-info → camel_ai-0.2.69a1.dist-info}/METADATA +1 -1
- {camel_ai-0.2.68.dist-info → camel_ai-0.2.69a1.dist-info}/RECORD +36 -29
- {camel_ai-0.2.68.dist-info → camel_ai-0.2.69a1.dist-info}/WHEEL +0 -0
- {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
|
|
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
|
|
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
|
|
93
|
+
the function. (default: :obj:`False`)
|
|
94
94
|
arguments (Optional[Dict[str, Any]]): The arguments for the
|
|
95
|
-
function. (default: :obj
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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"(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1009
|
-
|
|
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,
|
|
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
|
-
|
|
1235
|
-
|
|
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
|
-
|
|
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
|
|
1409
|
+
if task.failure_count > 3:
|
|
1388
1410
|
return True
|
|
1389
1411
|
|
|
1390
|
-
if task.get_depth()
|
|
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:
|
|
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
|
-
|
|
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))
|