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.

Files changed (39) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/_utils.py +38 -0
  3. camel/agents/chat_agent.py +1112 -287
  4. camel/datasets/base_generator.py +39 -10
  5. camel/environments/single_step.py +28 -3
  6. camel/memories/__init__.py +1 -2
  7. camel/memories/agent_memories.py +34 -0
  8. camel/memories/base.py +26 -0
  9. camel/memories/blocks/chat_history_block.py +117 -17
  10. camel/memories/context_creators/score_based.py +25 -384
  11. camel/messages/base.py +26 -0
  12. camel/models/aws_bedrock_model.py +1 -17
  13. camel/models/azure_openai_model.py +113 -67
  14. camel/models/model_factory.py +17 -1
  15. camel/models/moonshot_model.py +102 -5
  16. camel/models/openai_compatible_model.py +62 -32
  17. camel/models/openai_model.py +61 -35
  18. camel/models/samba_model.py +34 -15
  19. camel/models/sglang_model.py +41 -11
  20. camel/societies/workforce/__init__.py +2 -0
  21. camel/societies/workforce/events.py +122 -0
  22. camel/societies/workforce/role_playing_worker.py +15 -11
  23. camel/societies/workforce/single_agent_worker.py +143 -291
  24. camel/societies/workforce/utils.py +2 -1
  25. camel/societies/workforce/workflow_memory_manager.py +772 -0
  26. camel/societies/workforce/workforce.py +513 -188
  27. camel/societies/workforce/workforce_callback.py +74 -0
  28. camel/societies/workforce/workforce_logger.py +144 -140
  29. camel/societies/workforce/workforce_metrics.py +33 -0
  30. camel/storages/vectordb_storages/oceanbase.py +5 -4
  31. camel/toolkits/file_toolkit.py +166 -0
  32. camel/toolkits/message_integration.py +15 -13
  33. camel/toolkits/terminal_toolkit/terminal_toolkit.py +112 -79
  34. camel/types/enums.py +1 -0
  35. camel/utils/context_utils.py +201 -2
  36. {camel_ai-0.2.78.dist-info → camel_ai-0.2.79a1.dist-info}/METADATA +14 -13
  37. {camel_ai-0.2.78.dist-info → camel_ai-0.2.79a1.dist-info}/RECORD +39 -35
  38. {camel_ai-0.2.78.dist-info → camel_ai-0.2.79a1.dist-info}/WHEEL +0 -0
  39. {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
+ )