camel-ai 0.2.77__py3-none-any.whl → 0.2.79a0__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.

@@ -80,6 +80,7 @@ class AgentPool:
80
80
  self._in_use_agents: set = set()
81
81
  self._agent_last_used: dict = {}
82
82
  self._lock = asyncio.Lock()
83
+ self._condition = asyncio.Condition(self._lock)
83
84
 
84
85
  # Statistics
85
86
  self._total_borrows = 0
@@ -105,36 +106,31 @@ class AgentPool:
105
106
 
106
107
  async def get_agent(self) -> ChatAgent:
107
108
  r"""Get an agent from the pool, creating one if necessary."""
108
- async with self._lock:
109
+ async with self._condition:
109
110
  self._total_borrows += 1
110
111
 
111
- if self._available_agents:
112
- agent = self._available_agents.popleft()
113
- self._in_use_agents.add(id(agent))
114
- self._pool_hits += 1
115
- return agent
116
-
117
- # Check if we can create a new agent
118
- if len(self._in_use_agents) < self.max_size or self.auto_scale:
119
- agent = self._create_fresh_agent()
120
- self._in_use_agents.add(id(agent))
121
- return agent
122
-
123
- # Wait for available agent
124
- while True:
125
- async with self._lock:
112
+ # Try to get available agent or create new one
113
+ while True:
126
114
  if self._available_agents:
127
115
  agent = self._available_agents.popleft()
128
116
  self._in_use_agents.add(id(agent))
129
117
  self._pool_hits += 1
130
118
  return agent
131
- await asyncio.sleep(0.05)
119
+
120
+ # Check if we can create a new agent
121
+ if len(self._in_use_agents) < self.max_size or self.auto_scale:
122
+ agent = self._create_fresh_agent()
123
+ self._in_use_agents.add(id(agent))
124
+ return agent
125
+
126
+ # Wait for an agent to be returned
127
+ await self._condition.wait()
132
128
 
133
129
  async def return_agent(self, agent: ChatAgent) -> None:
134
130
  r"""Return an agent to the pool."""
135
131
  agent_id = id(agent)
136
132
 
137
- async with self._lock:
133
+ async with self._condition:
138
134
  if agent_id not in self._in_use_agents:
139
135
  return
140
136
 
@@ -145,6 +141,8 @@ class AgentPool:
145
141
  agent.reset()
146
142
  self._agent_last_used[agent_id] = time.time()
147
143
  self._available_agents.append(agent)
144
+ # Notify one waiting coroutine that an agent is available
145
+ self._condition.notify()
148
146
  else:
149
147
  # Remove tracking for agents not returned to pool
150
148
  self._agent_last_used.pop(agent_id, None)
@@ -154,7 +152,7 @@ class AgentPool:
154
152
  if not self.auto_scale:
155
153
  return
156
154
 
157
- async with self._lock:
155
+ async with self._condition:
158
156
  if not self._available_agents:
159
157
  return
160
158
 
@@ -428,6 +426,7 @@ class SingleAgentWorker(Worker):
428
426
  "usage"
429
427
  ) or final_response.info.get("token_usage")
430
428
  else:
429
+ final_response = response
431
430
  usage_info = response.info.get("usage") or response.info.get(
432
431
  "token_usage"
433
432
  )
@@ -562,10 +561,11 @@ class SingleAgentWorker(Worker):
562
561
  while True:
563
562
  try:
564
563
  # Fixed interval cleanup
565
- await asyncio.sleep(self.agent_pool.cleanup_interval)
566
-
567
564
  if self.agent_pool:
565
+ await asyncio.sleep(self.agent_pool.cleanup_interval)
568
566
  await self.agent_pool.cleanup_idle_agents()
567
+ else:
568
+ break
569
569
  except asyncio.CancelledError:
570
570
  break
571
571
  except Exception as e:
