camel-ai 0.2.75a6__py3-none-any.whl → 0.2.76__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 +1001 -205
- camel/agents/mcp_agent.py +30 -27
- camel/configs/__init__.py +6 -0
- camel/configs/amd_config.py +70 -0
- camel/configs/cometapi_config.py +104 -0
- camel/data_collectors/alpaca_collector.py +15 -6
- camel/environments/tic_tac_toe.py +1 -1
- camel/interpreters/__init__.py +2 -0
- camel/interpreters/docker/Dockerfile +3 -12
- camel/interpreters/microsandbox_interpreter.py +395 -0
- camel/loaders/__init__.py +11 -2
- camel/loaders/chunkr_reader.py +9 -0
- camel/memories/__init__.py +2 -1
- camel/memories/agent_memories.py +3 -1
- camel/memories/blocks/chat_history_block.py +21 -3
- camel/memories/records.py +88 -8
- camel/messages/base.py +127 -34
- camel/models/__init__.py +4 -0
- camel/models/amd_model.py +101 -0
- camel/models/azure_openai_model.py +0 -6
- camel/models/base_model.py +30 -0
- camel/models/cometapi_model.py +83 -0
- camel/models/model_factory.py +4 -0
- camel/models/openai_compatible_model.py +0 -6
- camel/models/openai_model.py +0 -6
- camel/models/zhipuai_model.py +61 -2
- camel/parsers/__init__.py +18 -0
- camel/parsers/mcp_tool_call_parser.py +176 -0
- camel/retrievers/auto_retriever.py +1 -0
- camel/runtimes/daytona_runtime.py +11 -12
- camel/societies/workforce/prompts.py +131 -50
- camel/societies/workforce/single_agent_worker.py +434 -49
- camel/societies/workforce/structured_output_handler.py +30 -18
- camel/societies/workforce/task_channel.py +43 -0
- camel/societies/workforce/utils.py +105 -12
- camel/societies/workforce/workforce.py +1322 -311
- camel/societies/workforce/workforce_logger.py +24 -5
- camel/storages/key_value_storages/json.py +15 -2
- camel/storages/object_storages/google_cloud.py +1 -1
- camel/storages/vectordb_storages/oceanbase.py +10 -11
- camel/storages/vectordb_storages/tidb.py +8 -6
- camel/tasks/task.py +4 -3
- camel/toolkits/__init__.py +18 -5
- camel/toolkits/aci_toolkit.py +45 -0
- camel/toolkits/code_execution.py +28 -1
- camel/toolkits/context_summarizer_toolkit.py +684 -0
- camel/toolkits/dingtalk.py +1135 -0
- camel/toolkits/edgeone_pages_mcp_toolkit.py +11 -31
- camel/toolkits/{file_write_toolkit.py → file_toolkit.py} +194 -34
- camel/toolkits/function_tool.py +6 -1
- camel/toolkits/google_drive_mcp_toolkit.py +12 -31
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +12 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +79 -2
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +95 -59
- camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +5 -612
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +0 -1
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +619 -95
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +7 -2
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +115 -219
- camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +1 -0
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +39 -6
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +405 -131
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +9 -5
- camel/toolkits/{openai_image_toolkit.py → image_generation_toolkit.py} +98 -31
- camel/toolkits/markitdown_toolkit.py +27 -1
- camel/toolkits/mcp_toolkit.py +348 -348
- camel/toolkits/message_integration.py +3 -0
- camel/toolkits/minimax_mcp_toolkit.py +195 -0
- camel/toolkits/note_taking_toolkit.py +18 -8
- camel/toolkits/notion_mcp_toolkit.py +16 -26
- camel/toolkits/origene_mcp_toolkit.py +8 -49
- camel/toolkits/playwright_mcp_toolkit.py +12 -31
- camel/toolkits/resend_toolkit.py +168 -0
- camel/toolkits/slack_toolkit.py +50 -1
- camel/toolkits/terminal_toolkit/__init__.py +18 -0
- camel/toolkits/terminal_toolkit/terminal_toolkit.py +924 -0
- camel/toolkits/terminal_toolkit/utils.py +532 -0
- camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
- camel/toolkits/video_analysis_toolkit.py +17 -11
- camel/toolkits/wechat_official_toolkit.py +483 -0
- camel/types/enums.py +124 -1
- camel/types/unified_model_type.py +5 -0
- camel/utils/commons.py +17 -0
- camel/utils/context_utils.py +804 -0
- camel/utils/mcp.py +136 -2
- camel/utils/token_counting.py +25 -17
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/METADATA +158 -59
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/RECORD +95 -76
- camel/loaders/pandas_reader.py +0 -368
- camel/toolkits/terminal_toolkit.py +0 -1788
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,14 +15,19 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
17
17
|
import datetime
|
|
18
|
+
import glob
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
18
21
|
import time
|
|
19
22
|
from collections import deque
|
|
20
|
-
from
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Dict, List, Optional
|
|
21
25
|
|
|
22
26
|
from colorama import Fore
|
|
23
27
|
|
|
24
28
|
from camel.agents import ChatAgent
|
|
25
29
|
from camel.agents.chat_agent import AsyncStreamingChatAgentResponse
|
|
30
|
+
from camel.logger import get_logger
|
|
26
31
|
from camel.societies.workforce.prompts import PROCESS_TASK_PROMPT
|
|
27
32
|
from camel.societies.workforce.structured_output_handler import (
|
|
28
33
|
StructuredOutputHandler,
|
|
@@ -30,13 +35,16 @@ from camel.societies.workforce.structured_output_handler import (
|
|
|
30
35
|
from camel.societies.workforce.utils import TaskResult
|
|
31
36
|
from camel.societies.workforce.worker import Worker
|
|
32
37
|
from camel.tasks.task import Task, TaskState, is_task_result_insufficient
|
|
38
|
+
from camel.utils.context_utils import ContextUtility, WorkflowSummary
|
|
39
|
+
|
|
40
|
+
logger = get_logger(__name__)
|
|
33
41
|
|
|
34
42
|
|
|
35
43
|
class AgentPool:
|
|
36
44
|
r"""A pool of agent instances for efficient reuse.
|
|
37
45
|
|
|
38
|
-
This pool manages a collection of pre-cloned agents
|
|
39
|
-
|
|
46
|
+
This pool manages a collection of pre-cloned agents with automatic
|
|
47
|
+
scaling and idle timeout cleanup.
|
|
40
48
|
|
|
41
49
|
Args:
|
|
42
50
|
base_agent (ChatAgent): The base agent to clone from.
|
|
@@ -48,6 +56,8 @@ class AgentPool:
|
|
|
48
56
|
(default: :obj:`True`)
|
|
49
57
|
idle_timeout (float): Time in seconds after which idle agents are
|
|
50
58
|
removed. (default: :obj:`180.0`)
|
|
59
|
+
cleanup_interval (float): Fixed interval in seconds between cleanup
|
|
60
|
+
checks. (default: :obj:`60.0`)
|
|
51
61
|
"""
|
|
52
62
|
|
|
53
63
|
def __init__(
|
|
@@ -56,12 +66,14 @@ class AgentPool:
|
|
|
56
66
|
initial_size: int = 1,
|
|
57
67
|
max_size: int = 10,
|
|
58
68
|
auto_scale: bool = True,
|
|
59
|
-
idle_timeout: float = 180.0,
|
|
69
|
+
idle_timeout: float = 180.0,
|
|
70
|
+
cleanup_interval: float = 60.0,
|
|
60
71
|
):
|
|
61
72
|
self.base_agent = base_agent
|
|
62
73
|
self.max_size = max_size
|
|
63
74
|
self.auto_scale = auto_scale
|
|
64
75
|
self.idle_timeout = idle_timeout
|
|
76
|
+
self.cleanup_interval = cleanup_interval
|
|
65
77
|
|
|
66
78
|
# Pool management
|
|
67
79
|
self._available_agents: deque = deque()
|
|
@@ -73,6 +85,7 @@ class AgentPool:
|
|
|
73
85
|
self._total_borrows = 0
|
|
74
86
|
self._total_clones_created = 0
|
|
75
87
|
self._pool_hits = 0
|
|
88
|
+
self._agents_cleaned = 0
|
|
76
89
|
|
|
77
90
|
# Initialize pool
|
|
78
91
|
self._initialize_pool(initial_size)
|
|
@@ -82,6 +95,7 @@ class AgentPool:
|
|
|
82
95
|
for _ in range(min(size, self.max_size)):
|
|
83
96
|
agent = self._create_fresh_agent()
|
|
84
97
|
self._available_agents.append(agent)
|
|
98
|
+
self._agent_last_used[id(agent)] = time.time()
|
|
85
99
|
|
|
86
100
|
def _create_fresh_agent(self) -> ChatAgent:
|
|
87
101
|
r"""Create a fresh agent instance."""
|
|
@@ -94,53 +108,46 @@ class AgentPool:
|
|
|
94
108
|
async with self._lock:
|
|
95
109
|
self._total_borrows += 1
|
|
96
110
|
|
|
97
|
-
# Try to get from available agents first
|
|
98
111
|
if self._available_agents:
|
|
99
112
|
agent = self._available_agents.popleft()
|
|
100
113
|
self._in_use_agents.add(id(agent))
|
|
101
114
|
self._pool_hits += 1
|
|
102
|
-
|
|
103
|
-
# Reset the agent state
|
|
104
|
-
agent.reset()
|
|
105
115
|
return agent
|
|
106
116
|
|
|
107
|
-
# Check if we can create new
|
|
108
|
-
|
|
109
|
-
self._in_use_agents
|
|
110
|
-
)
|
|
111
|
-
if total_agents < self.max_size:
|
|
117
|
+
# Check if we can create a new agent
|
|
118
|
+
if len(self._in_use_agents) < self.max_size or self.auto_scale:
|
|
112
119
|
agent = self._create_fresh_agent()
|
|
113
120
|
self._in_use_agents.add(id(agent))
|
|
114
121
|
return agent
|
|
115
122
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
agent = self._available_agents.popleft()
|
|
126
|
-
self._in_use_agents.add(id(agent))
|
|
127
|
-
agent.reset()
|
|
128
|
-
return agent
|
|
123
|
+
# Wait for available agent
|
|
124
|
+
while True:
|
|
125
|
+
async with self._lock:
|
|
126
|
+
if self._available_agents:
|
|
127
|
+
agent = self._available_agents.popleft()
|
|
128
|
+
self._in_use_agents.add(id(agent))
|
|
129
|
+
self._pool_hits += 1
|
|
130
|
+
return agent
|
|
131
|
+
await asyncio.sleep(0.05)
|
|
129
132
|
|
|
130
133
|
async def return_agent(self, agent: ChatAgent) -> None:
|
|
131
134
|
r"""Return an agent to the pool."""
|
|
135
|
+
agent_id = id(agent)
|
|
136
|
+
|
|
132
137
|
async with self._lock:
|
|
133
|
-
agent_id
|
|
138
|
+
if agent_id not in self._in_use_agents:
|
|
139
|
+
return
|
|
134
140
|
|
|
135
|
-
|
|
136
|
-
self._in_use_agents.remove(agent_id)
|
|
141
|
+
self._in_use_agents.discard(agent_id)
|
|
137
142
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
# Only add back to pool if under max size
|
|
144
|
+
if len(self._available_agents) < self.max_size:
|
|
145
|
+
agent.reset()
|
|
146
|
+
self._agent_last_used[agent_id] = time.time()
|
|
147
|
+
self._available_agents.append(agent)
|
|
148
|
+
else:
|
|
149
|
+
# Remove tracking for agents not returned to pool
|
|
150
|
+
self._agent_last_used.pop(agent_id, None)
|
|
144
151
|
|
|
145
152
|
async def cleanup_idle_agents(self) -> None:
|
|
146
153
|
r"""Remove idle agents from the pool to free memory."""
|
|
@@ -148,30 +155,35 @@ class AgentPool:
|
|
|
148
155
|
return
|
|
149
156
|
|
|
150
157
|
async with self._lock:
|
|
158
|
+
if not self._available_agents:
|
|
159
|
+
return
|
|
160
|
+
|
|
151
161
|
current_time = time.time()
|
|
152
162
|
agents_to_remove = []
|
|
153
163
|
|
|
154
164
|
for agent in list(self._available_agents):
|
|
155
165
|
agent_id = id(agent)
|
|
156
166
|
last_used = self._agent_last_used.get(agent_id, current_time)
|
|
157
|
-
|
|
158
167
|
if current_time - last_used > self.idle_timeout:
|
|
159
168
|
agents_to_remove.append(agent)
|
|
160
169
|
|
|
161
170
|
for agent in agents_to_remove:
|
|
162
171
|
self._available_agents.remove(agent)
|
|
163
|
-
|
|
164
|
-
self.
|
|
172
|
+
self._agent_last_used.pop(id(agent), None)
|
|
173
|
+
self._agents_cleaned += 1
|
|
165
174
|
|
|
166
175
|
def get_stats(self) -> dict:
|
|
167
176
|
r"""Get pool statistics."""
|
|
168
177
|
return {
|
|
169
178
|
"available_agents": len(self._available_agents),
|
|
170
179
|
"in_use_agents": len(self._in_use_agents),
|
|
180
|
+
"pool_size": len(self._available_agents)
|
|
181
|
+
+ len(self._in_use_agents),
|
|
171
182
|
"total_borrows": self._total_borrows,
|
|
172
183
|
"total_clones_created": self._total_clones_created,
|
|
173
184
|
"pool_hits": self._pool_hits,
|
|
174
185
|
"hit_rate": self._pool_hits / max(self._total_borrows, 1),
|
|
186
|
+
"agents_cleaned_up": self._agents_cleaned,
|
|
175
187
|
}
|
|
176
188
|
|
|
177
189
|
|
|
@@ -197,6 +209,16 @@ class SingleAgentWorker(Worker):
|
|
|
197
209
|
support native structured output. When disabled, the workforce
|
|
198
210
|
uses the native response_format parameter.
|
|
199
211
|
(default: :obj:`True`)
|
|
212
|
+
context_utility (ContextUtility, optional): Shared context utility
|
|
213
|
+
instance for workflow management. If provided, all workflow
|
|
214
|
+
operations will use this shared instance instead of creating
|
|
215
|
+
a new one. This ensures multiple workers share the same session
|
|
216
|
+
directory. (default: :obj:`None`)
|
|
217
|
+
enable_workflow_memory (bool, optional): Whether to enable workflow
|
|
218
|
+
memory accumulation during task execution. When enabled,
|
|
219
|
+
conversations from all task executions are accumulated for
|
|
220
|
+
potential workflow saving. Set to True if you plan to call
|
|
221
|
+
save_workflow_memories(). (default: :obj:`False`)
|
|
200
222
|
"""
|
|
201
223
|
|
|
202
224
|
def __init__(
|
|
@@ -208,6 +230,8 @@ class SingleAgentWorker(Worker):
|
|
|
208
230
|
pool_max_size: int = 10,
|
|
209
231
|
auto_scale_pool: bool = True,
|
|
210
232
|
use_structured_output_handler: bool = True,
|
|
233
|
+
context_utility: Optional[ContextUtility] = None,
|
|
234
|
+
enable_workflow_memory: bool = False,
|
|
211
235
|
) -> None:
|
|
212
236
|
node_id = worker.agent_id
|
|
213
237
|
super().__init__(
|
|
@@ -222,6 +246,18 @@ class SingleAgentWorker(Worker):
|
|
|
222
246
|
)
|
|
223
247
|
self.worker = worker
|
|
224
248
|
self.use_agent_pool = use_agent_pool
|
|
249
|
+
self.enable_workflow_memory = enable_workflow_memory
|
|
250
|
+
self._shared_context_utility = context_utility
|
|
251
|
+
self._context_utility: Optional[ContextUtility] = (
|
|
252
|
+
None # Will be initialized when needed
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# accumulator agent for collecting conversations
|
|
256
|
+
# from all task processing
|
|
257
|
+
self._conversation_accumulator: Optional[ChatAgent] = None
|
|
258
|
+
|
|
259
|
+
# note: context utility is set on the worker agent during save/load
|
|
260
|
+
# operations to avoid creating session folders during initialization
|
|
225
261
|
|
|
226
262
|
self.agent_pool: Optional[AgentPool] = None
|
|
227
263
|
self._cleanup_task: Optional[asyncio.Task] = None
|
|
@@ -264,6 +300,24 @@ class SingleAgentWorker(Worker):
|
|
|
264
300
|
await self.agent_pool.return_agent(agent)
|
|
265
301
|
# If not using pool, agent will be garbage collected
|
|
266
302
|
|
|
303
|
+
def _get_context_utility(self) -> ContextUtility:
|
|
304
|
+
r"""Get context utility with lazy initialization."""
|
|
305
|
+
if self._context_utility is None:
|
|
306
|
+
self._context_utility = (
|
|
307
|
+
self._shared_context_utility
|
|
308
|
+
or ContextUtility.get_workforce_shared()
|
|
309
|
+
)
|
|
310
|
+
return self._context_utility
|
|
311
|
+
|
|
312
|
+
def _get_conversation_accumulator(self) -> ChatAgent:
|
|
313
|
+
r"""Get or create the conversation accumulator agent."""
|
|
314
|
+
if self._conversation_accumulator is None:
|
|
315
|
+
# create a clone of the original worker to serve as accumulator
|
|
316
|
+
self._conversation_accumulator = self.worker.clone(
|
|
317
|
+
with_memory=False
|
|
318
|
+
)
|
|
319
|
+
return self._conversation_accumulator
|
|
320
|
+
|
|
267
321
|
async def _process_task(
|
|
268
322
|
self, task: Task, dependencies: List[Task]
|
|
269
323
|
) -> TaskState:
|
|
@@ -381,10 +435,36 @@ class SingleAgentWorker(Worker):
|
|
|
381
435
|
usage_info.get("total_tokens", 0) if usage_info else 0
|
|
382
436
|
)
|
|
383
437
|
|
|
438
|
+
# collect conversation from working agent to
|
|
439
|
+
# accumulator for workflow memory
|
|
440
|
+
# Only transfer memory if workflow memory is enabled
|
|
441
|
+
if self.enable_workflow_memory:
|
|
442
|
+
accumulator = self._get_conversation_accumulator()
|
|
443
|
+
|
|
444
|
+
# transfer all memory records from working agent to accumulator
|
|
445
|
+
try:
|
|
446
|
+
# retrieve all context records from the working agent
|
|
447
|
+
work_records = worker_agent.memory.retrieve()
|
|
448
|
+
|
|
449
|
+
# write these records to the accumulator's memory
|
|
450
|
+
memory_records = [
|
|
451
|
+
record.memory_record for record in work_records
|
|
452
|
+
]
|
|
453
|
+
accumulator.memory.write_records(memory_records)
|
|
454
|
+
|
|
455
|
+
logger.debug(
|
|
456
|
+
f"Transferred {len(memory_records)} memory records to "
|
|
457
|
+
f"accumulator"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
except Exception as e:
|
|
461
|
+
logger.warning(
|
|
462
|
+
f"Failed to transfer conversation to accumulator: {e}"
|
|
463
|
+
)
|
|
464
|
+
|
|
384
465
|
except Exception as e:
|
|
385
|
-
|
|
386
|
-
f"
|
|
387
|
-
f"{type(e).__name__}: {e}{Fore.RESET}"
|
|
466
|
+
logger.error(
|
|
467
|
+
f"Error processing task {task.id}: {type(e).__name__}: {e}"
|
|
388
468
|
)
|
|
389
469
|
# Store error information in task result
|
|
390
470
|
task.result = f"{type(e).__name__}: {e!s}"
|
|
@@ -429,13 +509,13 @@ class SingleAgentWorker(Worker):
|
|
|
429
509
|
task.additional_info["token_usage"] = {"total_tokens": total_tokens}
|
|
430
510
|
|
|
431
511
|
print(f"======\n{Fore.GREEN}Response from {self}:{Fore.RESET}")
|
|
512
|
+
logger.info(f"Response from {self}:")
|
|
432
513
|
|
|
433
514
|
if not self.use_structured_output_handler:
|
|
434
515
|
# Handle native structured output parsing
|
|
435
516
|
if task_result is None:
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
f"task result{Fore.RESET}"
|
|
517
|
+
logger.error(
|
|
518
|
+
"Error in worker step execution: Invalid task result"
|
|
439
519
|
)
|
|
440
520
|
task_result = TaskResult(
|
|
441
521
|
content="Failed to generate valid task result.",
|
|
@@ -446,6 +526,10 @@ class SingleAgentWorker(Worker):
|
|
|
446
526
|
print(
|
|
447
527
|
f"\n{color}{task_result.content}{Fore.RESET}\n======", # type: ignore[union-attr]
|
|
448
528
|
)
|
|
529
|
+
if task_result.failed: # type: ignore[union-attr]
|
|
530
|
+
logger.error(f"{task_result.content}") # type: ignore[union-attr]
|
|
531
|
+
else:
|
|
532
|
+
logger.info(f"{task_result.content}") # type: ignore[union-attr]
|
|
449
533
|
|
|
450
534
|
task.result = task_result.content # type: ignore[union-attr]
|
|
451
535
|
|
|
@@ -453,9 +537,9 @@ class SingleAgentWorker(Worker):
|
|
|
453
537
|
return TaskState.FAILED
|
|
454
538
|
|
|
455
539
|
if is_task_result_insufficient(task):
|
|
456
|
-
|
|
457
|
-
f"
|
|
458
|
-
f"task marked as failed
|
|
540
|
+
logger.warning(
|
|
541
|
+
f"Task {task.id}: Content validation failed - "
|
|
542
|
+
f"task marked as failed"
|
|
459
543
|
)
|
|
460
544
|
return TaskState.FAILED
|
|
461
545
|
return TaskState.DONE
|
|
@@ -477,16 +561,317 @@ class SingleAgentWorker(Worker):
|
|
|
477
561
|
r"""Periodically clean up idle agents from the pool."""
|
|
478
562
|
while True:
|
|
479
563
|
try:
|
|
480
|
-
|
|
564
|
+
# Fixed interval cleanup
|
|
565
|
+
await asyncio.sleep(self.agent_pool.cleanup_interval)
|
|
566
|
+
|
|
481
567
|
if self.agent_pool:
|
|
482
568
|
await self.agent_pool.cleanup_idle_agents()
|
|
483
569
|
except asyncio.CancelledError:
|
|
484
570
|
break
|
|
485
571
|
except Exception as e:
|
|
486
|
-
|
|
572
|
+
logger.warning(f"Error in pool cleanup: {e}")
|
|
487
573
|
|
|
488
574
|
def get_pool_stats(self) -> Optional[dict]:
|
|
489
575
|
r"""Get agent pool statistics if pool is enabled."""
|
|
490
576
|
if self.use_agent_pool and self.agent_pool:
|
|
491
577
|
return self.agent_pool.get_stats()
|
|
492
578
|
return None
|
|
579
|
+
|
|
580
|
+
def save_workflow_memories(self) -> Dict[str, Any]:
|
|
581
|
+
r"""Save the worker's current workflow memories using agent
|
|
582
|
+
summarization.
|
|
583
|
+
|
|
584
|
+
This method generates a workflow summary from the worker agent's
|
|
585
|
+
conversation history and saves it to a markdown file. The filename
|
|
586
|
+
is based on the worker's description for easy loading later.
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
Dict[str, Any]: Result dictionary with keys:
|
|
590
|
+
- status (str): "success" or "error"
|
|
591
|
+
- summary (str): Generated workflow summary
|
|
592
|
+
- file_path (str): Path to saved file
|
|
593
|
+
- worker_description (str): Worker description used
|
|
594
|
+
"""
|
|
595
|
+
try:
|
|
596
|
+
# validate requirements
|
|
597
|
+
validation_error = self._validate_workflow_save_requirements()
|
|
598
|
+
if validation_error:
|
|
599
|
+
return validation_error
|
|
600
|
+
|
|
601
|
+
# setup context utility and agent
|
|
602
|
+
context_util = self._get_context_utility()
|
|
603
|
+
self.worker.set_context_utility(context_util)
|
|
604
|
+
|
|
605
|
+
# prepare workflow summarization components
|
|
606
|
+
filename = self._generate_workflow_filename()
|
|
607
|
+
structured_prompt = self._prepare_workflow_prompt()
|
|
608
|
+
agent_to_summarize = self._select_agent_for_summarization(
|
|
609
|
+
context_util
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# generate and save workflow summary
|
|
613
|
+
result = agent_to_summarize.summarize(
|
|
614
|
+
filename=filename,
|
|
615
|
+
summary_prompt=structured_prompt,
|
|
616
|
+
response_format=WorkflowSummary,
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# add worker metadata and cleanup
|
|
620
|
+
result["worker_description"] = self.description
|
|
621
|
+
if self._conversation_accumulator is not None:
|
|
622
|
+
logger.info(
|
|
623
|
+
"Cleaning up conversation accumulator after workflow "
|
|
624
|
+
"summarization"
|
|
625
|
+
)
|
|
626
|
+
self._conversation_accumulator = None
|
|
627
|
+
|
|
628
|
+
return result
|
|
629
|
+
|
|
630
|
+
except Exception as e:
|
|
631
|
+
return {
|
|
632
|
+
"status": "error",
|
|
633
|
+
"summary": "",
|
|
634
|
+
"file_path": None,
|
|
635
|
+
"worker_description": self.description,
|
|
636
|
+
"message": f"Failed to save workflow memories: {e!s}",
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
def load_workflow_memories(
|
|
640
|
+
self,
|
|
641
|
+
pattern: Optional[str] = None,
|
|
642
|
+
max_files_to_load: int = 3,
|
|
643
|
+
session_id: Optional[str] = None,
|
|
644
|
+
) -> bool:
|
|
645
|
+
r"""Load workflow memories matching worker description
|
|
646
|
+
from saved files.
|
|
647
|
+
|
|
648
|
+
This method searches for workflow memory files that match the worker's
|
|
649
|
+
description and loads them into the agent's memory using
|
|
650
|
+
ContextUtility.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
pattern (Optional[str]): Custom search pattern for workflow
|
|
654
|
+
memory files.
|
|
655
|
+
If None, uses worker description to generate pattern.
|
|
656
|
+
max_files_to_load (int): Maximum number of workflow files to load.
|
|
657
|
+
(default: :obj:`3`)
|
|
658
|
+
session_id (Optional[str]): Specific workforce session ID to load
|
|
659
|
+
from. If None, searches across all sessions.
|
|
660
|
+
(default: :obj:`None`)
|
|
661
|
+
|
|
662
|
+
Returns:
|
|
663
|
+
bool: True if workflow memories were successfully loaded, False
|
|
664
|
+
otherwise.
|
|
665
|
+
"""
|
|
666
|
+
try:
|
|
667
|
+
# reset system message to original state before loading
|
|
668
|
+
# this prevents duplicate workflow context on multiple calls
|
|
669
|
+
if isinstance(self.worker, ChatAgent):
|
|
670
|
+
self.worker.reset_to_original_system_message()
|
|
671
|
+
|
|
672
|
+
# Find workflow memory files matching the pattern
|
|
673
|
+
workflow_files = self._find_workflow_files(pattern, session_id)
|
|
674
|
+
if not workflow_files:
|
|
675
|
+
return False
|
|
676
|
+
|
|
677
|
+
# Load the workflow memory files
|
|
678
|
+
loaded_count = self._load_workflow_files(
|
|
679
|
+
workflow_files, max_files_to_load
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
# Report results
|
|
683
|
+
logger.info(
|
|
684
|
+
f"Successfully loaded {loaded_count} workflow file(s) for "
|
|
685
|
+
f"{self.description}"
|
|
686
|
+
)
|
|
687
|
+
return loaded_count > 0
|
|
688
|
+
|
|
689
|
+
except Exception as e:
|
|
690
|
+
logger.warning(
|
|
691
|
+
f"Error loading workflow memories for {self.description}: "
|
|
692
|
+
f"{e!s}"
|
|
693
|
+
)
|
|
694
|
+
return False
|
|
695
|
+
|
|
696
|
+
def _find_workflow_files(
|
|
697
|
+
self, pattern: Optional[str], session_id: Optional[str] = None
|
|
698
|
+
) -> List[str]:
|
|
699
|
+
r"""Find and return sorted workflow files matching the pattern.
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
pattern (Optional[str]): Custom search pattern for workflow files.
|
|
703
|
+
If None, uses worker description to generate pattern.
|
|
704
|
+
session_id (Optional[str]): Specific session ID to search in.
|
|
705
|
+
If None, searches across all sessions.
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
List[str]: Sorted list of workflow file paths (empty if
|
|
709
|
+
validation fails).
|
|
710
|
+
"""
|
|
711
|
+
# Ensure we have a ChatAgent worker
|
|
712
|
+
if not isinstance(self.worker, ChatAgent):
|
|
713
|
+
logger.warning(
|
|
714
|
+
f"Cannot load workflow: {self.description} worker is not "
|
|
715
|
+
"a ChatAgent"
|
|
716
|
+
)
|
|
717
|
+
return []
|
|
718
|
+
|
|
719
|
+
# generate filename-safe search pattern from worker description
|
|
720
|
+
if pattern is None:
|
|
721
|
+
# sanitize description: spaces to underscores, remove special chars
|
|
722
|
+
clean_desc = self.description.lower().replace(" ", "_")
|
|
723
|
+
clean_desc = re.sub(r'[^a-z0-9_]', '', clean_desc)
|
|
724
|
+
pattern = f"{clean_desc}_workflow*.md"
|
|
725
|
+
|
|
726
|
+
# Get the base workforce_workflows directory
|
|
727
|
+
camel_workdir = os.environ.get("CAMEL_WORKDIR")
|
|
728
|
+
if camel_workdir:
|
|
729
|
+
base_dir = os.path.join(camel_workdir, "workforce_workflows")
|
|
730
|
+
else:
|
|
731
|
+
base_dir = "workforce_workflows"
|
|
732
|
+
|
|
733
|
+
# search for workflow files in specified or all session directories
|
|
734
|
+
if session_id:
|
|
735
|
+
search_path = str(Path(base_dir) / session_id / pattern)
|
|
736
|
+
else:
|
|
737
|
+
# search across all session directories using wildcard pattern
|
|
738
|
+
search_path = str(Path(base_dir) / "*" / pattern)
|
|
739
|
+
workflow_files = glob.glob(search_path)
|
|
740
|
+
|
|
741
|
+
if not workflow_files:
|
|
742
|
+
logger.info(f"No workflow files found for pattern: {pattern}")
|
|
743
|
+
return []
|
|
744
|
+
|
|
745
|
+
# prioritize most recent sessions by session timestamp in
|
|
746
|
+
# directory name
|
|
747
|
+
def extract_session_timestamp(filepath: str) -> str:
|
|
748
|
+
match = re.search(r'session_(\d{8}_\d{6}_\d{6})', filepath)
|
|
749
|
+
return match.group(1) if match else ""
|
|
750
|
+
|
|
751
|
+
workflow_files.sort(key=extract_session_timestamp, reverse=True)
|
|
752
|
+
return workflow_files
|
|
753
|
+
|
|
754
|
+
def _load_workflow_files(
|
|
755
|
+
self, workflow_files: List[str], max_files_to_load: int
|
|
756
|
+
) -> int:
|
|
757
|
+
r"""Load workflow files and return count of successful loads.
|
|
758
|
+
|
|
759
|
+
Args:
|
|
760
|
+
workflow_files (List[str]): List of workflow file paths to load.
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
int: Number of successfully loaded workflow files.
|
|
764
|
+
"""
|
|
765
|
+
loaded_count = 0
|
|
766
|
+
# limit loading to prevent context overflow
|
|
767
|
+
for file_path in workflow_files[:max_files_to_load]:
|
|
768
|
+
try:
|
|
769
|
+
# extract file and session info from full path
|
|
770
|
+
filename = os.path.basename(file_path).replace('.md', '')
|
|
771
|
+
session_dir = os.path.dirname(file_path)
|
|
772
|
+
session_id = os.path.basename(session_dir)
|
|
773
|
+
|
|
774
|
+
# create context utility for the specific session
|
|
775
|
+
# where file exists
|
|
776
|
+
temp_utility = ContextUtility.get_workforce_shared(session_id)
|
|
777
|
+
|
|
778
|
+
status = temp_utility.load_markdown_context_to_memory(
|
|
779
|
+
self.worker, filename
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
if "Context appended" in status:
|
|
783
|
+
loaded_count += 1
|
|
784
|
+
logger.info(f"Loaded workflow: {filename}")
|
|
785
|
+
else:
|
|
786
|
+
logger.warning(
|
|
787
|
+
f"Failed to load workflow {filename}: {status}"
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
except Exception as e:
|
|
791
|
+
logger.warning(
|
|
792
|
+
f"Failed to load workflow file {file_path}: {e!s}"
|
|
793
|
+
)
|
|
794
|
+
continue
|
|
795
|
+
|
|
796
|
+
return loaded_count
|
|
797
|
+
|
|
798
|
+
def _validate_workflow_save_requirements(self) -> Optional[Dict[str, Any]]:
|
|
799
|
+
r"""Validate requirements for workflow saving.
|
|
800
|
+
|
|
801
|
+
Returns:
|
|
802
|
+
Optional[Dict[str, Any]]: Error result dict if validation fails,
|
|
803
|
+
None if validation passes.
|
|
804
|
+
"""
|
|
805
|
+
if not isinstance(self.worker, ChatAgent):
|
|
806
|
+
return {
|
|
807
|
+
"status": "error",
|
|
808
|
+
"summary": "",
|
|
809
|
+
"file_path": None,
|
|
810
|
+
"worker_description": self.description,
|
|
811
|
+
"message": (
|
|
812
|
+
"Worker must be a ChatAgent instance to save workflow "
|
|
813
|
+
"memories"
|
|
814
|
+
),
|
|
815
|
+
}
|
|
816
|
+
return None
|
|
817
|
+
|
|
818
|
+
def _generate_workflow_filename(self) -> str:
|
|
819
|
+
r"""Generate a filename for the workflow based on worker description.
|
|
820
|
+
|
|
821
|
+
Returns:
|
|
822
|
+
str: Sanitized filename without timestamp (session already has
|
|
823
|
+
timestamp).
|
|
824
|
+
"""
|
|
825
|
+
clean_desc = self.description.lower().replace(" ", "_")
|
|
826
|
+
clean_desc = re.sub(r'[^a-z0-9_]', '', clean_desc)
|
|
827
|
+
return f"{clean_desc}_workflow"
|
|
828
|
+
|
|
829
|
+
def _prepare_workflow_prompt(self) -> str:
|
|
830
|
+
r"""Prepare the structured prompt for workflow summarization.
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
str: Structured prompt for workflow summary.
|
|
834
|
+
"""
|
|
835
|
+
workflow_prompt = WorkflowSummary.get_instruction_prompt()
|
|
836
|
+
return StructuredOutputHandler.generate_structured_prompt(
|
|
837
|
+
base_prompt=workflow_prompt, schema=WorkflowSummary
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
def _select_agent_for_summarization(
|
|
841
|
+
self, context_util: ContextUtility
|
|
842
|
+
) -> ChatAgent:
|
|
843
|
+
r"""Select the best agent for workflow summarization.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
context_util: Context utility to set on selected agent.
|
|
847
|
+
|
|
848
|
+
Returns:
|
|
849
|
+
ChatAgent: Agent to use for summarization.
|
|
850
|
+
"""
|
|
851
|
+
agent_to_summarize = self.worker
|
|
852
|
+
|
|
853
|
+
if self._conversation_accumulator is not None:
|
|
854
|
+
accumulator_messages, _ = (
|
|
855
|
+
self._conversation_accumulator.memory.get_context()
|
|
856
|
+
)
|
|
857
|
+
if accumulator_messages:
|
|
858
|
+
self._conversation_accumulator.set_context_utility(
|
|
859
|
+
context_util
|
|
860
|
+
)
|
|
861
|
+
agent_to_summarize = self._conversation_accumulator
|
|
862
|
+
logger.info(
|
|
863
|
+
f"Using conversation accumulator with "
|
|
864
|
+
f"{len(accumulator_messages)} messages for workflow "
|
|
865
|
+
f"summary"
|
|
866
|
+
)
|
|
867
|
+
else:
|
|
868
|
+
logger.info(
|
|
869
|
+
"Using original worker for workflow summary (no "
|
|
870
|
+
"accumulated conversations)"
|
|
871
|
+
)
|
|
872
|
+
else:
|
|
873
|
+
logger.info(
|
|
874
|
+
"Using original worker for workflow summary (no accumulator)"
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
return agent_to_summarize
|