camel-ai 0.2.78__py3-none-any.whl → 0.2.79a1__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/_utils.py +38 -0
- camel/agents/chat_agent.py +1112 -287
- camel/datasets/base_generator.py +39 -10
- camel/environments/single_step.py +28 -3
- camel/memories/__init__.py +1 -2
- camel/memories/agent_memories.py +34 -0
- camel/memories/base.py +26 -0
- camel/memories/blocks/chat_history_block.py +117 -17
- camel/memories/context_creators/score_based.py +25 -384
- camel/messages/base.py +26 -0
- camel/models/aws_bedrock_model.py +1 -17
- camel/models/azure_openai_model.py +113 -67
- camel/models/model_factory.py +17 -1
- camel/models/moonshot_model.py +102 -5
- camel/models/openai_compatible_model.py +62 -32
- camel/models/openai_model.py +61 -35
- camel/models/samba_model.py +34 -15
- camel/models/sglang_model.py +41 -11
- camel/societies/workforce/__init__.py +2 -0
- camel/societies/workforce/events.py +122 -0
- camel/societies/workforce/role_playing_worker.py +15 -11
- camel/societies/workforce/single_agent_worker.py +143 -291
- camel/societies/workforce/utils.py +2 -1
- camel/societies/workforce/workflow_memory_manager.py +772 -0
- camel/societies/workforce/workforce.py +513 -188
- camel/societies/workforce/workforce_callback.py +74 -0
- camel/societies/workforce/workforce_logger.py +144 -140
- camel/societies/workforce/workforce_metrics.py +33 -0
- camel/storages/vectordb_storages/oceanbase.py +5 -4
- camel/toolkits/file_toolkit.py +166 -0
- camel/toolkits/message_integration.py +15 -13
- camel/toolkits/terminal_toolkit/terminal_toolkit.py +112 -79
- camel/types/enums.py +1 -0
- camel/utils/context_utils.py +201 -2
- {camel_ai-0.2.78.dist-info → camel_ai-0.2.79a1.dist-info}/METADATA +14 -13
- {camel_ai-0.2.78.dist-info → camel_ai-0.2.79a1.dist-info}/RECORD +39 -35
- {camel_ai-0.2.78.dist-info → camel_ai-0.2.79a1.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.78.dist-info → camel_ai-0.2.79a1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
|
+
|
|
15
|
+
import glob
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
from camel.agents import ChatAgent
|
|
23
|
+
from camel.logger import get_logger
|
|
24
|
+
from camel.societies.workforce.structured_output_handler import (
|
|
25
|
+
StructuredOutputHandler,
|
|
26
|
+
)
|
|
27
|
+
from camel.types import OpenAIBackendRole
|
|
28
|
+
from camel.utils.context_utils import ContextUtility, WorkflowSummary
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WorkflowSelectionMethod(Enum):
|
|
34
|
+
r"""Enum representing the method used to select workflows.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
AGENT_SELECTED: Agent-based intelligent selection using metadata.
|
|
38
|
+
ROLE_NAME_MATCH: Pattern matching by role_name.
|
|
39
|
+
MOST_RECENT: Fallback to most recent workflows.
|
|
40
|
+
ALL_AVAILABLE: Returned all workflows (fewer than max requested).
|
|
41
|
+
NONE: No workflows available.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
AGENT_SELECTED = "agent_selected"
|
|
45
|
+
ROLE_NAME_MATCH = "role_name_match"
|
|
46
|
+
MOST_RECENT = "most_recent"
|
|
47
|
+
ALL_AVAILABLE = "all_available"
|
|
48
|
+
NONE = "none"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class WorkflowMemoryManager:
|
|
52
|
+
r"""Manages workflow memory operations for workforce workers.
|
|
53
|
+
|
|
54
|
+
This class encapsulates all workflow memory functionality including
|
|
55
|
+
intelligent loading, saving, and selection of workflows. It separates
|
|
56
|
+
workflow management concerns from the core worker task processing logic.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
worker (ChatAgent): The worker agent that will use workflows.
|
|
60
|
+
description (str): Description of the worker's role.
|
|
61
|
+
context_utility (Optional[ContextUtility]): Shared context utility
|
|
62
|
+
for workflow operations. If None, creates a new instance.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
worker: ChatAgent,
|
|
68
|
+
description: str,
|
|
69
|
+
context_utility: Optional[ContextUtility] = None,
|
|
70
|
+
):
|
|
71
|
+
# validate worker type at initialization
|
|
72
|
+
if not isinstance(worker, ChatAgent):
|
|
73
|
+
raise TypeError(
|
|
74
|
+
f"Worker must be a ChatAgent instance, "
|
|
75
|
+
f"got {type(worker).__name__}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
self.worker = worker
|
|
79
|
+
self.description = description
|
|
80
|
+
self._context_utility = context_utility
|
|
81
|
+
|
|
82
|
+
def _get_context_utility(self) -> ContextUtility:
|
|
83
|
+
r"""Get context utility with lazy initialization."""
|
|
84
|
+
if self._context_utility is None:
|
|
85
|
+
self._context_utility = ContextUtility.get_workforce_shared()
|
|
86
|
+
return self._context_utility
|
|
87
|
+
|
|
88
|
+
def load_workflows(
|
|
89
|
+
self,
|
|
90
|
+
pattern: Optional[str] = None,
|
|
91
|
+
max_files_to_load: int = 3,
|
|
92
|
+
session_id: Optional[str] = None,
|
|
93
|
+
use_smart_selection: bool = True,
|
|
94
|
+
) -> bool:
|
|
95
|
+
r"""Load workflow memories using intelligent agent-based selection.
|
|
96
|
+
|
|
97
|
+
This method uses the worker agent to intelligently select the most
|
|
98
|
+
relevant workflows based on workflow information (title, description,
|
|
99
|
+
tags) rather than simple filename pattern matching.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
pattern (Optional[str]): Legacy parameter for backward
|
|
103
|
+
compatibility. When use_smart_selection=False, uses this
|
|
104
|
+
pattern for file matching. Ignored when smart selection
|
|
105
|
+
is enabled.
|
|
106
|
+
max_files_to_load (int): Maximum number of workflow files to load.
|
|
107
|
+
(default: :obj:`3`)
|
|
108
|
+
session_id (Optional[str]): Specific workforce session ID to load
|
|
109
|
+
from. If None, searches across all sessions.
|
|
110
|
+
(default: :obj:`None`)
|
|
111
|
+
use_smart_selection (bool): Whether to use agent-based
|
|
112
|
+
intelligent workflow selection. When True, uses workflow
|
|
113
|
+
information and LLM to select most relevant workflows. When
|
|
114
|
+
False, falls back to pattern matching. (default: :obj:`True`)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
bool: True if workflow memories were successfully loaded, False
|
|
118
|
+
otherwise.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
# reset system message to original state before loading
|
|
122
|
+
# this prevents duplicate workflow context on multiple calls
|
|
123
|
+
self.worker.reset_to_original_system_message()
|
|
124
|
+
|
|
125
|
+
# determine which selection method to use
|
|
126
|
+
if use_smart_selection:
|
|
127
|
+
# smart selection: use workflow information and agent
|
|
128
|
+
# intelligence
|
|
129
|
+
context_util = self._get_context_utility()
|
|
130
|
+
workflows_metadata = context_util.get_all_workflows_info(
|
|
131
|
+
session_id
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if not workflows_metadata:
|
|
135
|
+
logger.info("No workflow files found")
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
# use agent to select most relevant workflows
|
|
139
|
+
selected_files, selection_method = (
|
|
140
|
+
self._select_relevant_workflows(
|
|
141
|
+
workflows_metadata, max_files_to_load, session_id
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if not selected_files:
|
|
146
|
+
logger.info(
|
|
147
|
+
f"No workflows selected "
|
|
148
|
+
f"(method: {selection_method.value})"
|
|
149
|
+
)
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
# log selection method used
|
|
153
|
+
logger.info(
|
|
154
|
+
f"Workflow selection method: {selection_method.value}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# load selected workflows
|
|
158
|
+
loaded_count = self._load_workflow_files(
|
|
159
|
+
selected_files, max_files_to_load
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
else:
|
|
163
|
+
# legacy pattern matching approach
|
|
164
|
+
workflow_files = self._find_workflow_files(pattern, session_id)
|
|
165
|
+
if not workflow_files:
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
loaded_count = self._load_workflow_files(
|
|
169
|
+
workflow_files, max_files_to_load
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# report results
|
|
173
|
+
if loaded_count > 0:
|
|
174
|
+
logger.info(
|
|
175
|
+
f"Successfully loaded {loaded_count} workflow file(s) for "
|
|
176
|
+
f"{self.description}"
|
|
177
|
+
)
|
|
178
|
+
return loaded_count > 0
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.warning(
|
|
182
|
+
f"Error loading workflow memories for {self.description}: "
|
|
183
|
+
f"{e!s}"
|
|
184
|
+
)
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
def save_workflow(
|
|
188
|
+
self, conversation_accumulator: Optional[ChatAgent] = None
|
|
189
|
+
) -> Dict[str, Any]:
|
|
190
|
+
r"""Save the worker's current workflow memories using agent
|
|
191
|
+
summarization.
|
|
192
|
+
|
|
193
|
+
This method generates a workflow summary from the worker agent's
|
|
194
|
+
conversation history and saves it to a markdown file.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
conversation_accumulator (Optional[ChatAgent]): Optional
|
|
198
|
+
accumulator agent with collected conversations. If provided,
|
|
199
|
+
uses this instead of the main worker agent.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Dict[str, Any]: Result dictionary with keys:
|
|
203
|
+
- status (str): "success" or "error"
|
|
204
|
+
- summary (str): Generated workflow summary
|
|
205
|
+
- file_path (str): Path to saved file
|
|
206
|
+
- worker_description (str): Worker description used
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
# setup context utility and agent
|
|
210
|
+
context_util = self._get_context_utility()
|
|
211
|
+
self.worker.set_context_utility(context_util)
|
|
212
|
+
|
|
213
|
+
# prepare workflow summarization components
|
|
214
|
+
structured_prompt = self._prepare_workflow_prompt()
|
|
215
|
+
|
|
216
|
+
# check if we should use role_name or let summarize extract
|
|
217
|
+
# task_title
|
|
218
|
+
clean_name = self._get_sanitized_role_name()
|
|
219
|
+
use_role_name_for_filename = clean_name not in {
|
|
220
|
+
'assistant',
|
|
221
|
+
'agent',
|
|
222
|
+
'user',
|
|
223
|
+
'system',
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# if role_name is explicit, use it for filename
|
|
227
|
+
# if role_name is generic, pass none to let summarize use
|
|
228
|
+
# task_title
|
|
229
|
+
filename = (
|
|
230
|
+
self._generate_workflow_filename()
|
|
231
|
+
if use_role_name_for_filename
|
|
232
|
+
else None
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# select agent for summarization
|
|
236
|
+
agent_to_summarize = self.worker
|
|
237
|
+
if conversation_accumulator is not None:
|
|
238
|
+
accumulator_messages, _ = (
|
|
239
|
+
conversation_accumulator.memory.get_context()
|
|
240
|
+
)
|
|
241
|
+
if accumulator_messages:
|
|
242
|
+
conversation_accumulator.set_context_utility(context_util)
|
|
243
|
+
agent_to_summarize = conversation_accumulator
|
|
244
|
+
logger.info(
|
|
245
|
+
f"Using conversation accumulator with "
|
|
246
|
+
f"{len(accumulator_messages)} messages for workflow "
|
|
247
|
+
f"summary"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# generate and save workflow summary
|
|
251
|
+
result = agent_to_summarize.summarize(
|
|
252
|
+
filename=filename,
|
|
253
|
+
summary_prompt=structured_prompt,
|
|
254
|
+
response_format=WorkflowSummary,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# add worker metadata
|
|
258
|
+
result["worker_description"] = self.description
|
|
259
|
+
return result
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
return {
|
|
263
|
+
"status": "error",
|
|
264
|
+
"summary": "",
|
|
265
|
+
"file_path": None,
|
|
266
|
+
"worker_description": self.description,
|
|
267
|
+
"message": f"Failed to save workflow memories: {e!s}",
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async def save_workflow_async(
|
|
271
|
+
self, conversation_accumulator: Optional[ChatAgent] = None
|
|
272
|
+
) -> Dict[str, Any]:
|
|
273
|
+
r"""Asynchronously save the worker's current workflow memories using
|
|
274
|
+
agent summarization.
|
|
275
|
+
|
|
276
|
+
This is the async version of save_workflow() that uses asummarize() for
|
|
277
|
+
non-blocking LLM calls, enabling parallel summarization of multiple
|
|
278
|
+
workers.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
conversation_accumulator (Optional[ChatAgent]): Optional
|
|
282
|
+
accumulator agent with collected conversations. If provided,
|
|
283
|
+
uses this instead of the main worker agent.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Dict[str, Any]: Result dictionary with keys:
|
|
287
|
+
- status (str): "success" or "error"
|
|
288
|
+
- summary (str): Generated workflow summary
|
|
289
|
+
- file_path (str): Path to saved file
|
|
290
|
+
- worker_description (str): Worker description used
|
|
291
|
+
"""
|
|
292
|
+
try:
|
|
293
|
+
# setup context utility and agent
|
|
294
|
+
context_util = self._get_context_utility()
|
|
295
|
+
self.worker.set_context_utility(context_util)
|
|
296
|
+
|
|
297
|
+
# prepare workflow summarization components
|
|
298
|
+
structured_prompt = self._prepare_workflow_prompt()
|
|
299
|
+
|
|
300
|
+
# select agent for summarization
|
|
301
|
+
agent_to_summarize = self.worker
|
|
302
|
+
if conversation_accumulator is not None:
|
|
303
|
+
accumulator_messages, _ = (
|
|
304
|
+
conversation_accumulator.memory.get_context()
|
|
305
|
+
)
|
|
306
|
+
if accumulator_messages:
|
|
307
|
+
conversation_accumulator.set_context_utility(context_util)
|
|
308
|
+
agent_to_summarize = conversation_accumulator
|
|
309
|
+
logger.info(
|
|
310
|
+
f"Using conversation accumulator with "
|
|
311
|
+
f"{len(accumulator_messages)} messages for workflow "
|
|
312
|
+
f"summary"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# check if we should use role_name or let asummarize extract
|
|
316
|
+
# task_title
|
|
317
|
+
clean_name = self._get_sanitized_role_name()
|
|
318
|
+
use_role_name_for_filename = clean_name not in {
|
|
319
|
+
'assistant',
|
|
320
|
+
'agent',
|
|
321
|
+
'user',
|
|
322
|
+
'system',
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# generate and save workflow summary
|
|
326
|
+
# if role_name is explicit, use it for filename
|
|
327
|
+
# if role_name is generic, pass none to let asummarize use
|
|
328
|
+
# task_title
|
|
329
|
+
filename = (
|
|
330
|
+
self._generate_workflow_filename()
|
|
331
|
+
if use_role_name_for_filename
|
|
332
|
+
else None
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# **KEY CHANGE**: Using asummarize() instead of summarize()
|
|
336
|
+
result = await agent_to_summarize.asummarize(
|
|
337
|
+
filename=filename,
|
|
338
|
+
summary_prompt=structured_prompt,
|
|
339
|
+
response_format=WorkflowSummary,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# add worker metadata
|
|
343
|
+
result["worker_description"] = self.description
|
|
344
|
+
return result
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
return {
|
|
348
|
+
"status": "error",
|
|
349
|
+
"summary": "",
|
|
350
|
+
"file_path": None,
|
|
351
|
+
"worker_description": self.description,
|
|
352
|
+
"message": f"Failed to save workflow memories: {e!s}",
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
def _select_relevant_workflows(
|
|
356
|
+
self,
|
|
357
|
+
workflows_metadata: List[Dict[str, Any]],
|
|
358
|
+
max_files: int,
|
|
359
|
+
session_id: Optional[str] = None,
|
|
360
|
+
) -> tuple[List[str], WorkflowSelectionMethod]:
|
|
361
|
+
r"""Use worker agent to select most relevant workflows.
|
|
362
|
+
|
|
363
|
+
This method creates a prompt with all available workflow information
|
|
364
|
+
and uses the worker agent to intelligently select the most relevant
|
|
365
|
+
workflows based on the worker's role and description.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
workflows_metadata (List[Dict[str, Any]]): List of workflow
|
|
369
|
+
information dicts (contains title, description, tags,
|
|
370
|
+
file_path).
|
|
371
|
+
max_files (int): Maximum number of workflows to select.
|
|
372
|
+
session_id (Optional[str]): Specific workforce session ID to
|
|
373
|
+
search in for fallback pattern matching. If None, searches
|
|
374
|
+
across all sessions. (default: :obj:`None`)
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
tuple[List[str], WorkflowSelectionMethod]: Tuple of (selected
|
|
378
|
+
workflow file paths, selection method used).
|
|
379
|
+
"""
|
|
380
|
+
if not workflows_metadata:
|
|
381
|
+
return [], WorkflowSelectionMethod.NONE
|
|
382
|
+
|
|
383
|
+
# format workflows for selection
|
|
384
|
+
workflows_str = self._format_workflows_for_selection(
|
|
385
|
+
workflows_metadata
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# create selection prompt
|
|
389
|
+
selection_prompt = (
|
|
390
|
+
f"You are a {self.description}. "
|
|
391
|
+
f"Review the following {len(workflows_metadata)} available "
|
|
392
|
+
f"workflow memories and select the {max_files} most relevant "
|
|
393
|
+
f"ones for your current role. Consider:\n"
|
|
394
|
+
f"1. Task similarity to your role\n"
|
|
395
|
+
f"2. Domain relevance\n"
|
|
396
|
+
f"3. Tool and capability overlap\n\n"
|
|
397
|
+
f"Available workflows:\n{workflows_str}\n\n"
|
|
398
|
+
f"Respond with ONLY the workflow numbers you selected "
|
|
399
|
+
f"(e.g., '1, 3, 5'), separated by commas. "
|
|
400
|
+
f"Select exactly {max_files} workflows."
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
# use worker agent for selection
|
|
405
|
+
from camel.messages import BaseMessage
|
|
406
|
+
|
|
407
|
+
selection_msg = BaseMessage.make_user_message(
|
|
408
|
+
role_name="user", content=selection_prompt
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
response = self.worker.step(selection_msg)
|
|
412
|
+
|
|
413
|
+
# parse response to extract workflow numbers
|
|
414
|
+
numbers_str = response.msgs[0].content
|
|
415
|
+
numbers = re.findall(r'\d+', numbers_str)
|
|
416
|
+
selected_indices = [int(n) - 1 for n in numbers[:max_files]]
|
|
417
|
+
|
|
418
|
+
# validate indices and get file paths
|
|
419
|
+
selected_paths = []
|
|
420
|
+
for idx in selected_indices:
|
|
421
|
+
if 0 <= idx < len(workflows_metadata):
|
|
422
|
+
selected_paths.append(workflows_metadata[idx]['file_path'])
|
|
423
|
+
else:
|
|
424
|
+
logger.warning(
|
|
425
|
+
f"Agent selected invalid workflow index {idx + 1}, "
|
|
426
|
+
f"only {len(workflows_metadata)} workflows available"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
if selected_paths:
|
|
430
|
+
logger.info(
|
|
431
|
+
f"Agent selected {len(selected_paths)} workflow(s) for "
|
|
432
|
+
f"{self.description}"
|
|
433
|
+
)
|
|
434
|
+
return selected_paths, WorkflowSelectionMethod.AGENT_SELECTED
|
|
435
|
+
|
|
436
|
+
# agent returned empty results
|
|
437
|
+
logger.warning(
|
|
438
|
+
"Agent selection returned no valid workflows, "
|
|
439
|
+
"falling back to role-based pattern matching"
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
except Exception as e:
|
|
443
|
+
logger.warning(
|
|
444
|
+
f"Error during agent selection: {e!s}. "
|
|
445
|
+
f"Falling back to role-based pattern matching"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
finally:
|
|
449
|
+
# clean up selection conversation from memory to prevent
|
|
450
|
+
# pollution. this runs whether selection succeeded, failed,
|
|
451
|
+
# or raised exception
|
|
452
|
+
self.worker.memory.clear()
|
|
453
|
+
if self.worker._system_message is not None:
|
|
454
|
+
self.worker.update_memory(
|
|
455
|
+
self.worker._system_message, OpenAIBackendRole.SYSTEM
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# fallback: try pattern matching by role_name
|
|
459
|
+
pattern_matched_files = self._find_workflow_files(
|
|
460
|
+
pattern=None, session_id=session_id
|
|
461
|
+
)
|
|
462
|
+
if pattern_matched_files:
|
|
463
|
+
return (
|
|
464
|
+
pattern_matched_files[:max_files],
|
|
465
|
+
WorkflowSelectionMethod.ROLE_NAME_MATCH,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# last resort: return most recent workflows
|
|
469
|
+
logger.info(
|
|
470
|
+
"No role-matched workflows found, using most recent workflows"
|
|
471
|
+
)
|
|
472
|
+
return (
|
|
473
|
+
[wf['file_path'] for wf in workflows_metadata[:max_files]],
|
|
474
|
+
WorkflowSelectionMethod.MOST_RECENT,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
def _format_workflows_for_selection(
|
|
478
|
+
self, workflows_metadata: List[Dict[str, Any]]
|
|
479
|
+
) -> str:
|
|
480
|
+
r"""Format workflow information into a readable prompt for selection.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
workflows_metadata (List[Dict[str, Any]]): List of workflow
|
|
484
|
+
information dicts (contains title, description, tags,
|
|
485
|
+
file_path).
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
str: Formatted string presenting workflows for LLM selection.
|
|
489
|
+
"""
|
|
490
|
+
if not workflows_metadata:
|
|
491
|
+
return "No workflows available."
|
|
492
|
+
|
|
493
|
+
formatted_lines = []
|
|
494
|
+
for i, workflow in enumerate(workflows_metadata, 1):
|
|
495
|
+
formatted_lines.append(f"\nWorkflow {i}:")
|
|
496
|
+
formatted_lines.append(f"- Title: {workflow.get('title', 'N/A')}")
|
|
497
|
+
formatted_lines.append(
|
|
498
|
+
f"- Description: {workflow.get('description', 'N/A')}"
|
|
499
|
+
)
|
|
500
|
+
tags = workflow.get('tags', [])
|
|
501
|
+
tags_str = ', '.join(tags) if tags else 'No tags'
|
|
502
|
+
formatted_lines.append(f"- Tags: {tags_str}")
|
|
503
|
+
formatted_lines.append(
|
|
504
|
+
f"- File: {workflow.get('file_path', 'N/A')}"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
return '\n'.join(formatted_lines)
|
|
508
|
+
|
|
509
|
+
def _find_workflow_files(
|
|
510
|
+
self, pattern: Optional[str], session_id: Optional[str] = None
|
|
511
|
+
) -> List[str]:
|
|
512
|
+
r"""Find and return sorted workflow files matching the pattern.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
pattern (Optional[str]): Custom search pattern for workflow files.
|
|
516
|
+
If None, uses worker role_name to generate pattern.
|
|
517
|
+
session_id (Optional[str]): Specific session ID to search in.
|
|
518
|
+
If None, searches across all sessions.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
List[str]: Sorted list of workflow file paths (empty if
|
|
522
|
+
validation fails).
|
|
523
|
+
"""
|
|
524
|
+
# generate filename-safe search pattern from worker role name
|
|
525
|
+
if pattern is None:
|
|
526
|
+
# get sanitized role name
|
|
527
|
+
clean_name = self._get_sanitized_role_name()
|
|
528
|
+
|
|
529
|
+
# check if role_name is generic
|
|
530
|
+
generic_names = {'assistant', 'agent', 'user', 'system'}
|
|
531
|
+
if clean_name in generic_names:
|
|
532
|
+
# for generic role names, search for all workflow files
|
|
533
|
+
# since filename is based on task_title
|
|
534
|
+
pattern = "*_workflow*.md"
|
|
535
|
+
else:
|
|
536
|
+
# for explicit role names, search for role-specific files
|
|
537
|
+
pattern = f"{clean_name}_workflow*.md"
|
|
538
|
+
|
|
539
|
+
# get the base workforce_workflows directory
|
|
540
|
+
camel_workdir = os.environ.get("CAMEL_WORKDIR")
|
|
541
|
+
if camel_workdir:
|
|
542
|
+
base_dir = os.path.join(camel_workdir, "workforce_workflows")
|
|
543
|
+
else:
|
|
544
|
+
base_dir = "workforce_workflows"
|
|
545
|
+
|
|
546
|
+
# search for workflow files in specified or all session directories
|
|
547
|
+
if session_id:
|
|
548
|
+
search_path = str(Path(base_dir) / session_id / pattern)
|
|
549
|
+
else:
|
|
550
|
+
# search across all session directories using wildcard pattern
|
|
551
|
+
search_path = str(Path(base_dir) / "*" / pattern)
|
|
552
|
+
workflow_files = glob.glob(search_path)
|
|
553
|
+
|
|
554
|
+
if not workflow_files:
|
|
555
|
+
logger.info(f"No workflow files found for pattern: {pattern}")
|
|
556
|
+
return []
|
|
557
|
+
|
|
558
|
+
# prioritize most recent sessions by session timestamp in
|
|
559
|
+
# directory name
|
|
560
|
+
def extract_session_timestamp(filepath: str) -> str:
|
|
561
|
+
match = re.search(r'session_(\d{8}_\d{6}_\d{6})', filepath)
|
|
562
|
+
return match.group(1) if match else ""
|
|
563
|
+
|
|
564
|
+
workflow_files.sort(key=extract_session_timestamp, reverse=True)
|
|
565
|
+
return workflow_files
|
|
566
|
+
|
|
567
|
+
def _collect_workflow_contents(
|
|
568
|
+
self, workflow_files: List[str]
|
|
569
|
+
) -> List[Dict[str, str]]:
|
|
570
|
+
r"""Collect and load workflow file contents.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
workflow_files (List[str]): List of workflow file paths to load.
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
List[Dict[str, str]]: List of dicts with 'filename' and
|
|
577
|
+
'content' keys.
|
|
578
|
+
"""
|
|
579
|
+
workflows_to_load = []
|
|
580
|
+
for file_path in workflow_files:
|
|
581
|
+
try:
|
|
582
|
+
# extract file and session info from full path
|
|
583
|
+
filename = os.path.basename(file_path).replace('.md', '')
|
|
584
|
+
session_dir = os.path.dirname(file_path)
|
|
585
|
+
session_id = os.path.basename(session_dir)
|
|
586
|
+
|
|
587
|
+
# create context utility for the specific session
|
|
588
|
+
temp_utility = ContextUtility.get_workforce_shared(session_id)
|
|
589
|
+
|
|
590
|
+
# load the workflow content
|
|
591
|
+
content = temp_utility.load_markdown_file(filename)
|
|
592
|
+
|
|
593
|
+
if content and content.strip():
|
|
594
|
+
# filter out metadata section
|
|
595
|
+
content = temp_utility._filter_metadata_from_content(
|
|
596
|
+
content
|
|
597
|
+
)
|
|
598
|
+
workflows_to_load.append(
|
|
599
|
+
{'filename': filename, 'content': content}
|
|
600
|
+
)
|
|
601
|
+
logger.info(f"Loaded workflow content: {filename}")
|
|
602
|
+
else:
|
|
603
|
+
logger.warning(
|
|
604
|
+
f"Workflow file empty or not found: {filename}"
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
except Exception as e:
|
|
608
|
+
logger.warning(
|
|
609
|
+
f"Failed to load workflow file {file_path}: {e!s}"
|
|
610
|
+
)
|
|
611
|
+
continue
|
|
612
|
+
|
|
613
|
+
return workflows_to_load
|
|
614
|
+
|
|
615
|
+
def _format_workflows_for_context(
|
|
616
|
+
self, workflows_to_load: List[Dict[str, str]]
|
|
617
|
+
) -> str:
|
|
618
|
+
r"""Format workflows into a context string for the agent.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
workflows_to_load (List[Dict[str, str]]): List of workflow
|
|
622
|
+
dicts with 'filename' and 'content' keys.
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
str: Formatted workflow context string with header and all
|
|
626
|
+
workflows.
|
|
627
|
+
"""
|
|
628
|
+
# create single header for all workflows
|
|
629
|
+
if len(workflows_to_load) == 1:
|
|
630
|
+
prefix_prompt = (
|
|
631
|
+
"The following is the context from a previous "
|
|
632
|
+
"session or workflow which might be useful for "
|
|
633
|
+
"the current task. This information might help you "
|
|
634
|
+
"understand the background, choose which tools to use, "
|
|
635
|
+
"and plan your next steps."
|
|
636
|
+
)
|
|
637
|
+
else:
|
|
638
|
+
prefix_prompt = (
|
|
639
|
+
f"The following are {len(workflows_to_load)} previous "
|
|
640
|
+
"workflows which might be useful for "
|
|
641
|
+
"the current task. These workflows provide context about "
|
|
642
|
+
"similar tasks, tools used, and approaches taken. "
|
|
643
|
+
"Review them to understand patterns and make informed "
|
|
644
|
+
"decisions for your current task."
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
# combine all workflows into single content block
|
|
648
|
+
combined_content = f"\n\n--- Previous Workflows ---\n{prefix_prompt}\n"
|
|
649
|
+
|
|
650
|
+
for i, workflow_data in enumerate(workflows_to_load, 1):
|
|
651
|
+
combined_content += (
|
|
652
|
+
f"\n\n{'=' * 60}\n"
|
|
653
|
+
f"Workflow {i}: {workflow_data['filename']}\n"
|
|
654
|
+
f"{'=' * 60}\n\n"
|
|
655
|
+
f"{workflow_data['content']}"
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
return combined_content
|
|
659
|
+
|
|
660
|
+
def _add_workflows_to_system_message(self, workflow_context: str) -> bool:
|
|
661
|
+
r"""Add workflow context to agent's system message.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
workflow_context (str): The formatted workflow context to add.
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
bool: True if successful, False otherwise.
|
|
668
|
+
"""
|
|
669
|
+
# check if agent has a system message
|
|
670
|
+
if self.worker._original_system_message is None:
|
|
671
|
+
logger.error(
|
|
672
|
+
f"Agent {self.worker.agent_id} has no system message. "
|
|
673
|
+
"Cannot append workflow memories."
|
|
674
|
+
)
|
|
675
|
+
return False
|
|
676
|
+
|
|
677
|
+
# update the current system message
|
|
678
|
+
current_system_message = self.worker._system_message
|
|
679
|
+
if current_system_message is not None:
|
|
680
|
+
new_sys_content = current_system_message.content + workflow_context
|
|
681
|
+
self.worker._system_message = (
|
|
682
|
+
current_system_message.create_new_instance(new_sys_content)
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# replace the system message in memory
|
|
686
|
+
self.worker.memory.clear()
|
|
687
|
+
self.worker.update_memory(
|
|
688
|
+
self.worker._system_message, OpenAIBackendRole.SYSTEM
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
return True
|
|
692
|
+
|
|
693
|
+
def _load_workflow_files(
|
|
694
|
+
self, workflow_files: List[str], max_workflows: int
|
|
695
|
+
) -> int:
|
|
696
|
+
r"""Load workflow files and return count of successful loads.
|
|
697
|
+
|
|
698
|
+
Loads all workflows together with a single header to avoid repetition.
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
workflow_files (List[str]): List of workflow file paths to load.
|
|
702
|
+
max_workflows (int): Maximum number of workflows to load.
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
int: Number of successfully loaded workflow files.
|
|
706
|
+
"""
|
|
707
|
+
if not workflow_files:
|
|
708
|
+
return 0
|
|
709
|
+
|
|
710
|
+
# collect workflow contents from files
|
|
711
|
+
workflows_to_load = self._collect_workflow_contents(
|
|
712
|
+
workflow_files[:max_workflows]
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
if not workflows_to_load:
|
|
716
|
+
return 0
|
|
717
|
+
|
|
718
|
+
# format workflows into context string
|
|
719
|
+
try:
|
|
720
|
+
workflow_context = self._format_workflows_for_context(
|
|
721
|
+
workflows_to_load
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
# add workflow context to agent's system message
|
|
725
|
+
if not self._add_workflows_to_system_message(workflow_context):
|
|
726
|
+
return 0
|
|
727
|
+
|
|
728
|
+
char_count = len(workflow_context)
|
|
729
|
+
logger.info(
|
|
730
|
+
f"Appended {len(workflows_to_load)} workflow(s) to agent "
|
|
731
|
+
f"{self.worker.agent_id} ({char_count} characters)"
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
return len(workflows_to_load)
|
|
735
|
+
|
|
736
|
+
except Exception as e:
|
|
737
|
+
logger.error(
|
|
738
|
+
f"Failed to append workflows to system message: {e!s}"
|
|
739
|
+
)
|
|
740
|
+
return 0
|
|
741
|
+
|
|
742
|
+
def _get_sanitized_role_name(self) -> str:
|
|
743
|
+
r"""Get the sanitized role name for the worker.
|
|
744
|
+
|
|
745
|
+
Returns:
|
|
746
|
+
str: Sanitized role name suitable for use in filenames.
|
|
747
|
+
"""
|
|
748
|
+
role_name = getattr(self.worker, 'role_name', 'assistant')
|
|
749
|
+
return ContextUtility.sanitize_workflow_filename(role_name)
|
|
750
|
+
|
|
751
|
+
def _generate_workflow_filename(self) -> str:
|
|
752
|
+
r"""Generate a filename for the workflow based on worker role name.
|
|
753
|
+
|
|
754
|
+
Uses the worker's explicit role_name when available.
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
str: Sanitized filename without timestamp and without .md
|
|
758
|
+
extension. Format: {role_name}_workflow
|
|
759
|
+
"""
|
|
760
|
+
clean_name = self._get_sanitized_role_name()
|
|
761
|
+
return f"{clean_name}_workflow"
|
|
762
|
+
|
|
763
|
+
def _prepare_workflow_prompt(self) -> str:
|
|
764
|
+
r"""Prepare the structured prompt for workflow summarization.
|
|
765
|
+
|
|
766
|
+
Returns:
|
|
767
|
+
str: Structured prompt for workflow summary.
|
|
768
|
+
"""
|
|
769
|
+
workflow_prompt = WorkflowSummary.get_instruction_prompt()
|
|
770
|
+
return StructuredOutputHandler.generate_structured_prompt(
|
|
771
|
+
base_prompt=workflow_prompt, schema=WorkflowSummary
|
|
772
|
+
)
|