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.

Files changed (97) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +1001 -205
  3. camel/agents/mcp_agent.py +30 -27
  4. camel/configs/__init__.py +6 -0
  5. camel/configs/amd_config.py +70 -0
  6. camel/configs/cometapi_config.py +104 -0
  7. camel/data_collectors/alpaca_collector.py +15 -6
  8. camel/environments/tic_tac_toe.py +1 -1
  9. camel/interpreters/__init__.py +2 -0
  10. camel/interpreters/docker/Dockerfile +3 -12
  11. camel/interpreters/microsandbox_interpreter.py +395 -0
  12. camel/loaders/__init__.py +11 -2
  13. camel/loaders/chunkr_reader.py +9 -0
  14. camel/memories/__init__.py +2 -1
  15. camel/memories/agent_memories.py +3 -1
  16. camel/memories/blocks/chat_history_block.py +21 -3
  17. camel/memories/records.py +88 -8
  18. camel/messages/base.py +127 -34
  19. camel/models/__init__.py +4 -0
  20. camel/models/amd_model.py +101 -0
  21. camel/models/azure_openai_model.py +0 -6
  22. camel/models/base_model.py +30 -0
  23. camel/models/cometapi_model.py +83 -0
  24. camel/models/model_factory.py +4 -0
  25. camel/models/openai_compatible_model.py +0 -6
  26. camel/models/openai_model.py +0 -6
  27. camel/models/zhipuai_model.py +61 -2
  28. camel/parsers/__init__.py +18 -0
  29. camel/parsers/mcp_tool_call_parser.py +176 -0
  30. camel/retrievers/auto_retriever.py +1 -0
  31. camel/runtimes/daytona_runtime.py +11 -12
  32. camel/societies/workforce/prompts.py +131 -50
  33. camel/societies/workforce/single_agent_worker.py +434 -49
  34. camel/societies/workforce/structured_output_handler.py +30 -18
  35. camel/societies/workforce/task_channel.py +43 -0
  36. camel/societies/workforce/utils.py +105 -12
  37. camel/societies/workforce/workforce.py +1322 -311
  38. camel/societies/workforce/workforce_logger.py +24 -5
  39. camel/storages/key_value_storages/json.py +15 -2
  40. camel/storages/object_storages/google_cloud.py +1 -1
  41. camel/storages/vectordb_storages/oceanbase.py +10 -11
  42. camel/storages/vectordb_storages/tidb.py +8 -6
  43. camel/tasks/task.py +4 -3
  44. camel/toolkits/__init__.py +18 -5
  45. camel/toolkits/aci_toolkit.py +45 -0
  46. camel/toolkits/code_execution.py +28 -1
  47. camel/toolkits/context_summarizer_toolkit.py +684 -0
  48. camel/toolkits/dingtalk.py +1135 -0
  49. camel/toolkits/edgeone_pages_mcp_toolkit.py +11 -31
  50. camel/toolkits/{file_write_toolkit.py → file_toolkit.py} +194 -34
  51. camel/toolkits/function_tool.py +6 -1
  52. camel/toolkits/google_drive_mcp_toolkit.py +12 -31
  53. camel/toolkits/hybrid_browser_toolkit/config_loader.py +12 -0
  54. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +79 -2
  55. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +95 -59
  56. camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
  57. camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +5 -612
  58. camel/toolkits/hybrid_browser_toolkit/ts/package.json +0 -1
  59. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +619 -95
  60. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +7 -2
  61. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +115 -219
  62. camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
  63. camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
  64. camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
  65. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +1 -0
  66. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +39 -6
  67. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +405 -131
  68. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +9 -5
  69. camel/toolkits/{openai_image_toolkit.py → image_generation_toolkit.py} +98 -31
  70. camel/toolkits/markitdown_toolkit.py +27 -1
  71. camel/toolkits/mcp_toolkit.py +348 -348
  72. camel/toolkits/message_integration.py +3 -0
  73. camel/toolkits/minimax_mcp_toolkit.py +195 -0
  74. camel/toolkits/note_taking_toolkit.py +18 -8
  75. camel/toolkits/notion_mcp_toolkit.py +16 -26
  76. camel/toolkits/origene_mcp_toolkit.py +8 -49
  77. camel/toolkits/playwright_mcp_toolkit.py +12 -31
  78. camel/toolkits/resend_toolkit.py +168 -0
  79. camel/toolkits/slack_toolkit.py +50 -1
  80. camel/toolkits/terminal_toolkit/__init__.py +18 -0
  81. camel/toolkits/terminal_toolkit/terminal_toolkit.py +924 -0
  82. camel/toolkits/terminal_toolkit/utils.py +532 -0
  83. camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
  84. camel/toolkits/video_analysis_toolkit.py +17 -11
  85. camel/toolkits/wechat_official_toolkit.py +483 -0
  86. camel/types/enums.py +124 -1
  87. camel/types/unified_model_type.py +5 -0
  88. camel/utils/commons.py +17 -0
  89. camel/utils/context_utils.py +804 -0
  90. camel/utils/mcp.py +136 -2
  91. camel/utils/token_counting.py +25 -17
  92. {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/METADATA +158 -59
  93. {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/RECORD +95 -76
  94. camel/loaders/pandas_reader.py +0 -368
  95. camel/toolkits/terminal_toolkit.py +0 -1788
  96. {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/WHEEL +0 -0
  97. {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 typing import Any, List, Optional
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. It supports
39
- auto-scaling based ondemand and intelligent reuse of existing agents.
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, # 3 minutes
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 agents
108
- total_agents = len(self._available_agents) + len(
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
- # Pool exhausted, wait and retry or create temporary agent
117
- if self.auto_scale:
118
- # Create a temporary agent that won't be returned to pool
119
- return self._create_fresh_agent()
120
- else:
121
- # Wait for an agent to become available
122
- while not self._available_agents:
123
- await asyncio.sleep(0.1)
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 = id(agent)
138
+ if agent_id not in self._in_use_agents:
139
+ return
134
140
 
135
- if agent_id in self._in_use_agents:
136
- self._in_use_agents.remove(agent_id)
141
+ self._in_use_agents.discard(agent_id)
137
142
 
138
- # Only return to pool if we're under max size
139
- if len(self._available_agents) < self.max_size:
140
- # Reset agent state before returning to pool
141
- agent.reset()
142
- self._available_agents.append(agent)
143
- self._agent_last_used[agent_id] = time.time()
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
- agent_id = id(agent)
164
- self._agent_last_used.pop(agent_id, None)
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
- print(
386
- f"{Fore.RED}Error processing task {task.id}: "
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
- print(
437
- f"{Fore.RED}Error in worker step execution: Invalid "
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
- print(
457
- f"{Fore.RED}Task {task.id}: Content validation failed - "
458
- f"task marked as failed{Fore.RESET}"
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
- await asyncio.sleep(60) # Cleanup every minute
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
- print(f"Error in pool cleanup: {e}")
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