@@ -581,9 +581,14 @@ class SingleAgentWorker(Worker):
581
581
  r"""Save the worker's current workflow memories using agent
582
582
  summarization.
583
583
 
584
+ .. deprecated:: 0.2.80
585
+ Use :meth:`save_workflow_memories_async` for async/await support
586
+ and better integration with parallel workflow saving.
587
+
584
588
  This method generates a workflow summary from the worker agent's
585
589
  conversation history and saves it to a markdown file. The filename
586
- is based on the worker's description for easy loading later.
590
+ is based on either the worker's explicit role_name or the generated
591
+ task_title from the summary.
587
592
 
588
593
  Returns:
589
594
  Dict[str, Any]: Result dictionary with keys:
@@ -591,7 +596,19 @@ class SingleAgentWorker(Worker):
591
596
  - summary (str): Generated workflow summary
592
597
  - file_path (str): Path to saved file
593
598
  - worker_description (str): Worker description used
599
+
600
+ See Also:
601
+ :meth:`save_workflow_memories_async`: Async version for better
602
+ performance in parallel workflows.
594
603
  """
604
+ import warnings
605
+
606
+ warnings.warn(
607
+ "save_workflow_memories() is synchronous. Consider using "
608
+ "save_workflow_memories_async() for async/await support.",
609
+ DeprecationWarning,
610
+ stacklevel=2,
611
+ )
595
612
  try:
596
613
  # validate requirements
597
614
  validation_error = self._validate_workflow_save_requirements()
@@ -603,13 +620,31 @@ class SingleAgentWorker(Worker):
603
620
  self.worker.set_context_utility(context_util)
604
621
 
605
622
  # prepare workflow summarization components
606
- filename = self._generate_workflow_filename()
607
623
  structured_prompt = self._prepare_workflow_prompt()
608
624
  agent_to_summarize = self._select_agent_for_summarization(
609
625
  context_util
610
626
  )
611
627
 
628
+ # check if we should use role_name or let summarize extract
629
+ # task_title
630
+ role_name = getattr(self.worker, 'role_name', 'assistant')
631
+ use_role_name_for_filename = role_name.lower() not in {
632
+ 'assistant',
633
+ 'agent',
634
+ 'user',
635
+ 'system',
636
+ }
637
+
612
638
  # generate and save workflow summary
639
+ # if role_name is explicit, use it for filename
640
+ # if role_name is generic, pass none to let summarize use
641
+ # task_title
642
+ filename = (
643
+ self._generate_workflow_filename()
644
+ if use_role_name_for_filename
645
+ else None
646
+ )
647
+
613
648
  result = agent_to_summarize.summarize(
614
649
  filename=filename,
615
650
  summary_prompt=structured_prompt,
@@ -636,6 +671,84 @@ class SingleAgentWorker(Worker):
636
671
  "message": f"Failed to save workflow memories: {e!s}",
637
672
  }
638
673
 
