camel-ai 0.2.71a5__py3-none-any.whl → 0.2.71a6__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.

@@ -47,6 +47,9 @@ from camel.societies.workforce.prompts import (
47
47
  )
48
48
  from camel.societies.workforce.role_playing_worker import RolePlayingWorker
49
49
  from camel.societies.workforce.single_agent_worker import SingleAgentWorker
50
+ from camel.societies.workforce.structured_output_handler import (
51
+ StructuredOutputHandler,
52
+ )
50
53
  from camel.societies.workforce.task_channel import TaskChannel
51
54
  from camel.societies.workforce.utils import (
52
55
  FailureContext,
@@ -168,6 +171,14 @@ class Workforce(BaseNode):
168
171
  SingleAgentWorker instances; RolePlayingWorker and nested
169
172
  Workforce instances do not participate in memory sharing.
170
173
  (default: :obj:`False`)
174
+ use_structured_output_handler (bool, optional): Whether to use the
175
+ structured output handler instead of native structured output.
176
+ When enabled, the workforce will use prompts with structured
177
+ output instructions and regex extraction to parse responses.
178
+ This ensures compatibility with agents that don't reliably
179
+ support native structured output. When disabled, the workforce
180
+ uses the native response_format parameter.
181
+ (default: :obj:`True`)
171
182
 
172
183
  Example:
173
184
  >>> import asyncio
@@ -218,6 +229,7 @@ class Workforce(BaseNode):
218
229
  new_worker_agent: Optional[ChatAgent] = None,
219
230
  graceful_shutdown_timeout: float = 15.0,
220
231
  share_memory: bool = False,
232
+ use_structured_output_handler: bool = True,
221
233
  ) -> None:
222
234
  super().__init__(description)
223
235
  self._child_listening_tasks: Deque[
@@ -227,6 +239,9 @@ class Workforce(BaseNode):
227
239
  self.new_worker_agent = new_worker_agent
228
240
  self.graceful_shutdown_timeout = graceful_shutdown_timeout
229
241
  self.share_memory = share_memory
242
+ self.use_structured_output_handler = use_structured_output_handler
243
+ if self.use_structured_output_handler:
244
+ self.structured_handler = StructuredOutputHandler()
230
245
  self.metrics_logger = WorkforceLogger(workforce_id=self.node_id)
231
246
  self._task: Optional[Task] = None
232
247
  self._pending_tasks: Deque[Task] = deque()
@@ -729,12 +744,55 @@ class Workforce(BaseNode):
729
744
  )
730
745
 
731
746
  try:
732
- # Get decision from task agent
733
- self.task_agent.reset()
734
- response = self.task_agent.step(
735
- analysis_prompt, response_format=RecoveryDecision
736
- )
737
- return response.msg.parsed
747
+ # Check if we should use structured handler
748
+ if self.use_structured_output_handler:
749
+ # Use structured handler
750
+ enhanced_prompt = (
751
+ self.structured_handler.generate_structured_prompt(
752
+ base_prompt=analysis_prompt,
753
+ schema=RecoveryDecision,
754
+ examples=[
755
+ {
756
+ "strategy": "RETRY",
757
+ "reasoning": "Temporary network error, "
758
+ "worth retrying",
759
+ "modified_task_content": None,
760
+ }
761
+ ],
762
+ )
763
+ )
764
+
765
+ self.task_agent.reset()
766
+ response = self.task_agent.step(enhanced_prompt)
767
+
768
+ result = self.structured_handler.parse_structured_response(
769
+ response.msg.content if response.msg else "",
770
+ schema=RecoveryDecision,
771
+ fallback_values={
772
+ "strategy": RecoveryStrategy.RETRY,
773
+ "reasoning": "Defaulting to retry due to parsing "
774
+ "issues",
775
+ "modified_task_content": None,
776
+ },
777
+ )
778
+ # Ensure we return a RecoveryDecision instance
779
+ if isinstance(result, RecoveryDecision):
780
+ return result
781
+ elif isinstance(result, dict):
782
+ return RecoveryDecision(**result)
783
+ else:
784
+ return RecoveryDecision(
785
+ strategy=RecoveryStrategy.RETRY,
786
+ reasoning="Failed to parse recovery decision",
787
+ modified_task_content=None,
788
+ )
789
+ else:
790
+ # Use existing native structured output code
791
+ self.task_agent.reset()
792
+ response = self.task_agent.step(
793
+ analysis_prompt, response_format=RecoveryDecision
794
+ )
795
+ return response.msg.parsed
738
796
 
