camel-ai 0.2.71a5__py3-none-any.whl → 0.2.71a7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of camel-ai might be problematic. Click here for more details.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +51 -1
- camel/societies/workforce/role_playing_worker.py +64 -7
- camel/societies/workforce/single_agent_worker.py +73 -18
- camel/societies/workforce/structured_output_handler.py +500 -0
- camel/societies/workforce/worker.py +2 -0
- camel/societies/workforce/workforce.py +312 -147
- camel/tasks/task.py +1 -1
- camel/toolkits/file_write_toolkit.py +179 -124
- camel/toolkits/hybrid_browser_toolkit/actions.py +235 -60
- camel/toolkits/hybrid_browser_toolkit/agent.py +25 -8
- camel/toolkits/hybrid_browser_toolkit/browser_session.py +574 -164
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +996 -126
- camel/toolkits/hybrid_browser_toolkit/stealth_config.py +116 -0
- camel/toolkits/hybrid_browser_toolkit/stealth_script.js +0 -0
- camel/toolkits/note_taking_toolkit.py +7 -13
- {camel_ai-0.2.71a5.dist-info → camel_ai-0.2.71a7.dist-info}/METADATA +1 -1
- {camel_ai-0.2.71a5.dist-info → camel_ai-0.2.71a7.dist-info}/RECORD +20 -17
- {camel_ai-0.2.71a5.dist-info → camel_ai-0.2.71a7.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.71a5.dist-info → camel_ai-0.2.71a7.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
#
|
|
733
|
-
self.
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
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
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
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
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
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
|
-
|
|
1889
|
-
|
|
1890
|
-
f"{response.msg.content[:100]}..."
|
|
2041
|
+
"Coordinator agent returned empty response for "
|
|
2042
|
+
"worker creation"
|
|
1891
2043
|
)
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
f"
|
|
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
|
|
|
@@ -1973,17 +2145,7 @@ class Workforce(BaseNode):
|
|
|
1973
2145
|
f"Current pending tasks: {len(self._pending_tasks)}, "
|
|
1974
2146
|
f"In-flight tasks: {self._in_flight_tasks}"
|
|
1975
2147
|
)
|
|
1976
|
-
logger.
|
|
1977
|
-
|
|
1978
|
-
if self._pending_tasks and self._assignees:
|
|
1979
|
-
for task in self._pending_tasks:
|
|
1980
|
-
if task.id in self._assignees:
|
|
1981
|
-
# Mark task as failed and decrement counter
|
|
1982
|
-
task.set_state(TaskState.FAILED)
|
|
1983
|
-
self._decrement_in_flight_tasks(
|
|
1984
|
-
task.id, "timeout/error in _get_returned_task"
|
|
1985
|
-
)
|
|
1986
|
-
return task
|
|
2148
|
+
logger.error(error_msg, exc_info=True)
|
|
1987
2149
|
return None
|
|
1988
2150
|
|
|
1989
2151
|
async def _post_ready_tasks(self) -> None:
|
|
@@ -2059,12 +2221,8 @@ class Workforce(BaseNode):
|
|
|
2059
2221
|
task.failure_count += 1
|
|
2060
2222
|
|
|
2061
2223
|
# Determine detailed failure information
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
f"response: {task.result[:100] if task.result else ''}..."
|
|
2065
|
-
else:
|
|
2066
|
-
failure_reason = "Task marked as failed despite "
|
|
2067
|
-
f"having result: {(task.result or '')[:100]}..."
|
|
2224
|
+
# Use the actual error/result stored in task.result
|
|
2225
|
+
failure_reason = task.result or "Unknown error"
|
|
2068
2226
|
|
|
2069
2227
|
# Add context about the worker and task
|
|
2070
2228
|
worker_id = task.assigned_worker_id or "unknown"
|
|
@@ -2129,101 +2287,107 @@ class Workforce(BaseNode):
|
|
|
2129
2287
|
f"{recovery_decision.reasoning}"
|
|
2130
2288
|
)
|
|
2131
2289
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
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
|
-
)
|
|
2290
|
+
# Clean up tracking before attempting recovery
|
|
2291
|
+
if task.id in self._assignees:
|
|
2292
|
+
await self._channel.archive_task(task.id)
|
|
2293
|
+
self._cleanup_task_tracking(task.id)
|
|
2147
2294
|
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
task.
|
|
2152
|
-
|
|
2295
|
+
try:
|
|
2296
|
+
if recovery_decision.strategy == RecoveryStrategy.RETRY:
|
|
2297
|
+
# Simply retry the task by reposting it
|
|
2298
|
+
if task.id in self._assignees:
|
|
2299
|
+
assignee_id = self._assignees[task.id]
|
|
2300
|
+
await self._post_task(task, assignee_id)
|
|
2301
|
+
action_taken = f"retried with same worker {assignee_id}"
|
|
2302
|
+
else:
|
|
2303
|
+
# Find a new assignee and retry
|
|
2304
|
+
batch_result = await self._find_assignee([task])
|
|
2305
|
+
assignment = batch_result.assignments[0]
|
|
2306
|
+
self._assignees[task.id] = assignment.assignee_id
|
|
2307
|
+
await self._post_task(task, assignment.assignee_id)
|
|
2308
|
+
action_taken = (
|
|
2309
|
+
f"retried with new worker {assignment.assignee_id}"
|
|
2310
|
+
)
|
|
2153
2311
|
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
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
|
-
)
|
|
2312
|
+
elif recovery_decision.strategy == RecoveryStrategy.REPLAN:
|
|
2313
|
+
# Modify the task content and retry
|
|
2314
|
+
if recovery_decision.modified_task_content:
|
|
2315
|
+
task.content = recovery_decision.modified_task_content
|
|
2316
|
+
logger.info(f"Task {task.id} content modified for replan")
|
|
2171
2317
|
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
self.
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2318
|
+
# Repost the modified task
|
|
2319
|
+
if task.id in self._assignees:
|
|
2320
|
+
assignee_id = self._assignees[task.id]
|
|
2321
|
+
await self._post_task(task, assignee_id)
|
|
2322
|
+
action_taken = (
|
|
2323
|
+
f"replanned and retried with worker {assignee_id}"
|
|
2324
|
+
)
|
|
2325
|
+
else:
|
|
2326
|
+
# Find a new assignee for the replanned task
|
|
2327
|
+
batch_result = await self._find_assignee([task])
|
|
2328
|
+
assignment = batch_result.assignments[0]
|
|
2329
|
+
self._assignees[task.id] = assignment.assignee_id
|
|
2330
|
+
await self._post_task(task, assignment.assignee_id)
|
|
2331
|
+
action_taken = (
|
|
2332
|
+
f"replanned and assigned to "
|
|
2333
|
+
f"worker {assignment.assignee_id}"
|
|
2187
2334
|
)
|
|
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
2335
|
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2336
|
+
elif recovery_decision.strategy == RecoveryStrategy.DECOMPOSE:
|
|
2337
|
+
# Decompose the task into subtasks
|
|
2338
|
+
subtasks = self._decompose_task(task)
|
|
2339
|
+
if self.metrics_logger and subtasks:
|
|
2340
|
+
self.metrics_logger.log_task_decomposed(
|
|
2341
|
+
parent_task_id=task.id,
|
|
2342
|
+
subtask_ids=[st.id for st in subtasks],
|
|
2343
|
+
)
|
|
2344
|
+
for subtask in subtasks:
|
|
2345
|
+
self.metrics_logger.log_task_created(
|
|
2346
|
+
task_id=subtask.id,
|
|
2347
|
+
description=subtask.content,
|
|
2348
|
+
parent_task_id=task.id,
|
|
2349
|
+
task_type=subtask.type,
|
|
2350
|
+
metadata=subtask.additional_info,
|
|
2351
|
+
)
|
|
2352
|
+
# Insert packets at the head of the queue
|
|
2353
|
+
self._pending_tasks.extendleft(reversed(subtasks))
|
|
2197
2354
|
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
f"Task {task.id} failed and was {action_taken}. "
|
|
2201
|
-
f"Dependencies updated for subtasks."
|
|
2202
|
-
)
|
|
2355
|
+
await self._post_ready_tasks()
|
|
2356
|
+
action_taken = f"decomposed into {len(subtasks)} subtasks"
|
|
2203
2357
|
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
f"Syncing shared memory after task {task.id} decomposition"
|
|
2358
|
+
logger.debug(
|
|
2359
|
+
f"Task {task.id} failed and was {action_taken}. "
|
|
2360
|
+
f"Dependencies updated for subtasks."
|
|
2208
2361
|
)
|
|
2209
|
-
self._sync_shared_memory()
|
|
2210
2362
|
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2363
|
+
# Sync shared memory after task decomposition
|
|
2364
|
+
if self.share_memory:
|
|
2365
|
+
logger.info(
|
|
2366
|
+
f"Syncing shared memory after "
|
|
2367
|
+
f"task {task.id} decomposition"
|
|
2368
|
+
)
|
|
2369
|
+
self._sync_shared_memory()
|
|
2214
2370
|
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
action_taken = (
|
|
2219
|
-
f"created new worker {assignee.node_id} and assigned "
|
|
2220
|
-
f"task {task.id} to it"
|
|
2221
|
-
)
|
|
2371
|
+
# Check if any pending tasks are now ready to execute
|
|
2372
|
+
await self._post_ready_tasks()
|
|
2373
|
+
return False
|
|
2222
2374
|
|
|
2223
|
-
|
|
2224
|
-
|
|
2375
|
+
elif recovery_decision.strategy == RecoveryStrategy.CREATE_WORKER:
|
|
2376
|
+
assignee = await self._create_worker_node_for_task(task)
|
|
2377
|
+
await self._post_task(task, assignee.node_id)
|
|
2378
|
+
action_taken = (
|
|
2379
|
+
f"created new worker {assignee.node_id} and assigned "
|
|
2380
|
+
f"task {task.id} to it"
|
|
2381
|
+
)
|
|
2382
|
+
except Exception as e:
|
|
2383
|
+
logger.error(f"Recovery strategy failed for task {task.id}: {e}")
|
|
2384
|
+
# If max retries reached, halt the workforce
|
|
2385
|
+
if task.failure_count >= MAX_TASK_RETRIES:
|
|
2386
|
+
self._completed_tasks.append(task)
|
|
2387
|
+
return True
|
|
2388
|
+
self._completed_tasks.append(task)
|
|
2389
|
+
return False
|
|
2225
2390
|
|
|
2226
|
-
self._cleanup_task_tracking(task.id)
|
|
2227
2391
|
logger.debug(
|
|
2228
2392
|
f"Task {task.id} failed and was {action_taken}. "
|
|
2229
2393
|
f"Updating dependency state."
|
|
@@ -2716,6 +2880,7 @@ class Workforce(BaseNode):
|
|
|
2716
2880
|
else None,
|
|
2717
2881
|
graceful_shutdown_timeout=self.graceful_shutdown_timeout,
|
|
2718
2882
|
share_memory=self.share_memory,
|
|
2883
|
+
use_structured_output_handler=self.use_structured_output_handler,
|
|
2719
2884
|
)
|
|
2720
2885
|
|
|
2721
2886
|
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}"):
|