674
+ async def save_workflow_memories_async(self) -> Dict[str, Any]:
675
+ r"""Asynchronously save the worker's current workflow memories using
676
+ agent summarization.
677
+
678
+ This is the async version of save_workflow_memories() that uses
679
+ asummarize() for non-blocking LLM calls, enabling parallel
680
+ summarization of multiple workers.
681
+
682
+ Returns:
683
+ Dict[str, Any]: Result dictionary with keys:
684
+ - status (str): "success" or "error"
685
+ - summary (str): Generated workflow summary
686
+ - file_path (str): Path to saved file
687
+ - worker_description (str): Worker description used
688
+ """
689
+ try:
690
+ # validate requirements
691
+ validation_error = self._validate_workflow_save_requirements()
692
+ if validation_error:
693
+ return validation_error
694
+
695
+ # setup context utility and agent
696
+ context_util = self._get_context_utility()
697
+ self.worker.set_context_utility(context_util)
698
+
699
+ # prepare workflow summarization components
700
+ structured_prompt = self._prepare_workflow_prompt()
701
+ agent_to_summarize = self._select_agent_for_summarization(
702
+ context_util
703
+ )
704
+
705
+ # check if we should use role_name or let summarize extract
706
+ # task_title
707
+ role_name = getattr(self.worker, 'role_name', 'assistant')
708
+ use_role_name_for_filename = role_name.lower() not in {
709
+ 'assistant',
710
+ 'agent',
711
+ 'user',
712
+ 'system',
713
+ }
714
+
715
+ # generate and save workflow summary
716
+ # if role_name is explicit, use it for filename
717
+ # if role_name is generic, pass none to let summarize use
718
+ # task_title
719
+ filename = (
720
+ self._generate_workflow_filename()
721
+ if use_role_name_for_filename
722
+ else None
723
+ )
724
+
725
+ # **KEY CHANGE**: Using asummarize() instead of summarize()
726
+ result = await agent_to_summarize.asummarize(
727
+ filename=filename,
728
+ summary_prompt=structured_prompt,
729
+ response_format=WorkflowSummary,
730
+ )
731
+
732
+ # add worker metadata and cleanup
733
+ result["worker_description"] = self.description
734
+ if self._conversation_accumulator is not None:
735
+ logger.info(
736
+ "Cleaning up conversation accumulator after workflow "
737
+ "summarization"
738
+ )
739
+ self._conversation_accumulator = None
740
+
741
+ return result
742
+
743
+ except Exception as e:
744
+ return {
745
+ "status": "error",
746
+ "summary": "",
747
+ "file_path": None,
748
+ "worker_description": self.description,
749
+ "message": f"Failed to save workflow memories: {e!s}",
750
+ }
751
+
639
752
  def load_workflow_memories(
640
753
  self,
641
754
  pattern: Optional[str] = None,
@@ -716,12 +829,23 @@ class SingleAgentWorker(Worker):
716
829
  )
717
830
  return []
718
831
 
719
- # generate filename-safe search pattern from worker description
832
+ # generate filename-safe search pattern from worker role name
720
833
  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"
834
+ from camel.utils.context_utils import ContextUtility
835
+
836
+ # get role_name (always available, defaults to "assistant")
837
+ role_name = getattr(self.worker, 'role_name', 'assistant')
838
+ clean_name = ContextUtility.sanitize_workflow_filename(role_name)
839
+
840
+ # check if role_name is generic
841
+ generic_names = {'assistant', 'agent', 'user', 'system'}
842
+ if clean_name in generic_names:
843
+ # for generic role names, search for all workflow files
844
+ # since filename is based on task_title
845
+ pattern = "*_workflow*.md"
846
+ else:
847
+ # for explicit role names, search for role-specific files
848
+ pattern = f"{clean_name}_workflow*.md"
725
849
 
726
850
  # Get the base workforce_workflows directory
727
851
  camel_workdir = os.environ.get("CAMEL_WORKDIR")
@@ -816,15 +940,21 @@ class SingleAgentWorker(Worker):
816
940
  return None
817
941
 
818
942
  def _generate_workflow_filename(self) -> str:
819
- r"""Generate a filename for the workflow based on worker description.
943
+ r"""Generate a filename for the workflow based on worker role name.
944
+
945
+ Uses the worker's explicit role_name when available.
820
946
 
821
947
  Returns:
822
- str: Sanitized filename without timestamp (session already has
823
- timestamp).
948
+ str: Sanitized filename without timestamp and without .md
949
+ extension. Format: {role_name}_workflow
824
950
  """
825
- clean_desc = self.description.lower().replace(" ", "_")
826
- clean_desc = re.sub(r'[^a-z0-9_]', '', clean_desc)
827
- return f"{clean_desc}_workflow"
951
+ from camel.utils.context_utils import ContextUtility
952
+
953
+ # get role_name (always available, defaults to "assistant"/"Assistant")
954
+ role_name = getattr(self.worker, 'role_name', 'assistant')
955
+ clean_name = ContextUtility.sanitize_workflow_filename(role_name)
956
+
957
+ return f"{clean_name}_workflow"
828
958
 
829
959
  def _prepare_workflow_prompt(self) -> str:
830
960
  r"""Prepare the structured prompt for workflow summarization.