739
797
  except Exception as e:
740
798
  logger.warning(
@@ -1338,6 +1396,9 @@ class Workforce(BaseNode):
1338
1396
  start_coroutine, self._loop
1339
1397
  )
1340
1398
  self._child_listening_tasks.append(child_task)
1399
+ else:
1400
+ # Close the coroutine to prevent RuntimeWarning
1401
+ start_coroutine.close()
1341
1402
 
1342
1403
  def add_single_agent_worker(
1343
1404
  self,
@@ -1372,6 +1433,7 @@ class Workforce(BaseNode):
1372
1433
  description=description,
1373
1434
  worker=worker,
1374
1435
  pool_max_size=pool_max_size,
1436
+ use_structured_output_handler=self.use_structured_output_handler,
1375
1437
  )
1376
1438
  self._children.append(worker_node)
1377
1439
 
@@ -1450,6 +1512,7 @@ class Workforce(BaseNode):
1450
1512
  user_agent_kwargs=user_agent_kwargs,
1451
1513
  summarize_agent_kwargs=summarize_agent_kwargs,
1452
1514
  chat_turn_limit=chat_turn_limit,
1515
+ use_structured_output_handler=self.use_structured_output_handler,
1453
1516
  )
1454
1517
  self._children.append(worker_node)
1455
1518
 
@@ -1623,26 +1686,73 @@ class Workforce(BaseNode):
1623
1686
  )
1624
1687
  prompt = prompt + f"\n\n{feedback}"
1625
1688
 
1626
- response = self.coordinator_agent.step(
1627
- prompt, response_format=TaskAssignResult
1628
- )
1629
-
1630
- if response.msg is None or response.msg.content is None:
1631
- logger.error(
1632
- "Coordinator agent returned empty response for task assignment"
1689
+ # Check if we should use structured handler
1690
+ if self.use_structured_output_handler:
1691
+ # Use structured handler for prompt-based extraction
1692
+ enhanced_prompt = (
1693
+ self.structured_handler.generate_structured_prompt(
1694
+ base_prompt=prompt,
1695
+ schema=TaskAssignResult,
1696
+ examples=[
1697
+ {
1698
+ "assignments": [
1699
+ {
1700
+ "task_id": "task_1",
1701
+ "assignee_id": "worker_123",
1702
+ "dependencies": [],
1703
+ }
1704
+ ]
1705
+ }
1706
+ ],
1707
+ )
1633
1708
  )
1634
- return TaskAssignResult(assignments=[])
1635
1709
 
1636
- try:
1637
- result_dict = json.loads(response.msg.content, parse_int=str)
1638
- return TaskAssignResult(**result_dict)
1639
- except json.JSONDecodeError as e:
1640
- logger.error(
1641
- f"JSON parsing error in task assignment: Invalid response "
1642
- f"format - {e}. Response content: "
1643
- f"{response.msg.content[:50]}..."
1710
+ # Get response without structured format
1711
+ response = self.coordinator_agent.step(enhanced_prompt)
1712
+
1713
+ if response.msg is None or response.msg.content is None:
1714
+ logger.error(
1715
+ "Coordinator agent returned empty response for "
1716
+ "task assignment"
1717
+ )
1718
+ return TaskAssignResult(assignments=[])
1719
+
1720
+ # Parse with structured handler
1721
+ result = self.structured_handler.parse_structured_response(
1722
+ response.msg.content,
1723
+ schema=TaskAssignResult,
1724
+ fallback_values={"assignments": []},
1725
+ )
1726
+ # Ensure we return a TaskAssignResult instance
1727
+ if isinstance(result, TaskAssignResult):
1728
+ return result
1729
+ elif isinstance(result, dict):
1730
+ return TaskAssignResult(**result)
1731
+ else:
1732
+ return TaskAssignResult(assignments=[])
1733
+ else:
1734
+ # Use existing native structured output code
1735
+ response = self.coordinator_agent.step(
1736
+ prompt, response_format=TaskAssignResult
1644
1737
  )
1645
- return TaskAssignResult(assignments=[])
1738
+
1739
+ if response.msg is None or response.msg.content is None:
1740
+ logger.error(
1741
+ "Coordinator agent returned empty response for "
1742
+ "task assignment"
1743
+ )
1744
+ return TaskAssignResult(assignments=[])
1745
+
1746
+ try:
1747
+ result_dict = json.loads(response.msg.content, parse_int=str)
1748
+ return TaskAssignResult(**result_dict)
1749
+ except json.JSONDecodeError as e:
1750
+ logger.error(
1751
+ f"JSON parsing error in task assignment: Invalid response "
1752
+ f"format - {e}. Response content: "
1753
+ f"{response.msg.content[:50]}..."
1754
+ )
1755
+ return TaskAssignResult(assignments=[])
1646
1756
 
1647
1757
  def _validate_assignments(
1648
1758
  self, assignments: List[TaskAssignment], valid_ids: Set[str]
@@ -1844,6 +1954,10 @@ class Workforce(BaseNode):
1844
1954
  logger.error(
1845
1955
  f"Failed to post task {task.id} to {assignee_id}: {e}"
1846
1956
  )
1957
+ print(
1958
+ f"{Fore.RED}Failed to post task {task.id} to {assignee_id}: "
1959
+ f"{e}{Fore.RESET}"
1960
+ )
1847
1961
 
1848
1962
  async def _post_dependency(self, dependency: Task) -> None:
1849
1963
  await self._channel.post_dependency(dependency, self.node_id)
@@ -1864,35 +1978,92 @@ class Workforce(BaseNode):
1864
1978
  child_nodes_info=self._get_child_nodes_info(),
1865
1979
  additional_info=task.additional_info,
1866
1980
  )
1867
- response = self.coordinator_agent.step(
1868
- prompt, response_format=WorkerConf
1869
- )
1870
- if response.msg is None or response.msg.content is None:
1871
- logger.error(
1872
- "Coordinator agent returned empty response for worker creation"
1873
- )
1874
- # Create a fallback worker configuration
1875
- new_node_conf = WorkerConf(
1876
- description=f"Fallback worker for "
1877
- f"task: {task.content[:50]}...",
1878
- role="General Assistant",
1879
- sys_msg="You are a general assistant that can help "
1880
- "with various tasks.",
1981
+ # Check if we should use structured handler
1982
+ if self.use_structured_output_handler:
1983
+ # Use structured handler
1984
+ enhanced_prompt = (
1985
+ self.structured_handler.generate_structured_prompt(
1986
+ base_prompt=prompt,
1987
+ schema=WorkerConf,
1988
+ examples=[
1989
+ {
1990
+ "description": "Data analysis specialist",
1991
+ "role": "Data Analyst",
1992
+ "sys_msg": "You are an expert data analyst.",
1993
+ }
1994
+ ],
1995
+ )
1881
1996
  )
1997
+
1998
+ response = self.coordinator_agent.step(enhanced_prompt)
1999
+
2000
+ if response.msg is None or response.msg.content is None:
2001
+ logger.error(
2002
+ "Coordinator agent returned empty response for "
2003
+ "worker creation"
2004
+ )
2005
+ new_node_conf = WorkerConf(
2006
+ description=f"Fallback worker for task: "
2007
+ f"{task.content[:50]}...",
2008
+ role="General Assistant",
2009
+ sys_msg="You are a general assistant that can help "
2010
+ "with various tasks.",
2011
+ )
2012
+ else:
2013
+ result = self.structured_handler.parse_structured_response(
2014
+ response.msg.content,
2015
+ schema=WorkerConf,
2016
+ fallback_values={
2017
+ "description": f"Worker for task: "
2018
+ f"{task.content[:50]}...",
2019
+ "role": "Task Specialist",
2020
+ "sys_msg": f"You are a specialist for: {task.content}",
2021
+ },
2022
+ )
2023
+ # Ensure we have a WorkerConf instance
2024
+ if isinstance(result, WorkerConf):
2025
+ new_node_conf = result
2026
+ elif isinstance(result, dict):
2027
+ new_node_conf = WorkerConf(**result)
2028
+ else:
2029
+ new_node_conf = WorkerConf(
2030
+ description=f"Worker for task: {task.content[:50]}...",
2031
+ role="Task Specialist",
2032
+ sys_msg=f"You are a specialist for: {task.content}",
2033
+ )
1882
2034
  else:
1883
- try:
1884
- result_dict = json.loads(response.msg.content)
1885
- new_node_conf = WorkerConf(**result_dict)
1886
- except json.JSONDecodeError as e:
2035
+ # Use existing native structured output code
2036
+ response = self.coordinator_agent.step(
2037
+ prompt, response_format=WorkerConf
2038
+ )
2039
+ if response.msg is None or response.msg.content is None:
1887
2040
  logger.error(
1888
- f"JSON parsing error in worker creation: Invalid response "
1889
- f"format - {e}. Response content: "
1890
- f"{response.msg.content[:100]}..."
2041
+ "Coordinator agent returned empty response for "
2042
+ "worker creation"
1891
2043
  )
1892
- raise RuntimeError(
1893
- f"Failed to create worker for task {task.id}: "
1894
- f"Coordinator agent returned malformed JSON response. "
2044
+ # Create a fallback worker configuration
2045
+ new_node_conf = WorkerConf(
2046
+ description=f"Fallback worker for "
2047
+ f"task: {task.content[:50]}...",
2048
+ role="General Assistant",
2049
+ sys_msg="You are a general assistant that can help "
2050
+ "with various tasks.",
1895
2051
  )
2052
+ else:
2053
+ try:
2054
+ result_dict = json.loads(response.msg.content)
2055
+ new_node_conf = WorkerConf(**result_dict)
2056
+ except json.JSONDecodeError as e:
2057
+ logger.error(
2058
+ f"JSON parsing error in worker creation: Invalid "
2059
+ f"response format - {e}. Response content: "
2060
+ f"format - {e}. Response content: "
2061
+ f"{response.msg.content[:100]}..."
2062
+ )
2063
+ raise RuntimeError(
2064
+ f"Failed to create worker for task {task.id}: "
2065
+ f"Coordinator agent returned malformed JSON response. "
2066
+ ) from e
1896
2067
 
1897
2068
  new_agent = await self._create_new_agent(
1898
2069
  new_node_conf.role,
@@ -1903,6 +2074,7 @@ class Workforce(BaseNode):
1903
2074
  description=new_node_conf.description,
1904
2075
  worker=new_agent,
1905
2076
  pool_max_size=DEFAULT_WORKER_POOL_SIZE,
2077
+ use_structured_output_handler=self.use_structured_output_handler,
1906
2078
  )
1907
2079
  new_node.set_channel(self._channel)
1908
2080
 
@@ -2129,101 +2301,107 @@ class Workforce(BaseNode):
2129
2301
  f"{recovery_decision.reasoning}"
2130
2302
  )
2131
2303
 
2132
- if recovery_decision.strategy == RecoveryStrategy.RETRY:
2133
- # Simply retry the task by reposting it
2134
- if task.id in self._assignees:
2135
- assignee_id = self._assignees[task.id]
2136
- await self._post_task(task, assignee_id)
2137
- action_taken = f"retried with same worker {assignee_id}"
2138
- else:
2139
- # Find a new assignee and retry
2140
- batch_result = await self._find_assignee([task])
2141
- assignment = batch_result.assignments[0]
2142
- self._assignees[task.id] = assignment.assignee_id
2143
- await self._post_task(task, assignment.assignee_id)
2144
- action_taken = (
2145
- f"retried with new worker {assignment.assignee_id}"
2146
- )
2304
+ # Clean up tracking before attempting recovery
2305
+ if task.id in self._assignees:
2306
+ await self._channel.archive_task(task.id)
2307
+ self._cleanup_task_tracking(task.id)
2147
2308
 
2148
- elif recovery_decision.strategy == RecoveryStrategy.REPLAN:
2149
- # Modify the task content and retry
2150
- if recovery_decision.modified_task_content:
2151
- task.content = recovery_decision.modified_task_content
2152
- logger.info(f"Task {task.id} content modified for replan")
2309
+ try:
2310
+ if recovery_decision.strategy == RecoveryStrategy.RETRY:
2311
+ # Simply retry the task by reposting it
2312
+ if task.id in self._assignees:
2313
+ assignee_id = self._assignees[task.id]
2314
+ await self._post_task(task, assignee_id)
2315
+ action_taken = f"retried with same worker {assignee_id}"
2316
+ else:
2317
+ # Find a new assignee and retry
2318
+ batch_result = await self._find_assignee([task])
2319
+ assignment = batch_result.assignments[0]
2320
+ self._assignees[task.id] = assignment.assignee_id
2321
+ await self._post_task(task, assignment.assignee_id)
2322
+ action_taken = (
2323
+ f"retried with new worker {assignment.assignee_id}"
2324
+ )
2153
2325
 
2154
- # Repost the modified task
2155
- if task.id in self._assignees:
2156
- assignee_id = self._assignees[task.id]
2157
- await self._post_task(task, assignee_id)
2158
- action_taken = (
2159
- f"replanned and retried with worker {assignee_id}"
2160
- )
2161
- else:
2162
- # Find a new assignee for the replanned task
2163
- batch_result = await self._find_assignee([task])
2164
- assignment = batch_result.assignments[0]
2165
- self._assignees[task.id] = assignment.assignee_id
2166
- await self._post_task(task, assignment.assignee_id)
2167
- action_taken = (
2168
- f"replanned and assigned to "
2169
- f"worker {assignment.assignee_id}"
2170
- )
2326
+ elif recovery_decision.strategy == RecoveryStrategy.REPLAN:
2327
+ # Modify the task content and retry
2328
+ if recovery_decision.modified_task_content:
2329
+ task.content = recovery_decision.modified_task_content
2330
+ logger.info(f"Task {task.id} content modified for replan")
2171
2331
 
2172
- elif recovery_decision.strategy == RecoveryStrategy.DECOMPOSE:
2173
- # Decompose the task into subtasks
2174
- subtasks = self._decompose_task(task)
2175
- if self.metrics_logger and subtasks:
2176
- self.metrics_logger.log_task_decomposed(
2177
- parent_task_id=task.id,
2178
- subtask_ids=[st.id for st in subtasks],
2179
- )
2180
- for subtask in subtasks:
2181
- self.metrics_logger.log_task_created(
2182
- task_id=subtask.id,
2183
- description=subtask.content,
2184
- parent_task_id=task.id,
2185
- task_type=subtask.type,
2186
- metadata=subtask.additional_info,
2332
+ # Repost the modified task
2333
+ if task.id in self._assignees:
2334
+ assignee_id = self._assignees[task.id]
2335
+ await self._post_task(task, assignee_id)
2336
+ action_taken = (
2337
+ f"replanned and retried with worker {assignee_id}"
2338
+ )
2339
+ else:
2340
+ # Find a new assignee for the replanned task
2341
+ batch_result = await self._find_assignee([task])
2342
+ assignment = batch_result.assignments[0]
2343
+ self._assignees[task.id] = assignment.assignee_id
2344
+ await self._post_task(task, assignment.assignee_id)
2345
+ action_taken = (
2346
+ f"replanned and assigned to "
2347
+ f"worker {assignment.assignee_id}"
2187
2348
  )
2188
- # Insert packets at the head of the queue
2189
- self._pending_tasks.extendleft(reversed(subtasks))
2190
-
2191
- await self._post_ready_tasks()
2192
- action_taken = f"decomposed into {len(subtasks)} subtasks"
2193
2349
 
2194
- # Handle task completion differently for decomposed tasks
2195
- if task.id in self._assignees:
2196
- await self._channel.archive_task(task.id)
2350
+ elif recovery_decision.strategy == RecoveryStrategy.DECOMPOSE:
2351
+ # Decompose the task into subtasks
2352
+ subtasks = self._decompose_task(task)
2353
+ if self.metrics_logger and subtasks:
2354
+ self.metrics_logger.log_task_decomposed(
2355
+ parent_task_id=task.id,
2356
+ subtask_ids=[st.id for st in subtasks],
2357
+ )
2358
+ for subtask in subtasks:
2359
+ self.metrics_logger.log_task_created(
2360
+ task_id=subtask.id,
2361
+ description=subtask.content,
2362
+ parent_task_id=task.id,
2363
+ task_type=subtask.type,
2364
+ metadata=subtask.additional_info,
2365
+ )
2366
+ # Insert packets at the head of the queue
2367
+ self._pending_tasks.extendleft(reversed(subtasks))
2197
2368
 
2198
- self._cleanup_task_tracking(task.id)
2199
- logger.debug(
2200
- f"Task {task.id} failed and was {action_taken}. "
2201
- f"Dependencies updated for subtasks."
2202
- )
2369
+ await self._post_ready_tasks()
2370
+ action_taken = f"decomposed into {len(subtasks)} subtasks"
2203
2371
 
2204
- # Sync shared memory after task decomposition
2205
- if self.share_memory:
2206
- logger.info(
2207
- f"Syncing shared memory after task {task.id} decomposition"
2372
+ logger.debug(
2373
+ f"Task {task.id} failed and was {action_taken}. "
2374
+ f"Dependencies updated for subtasks."
2208
2375
  )
2209
- self._sync_shared_memory()
2210
2376
 
2211
- # Check if any pending tasks are now ready to execute
2212
- await self._post_ready_tasks()
2213
- return False
2377
+ # Sync shared memory after task decomposition
2378
+ if self.share_memory:
2379
+ logger.info(
2380
+ f"Syncing shared memory after "
2381
+ f"task {task.id} decomposition"
2382
+ )
2383
+ self._sync_shared_memory()
2214
2384
 
2215
- elif recovery_decision.strategy == RecoveryStrategy.CREATE_WORKER:
2216
- assignee = await self._create_worker_node_for_task(task)
2217
- await self._post_task(task, assignee.node_id)
2218
- action_taken = (
2219
- f"created new worker {assignee.node_id} and assigned "
2220
- f"task {task.id} to it"
2221
- )
2385
+ # Check if any pending tasks are now ready to execute
2386
+ await self._post_ready_tasks()
2387
+ return False
2222
2388
 
2223
- if task.id in self._assignees:
2224
- await self._channel.archive_task(task.id)
2389
+ elif recovery_decision.strategy == RecoveryStrategy.CREATE_WORKER:
2390
+ assignee = await self._create_worker_node_for_task(task)
2391
+ await self._post_task(task, assignee.node_id)
2392
+ action_taken = (
2393
+ f"created new worker {assignee.node_id} and assigned "
2394
+ f"task {task.id} to it"
2395
+ )
2396
+ except Exception as e:
2397
+ logger.error(f"Recovery strategy failed for task {task.id}: {e}")
2398
+ # If max retries reached, halt the workforce
2399
+ if task.failure_count >= MAX_TASK_RETRIES:
2400
+ self._completed_tasks.append(task)
2401
+ return True
2402
+ self._completed_tasks.append(task)
2403
+ return False
2225
2404
 
2226
- self._cleanup_task_tracking(task.id)
2227
2405
  logger.debug(
2228
2406
  f"Task {task.id} failed and was {action_taken}. "
2229
2407
  f"Updating dependency state."
@@ -2716,6 +2894,7 @@ class Workforce(BaseNode):
2716
2894
  else None,
2717
2895
  graceful_shutdown_timeout=self.graceful_shutdown_timeout,
2718
2896
  share_memory=self.share_memory,
2897
+ use_structured_output_handler=self.use_structured_output_handler,
2719
2898
  )
2720
2899
 
2721
2900
  for child in self._children:
camel/tasks/task.py CHANGED
@@ -184,7 +184,7 @@ def parse_response(
184
184
  tasks = []
185
185
  if task_id is None:
186
186
  task_id = "0"
187
- for i, content in enumerate(tasks_content):
187
+ for i, content in enumerate(tasks_content, 1):
188
188
  stripped_content = content.strip()
189
189
  # validate subtask content before creating the task
190
190
  if validate_task_content(stripped_content, f"{task_id}.{i}"):