camel-ai 0.2.71a3__py3-none-any.whl → 0.2.71a4__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/interpreters/docker_interpreter.py +3 -2
- camel/loaders/base_loader.py +85 -0
- camel/societies/workforce/workforce.py +144 -33
- camel/toolkits/__init__.py +5 -2
- camel/toolkits/craw4ai_toolkit.py +2 -2
- camel/toolkits/file_write_toolkit.py +6 -6
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +9 -3
- camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +31 -8
- camel/toolkits/note_taking_toolkit.py +90 -0
- camel/toolkits/openai_image_toolkit.py +292 -0
- camel/toolkits/slack_toolkit.py +4 -4
- camel/toolkits/terminal_toolkit.py +223 -73
- camel/utils/mcp_client.py +37 -1
- {camel_ai-0.2.71a3.dist-info → camel_ai-0.2.71a4.dist-info}/METADATA +43 -4
- {camel_ai-0.2.71a3.dist-info → camel_ai-0.2.71a4.dist-info}/RECORD +18 -16
- camel/toolkits/dalle_toolkit.py +0 -175
- {camel_ai-0.2.71a3.dist-info → camel_ai-0.2.71a4.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.71a3.dist-info → camel_ai-0.2.71a4.dist-info}/licenses/LICENSE +0 -0
camel/__init__.py
CHANGED
|
@@ -146,8 +146,9 @@ class DockerInterpreter(BaseInterpreter):
|
|
|
146
146
|
tar_stream = io.BytesIO()
|
|
147
147
|
with tarfile.open(fileobj=tar_stream, mode='w') as tar:
|
|
148
148
|
tarinfo = tarfile.TarInfo(name=filename)
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
encoded_content = content.encode('utf-8')
|
|
150
|
+
tarinfo.size = len(encoded_content)
|
|
151
|
+
tar.addfile(tarinfo, io.BytesIO(encoded_content))
|
|
151
152
|
tar_stream.seek(0)
|
|
152
153
|
|
|
153
154
|
# copy the tar into the container
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Union
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseLoader(ABC):
|
|
20
|
+
r"""Abstract base class for all data loaders in CAMEL."""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def _load_single(self, source: Union[str, Path]) -> Dict[str, Any]:
|
|
24
|
+
r"""Load data from a single source.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
source (Union[str, Path]): The data source to load from.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dict[str, Any]: A dictionary containing the loaded data. It is
|
|
31
|
+
recommended that the dictionary includes a "content" key with
|
|
32
|
+
the primary data and optional metadata keys.
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def load(
|
|
37
|
+
self,
|
|
38
|
+
source: Union[str, Path, List[Union[str, Path]]],
|
|
39
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
40
|
+
r"""Load data from one or multiple sources.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
source (Union[str, Path, List[Union[str, Path]]]): The data source
|
|
44
|
+
(s) to load from. Can be:
|
|
45
|
+
- A single path/URL (str or Path)
|
|
46
|
+
- A list of paths/URLs
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dict[str, List[Dict[str, Any]]]: A dictionary with a single key
|
|
50
|
+
"contents" containing a list of loaded data. If a single source
|
|
51
|
+
is provided, the list will contain a single item.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If no sources are provided
|
|
55
|
+
Exception: If loading fails for any source
|
|
56
|
+
"""
|
|
57
|
+
if not source:
|
|
58
|
+
raise ValueError("At least one source must be provided")
|
|
59
|
+
|
|
60
|
+
# Convert single source to list for uniform processing
|
|
61
|
+
sources = [source] if isinstance(source, (str, Path)) else list(source)
|
|
62
|
+
|
|
63
|
+
# Process all sources
|
|
64
|
+
results = []
|
|
65
|
+
for i, src in enumerate(sources, 1):
|
|
66
|
+
try:
|
|
67
|
+
content = self._load_single(src)
|
|
68
|
+
results.append(content)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise RuntimeError(
|
|
71
|
+
f"Error loading source {i}/{len(sources)}: {src}"
|
|
72
|
+
) from e
|
|
73
|
+
|
|
74
|
+
return {"contents": results}
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def supported_formats(self) -> set[str]:
|
|
79
|
+
r"""Get the set of supported file formats or data sources.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
set[str]: A set of strings representing the supported formats/
|
|
83
|
+
sources.
|
|
84
|
+
"""
|
|
85
|
+
pass
|
|
@@ -19,7 +19,16 @@ import time
|
|
|
19
19
|
import uuid
|
|
20
20
|
from collections import deque
|
|
21
21
|
from enum import Enum
|
|
22
|
-
from typing import
|
|
22
|
+
from typing import (
|
|
23
|
+
Any,
|
|
24
|
+
Coroutine,
|
|
25
|
+
Deque,
|
|
26
|
+
Dict,
|
|
27
|
+
List,
|
|
28
|
+
Optional,
|
|
29
|
+
Set,
|
|
30
|
+
Tuple,
|
|
31
|
+
)
|
|
23
32
|
|
|
24
33
|
from colorama import Fore
|
|
25
34
|
|
|
@@ -200,7 +209,7 @@ class Workforce(BaseNode):
|
|
|
200
209
|
children: Optional[List[BaseNode]] = None,
|
|
201
210
|
coordinator_agent: Optional[ChatAgent] = None,
|
|
202
211
|
task_agent: Optional[ChatAgent] = None,
|
|
203
|
-
new_worker_agent: Optional[ChatAgent] = None,
|
|
212
|
+
new_worker_agent: Optional[ChatAgent] = None,
|
|
204
213
|
graceful_shutdown_timeout: float = 15.0,
|
|
205
214
|
share_memory: bool = False,
|
|
206
215
|
) -> None:
|
|
@@ -325,9 +334,10 @@ class Workforce(BaseNode):
|
|
|
325
334
|
"settings (ModelPlatformType.DEFAULT, ModelType.DEFAULT) "
|
|
326
335
|
"with default system message and TaskPlanningToolkit."
|
|
327
336
|
)
|
|
337
|
+
task_tools = TaskPlanningToolkit().get_tools()
|
|
328
338
|
self.task_agent = ChatAgent(
|
|
329
339
|
task_sys_msg,
|
|
330
|
-
tools=
|
|
340
|
+
tools=task_tools, # type: ignore[arg-type]
|
|
331
341
|
)
|
|
332
342
|
else:
|
|
333
343
|
logger.info(
|
|
@@ -563,6 +573,44 @@ class Workforce(BaseNode):
|
|
|
563
573
|
except Exception as e:
|
|
564
574
|
logger.warning(f"Error synchronizing shared memory: {e}")
|
|
565
575
|
|
|
576
|
+
def _update_dependencies_for_decomposition(
|
|
577
|
+
self, original_task: Task, subtasks: List[Task]
|
|
578
|
+
) -> None:
|
|
579
|
+
r"""Update dependency tracking when a task is decomposed into subtasks.
|
|
580
|
+
Tasks that depended on the original task should now depend on all
|
|
581
|
+
subtasks. The last subtask inherits the original task's dependencies.
|
|
582
|
+
"""
|
|
583
|
+
if not subtasks:
|
|
584
|
+
return
|
|
585
|
+
|
|
586
|
+
original_task_id = original_task.id
|
|
587
|
+
subtask_ids = [subtask.id for subtask in subtasks]
|
|
588
|
+
|
|
589
|
+
# Find tasks that depend on the original task
|
|
590
|
+
dependent_task_ids = [
|
|
591
|
+
task_id
|
|
592
|
+
for task_id, deps in self._task_dependencies.items()
|
|
593
|
+
if original_task_id in deps
|
|
594
|
+
]
|
|
595
|
+
|
|
596
|
+
# Update dependent tasks to depend on all subtasks
|
|
597
|
+
for task_id in dependent_task_ids:
|
|
598
|
+
dependencies = self._task_dependencies[task_id]
|
|
599
|
+
dependencies.remove(original_task_id)
|
|
600
|
+
dependencies.extend(subtask_ids)
|
|
601
|
+
|
|
602
|
+
# The last subtask inherits original task's dependencies (if any)
|
|
603
|
+
if original_task_id in self._task_dependencies:
|
|
604
|
+
original_dependencies = self._task_dependencies[original_task_id]
|
|
605
|
+
if original_dependencies:
|
|
606
|
+
# Set dependencies for the last subtask to maintain execution
|
|
607
|
+
# order
|
|
608
|
+
self._task_dependencies[subtask_ids[-1]] = (
|
|
609
|
+
original_dependencies.copy()
|
|
610
|
+
)
|
|
611
|
+
# Remove original task dependencies as it's now decomposed
|
|
612
|
+
del self._task_dependencies[original_task_id]
|
|
613
|
+
|
|
566
614
|
def _cleanup_task_tracking(self, task_id: str) -> None:
|
|
567
615
|
r"""Clean up tracking data for a task to prevent memory leaks.
|
|
568
616
|
|
|
@@ -590,6 +638,10 @@ class Workforce(BaseNode):
|
|
|
590
638
|
for subtask in subtasks:
|
|
591
639
|
subtask.parent = task
|
|
592
640
|
|
|
641
|
+
# Update dependency tracking for decomposed task
|
|
642
|
+
if subtasks:
|
|
643
|
+
self._update_dependencies_for_decomposition(task, subtasks)
|
|
644
|
+
|
|
593
645
|
return subtasks
|
|
594
646
|
|
|
595
647
|
# Human intervention methods
|
|
@@ -1436,7 +1488,9 @@ class Workforce(BaseNode):
|
|
|
1436
1488
|
|
|
1437
1489
|
return valid_assignments, invalid_assignments
|
|
1438
1490
|
|
|
1439
|
-
def _handle_task_assignment_fallbacks(
|
|
1491
|
+
async def _handle_task_assignment_fallbacks(
|
|
1492
|
+
self, tasks: List[Task]
|
|
1493
|
+
) -> List:
|
|
1440
1494
|
r"""Create new workers for unassigned tasks as fallback.
|
|
1441
1495
|
|
|
1442
1496
|
Args:
|
|
@@ -1449,7 +1503,7 @@ class Workforce(BaseNode):
|
|
|
1449
1503
|
|
|
1450
1504
|
for task in tasks:
|
|
1451
1505
|
logger.info(f"Creating new worker for unassigned task {task.id}")
|
|
1452
|
-
new_worker = self._create_worker_node_for_task(task)
|
|
1506
|
+
new_worker = await self._create_worker_node_for_task(task)
|
|
1453
1507
|
|
|
1454
1508
|
assignment = TaskAssignment(
|
|
1455
1509
|
task_id=task.id,
|
|
@@ -1460,7 +1514,7 @@ class Workforce(BaseNode):
|
|
|
1460
1514
|
|
|
1461
1515
|
return fallback_assignments
|
|
1462
1516
|
|
|
1463
|
-
def _handle_assignment_retry_and_fallback(
|
|
1517
|
+
async def _handle_assignment_retry_and_fallback(
|
|
1464
1518
|
self,
|
|
1465
1519
|
invalid_assignments: List[TaskAssignment],
|
|
1466
1520
|
tasks: List[Task],
|
|
@@ -1531,14 +1585,14 @@ class Workforce(BaseNode):
|
|
|
1531
1585
|
f"Creating fallback workers for {len(unassigned_tasks)} "
|
|
1532
1586
|
f"unassigned tasks"
|
|
1533
1587
|
)
|
|
1534
|
-
fallback_assignments =
|
|
1535
|
-
unassigned_tasks
|
|
1588
|
+
fallback_assignments = (
|
|
1589
|
+
await self._handle_task_assignment_fallbacks(unassigned_tasks)
|
|
1536
1590
|
)
|
|
1537
1591
|
final_assignments.extend(fallback_assignments)
|
|
1538
1592
|
|
|
1539
1593
|
return final_assignments
|
|
1540
1594
|
|
|
1541
|
-
def _find_assignee(
|
|
1595
|
+
async def _find_assignee(
|
|
1542
1596
|
self,
|
|
1543
1597
|
tasks: List[Task],
|
|
1544
1598
|
) -> TaskAssignResult:
|
|
@@ -1580,7 +1634,7 @@ class Workforce(BaseNode):
|
|
|
1580
1634
|
# invalid assignments and unassigned tasks
|
|
1581
1635
|
all_problem_assignments = invalid_assignments
|
|
1582
1636
|
retry_and_fallback_assignments = (
|
|
1583
|
-
self._handle_assignment_retry_and_fallback(
|
|
1637
|
+
await self._handle_assignment_retry_and_fallback(
|
|
1584
1638
|
all_problem_assignments, tasks, valid_worker_ids
|
|
1585
1639
|
)
|
|
1586
1640
|
)
|
|
@@ -1616,7 +1670,7 @@ class Workforce(BaseNode):
|
|
|
1616
1670
|
async def _post_dependency(self, dependency: Task) -> None:
|
|
1617
1671
|
await self._channel.post_dependency(dependency, self.node_id)
|
|
1618
1672
|
|
|
1619
|
-
def _create_worker_node_for_task(self, task: Task) -> Worker:
|
|
1673
|
+
async def _create_worker_node_for_task(self, task: Task) -> Worker:
|
|
1620
1674
|
r"""Creates a new worker node for a given task and add it to the
|
|
1621
1675
|
children list of this node. This is one of the actions that
|
|
1622
1676
|
the coordinator can take when a task has failed.
|
|
@@ -1662,7 +1716,7 @@ class Workforce(BaseNode):
|
|
|
1662
1716
|
f"Coordinator agent returned malformed JSON response. "
|
|
1663
1717
|
)
|
|
1664
1718
|
|
|
1665
|
-
new_agent = self._create_new_agent(
|
|
1719
|
+
new_agent = await self._create_new_agent(
|
|
1666
1720
|
new_node_conf.role,
|
|
1667
1721
|
new_node_conf.sys_msg,
|
|
1668
1722
|
)
|
|
@@ -1689,14 +1743,19 @@ class Workforce(BaseNode):
|
|
|
1689
1743
|
)
|
|
1690
1744
|
return new_node
|
|
1691
1745
|
|
|
1692
|
-
def _create_new_agent(self, role: str, sys_msg: str) -> ChatAgent:
|
|
1746
|
+
async def _create_new_agent(self, role: str, sys_msg: str) -> ChatAgent:
|
|
1693
1747
|
worker_sys_msg = BaseMessage.make_assistant_message(
|
|
1694
1748
|
role_name=role,
|
|
1695
1749
|
content=sys_msg,
|
|
1696
1750
|
)
|
|
1697
1751
|
|
|
1698
1752
|
if self.new_worker_agent is not None:
|
|
1699
|
-
|
|
1753
|
+
# Clone the template agent to create an independent instance
|
|
1754
|
+
cloned_agent = self.new_worker_agent.clone(with_memory=False)
|
|
1755
|
+
# Update the system message for the specific role
|
|
1756
|
+
cloned_agent._system_message = worker_sys_msg
|
|
1757
|
+
cloned_agent.init_messages() # Initialize with new system message
|
|
1758
|
+
return cloned_agent
|
|
1700
1759
|
else:
|
|
1701
1760
|
# Default tools for a new agent
|
|
1702
1761
|
function_list = [
|
|
@@ -1712,7 +1771,7 @@ class Workforce(BaseNode):
|
|
|
1712
1771
|
)
|
|
1713
1772
|
|
|
1714
1773
|
return ChatAgent(
|
|
1715
|
-
worker_sys_msg,
|
|
1774
|
+
system_message=worker_sys_msg,
|
|
1716
1775
|
model=model,
|
|
1717
1776
|
tools=function_list, # type: ignore[arg-type]
|
|
1718
1777
|
pause_event=self._pause_event,
|
|
@@ -1765,7 +1824,7 @@ class Workforce(BaseNode):
|
|
|
1765
1824
|
f"Found {len(tasks_to_assign)} new tasks. "
|
|
1766
1825
|
f"Requesting assignment..."
|
|
1767
1826
|
)
|
|
1768
|
-
batch_result = self._find_assignee(tasks_to_assign)
|
|
1827
|
+
batch_result = await self._find_assignee(tasks_to_assign)
|
|
1769
1828
|
logger.debug(
|
|
1770
1829
|
f"Coordinator returned assignments:\n"
|
|
1771
1830
|
f"{json.dumps(batch_result.dict(), indent=2)}"
|
|
@@ -1788,17 +1847,19 @@ class Workforce(BaseNode):
|
|
|
1788
1847
|
# Step 2: Iterate through all pending tasks and post those that are
|
|
1789
1848
|
# ready
|
|
1790
1849
|
posted_tasks = []
|
|
1791
|
-
# Pre-compute completed task IDs
|
|
1792
|
-
|
|
1850
|
+
# Pre-compute completed task IDs and their states for O(1) lookups
|
|
1851
|
+
completed_tasks_info = {t.id: t.state for t in self._completed_tasks}
|
|
1793
1852
|
|
|
1794
1853
|
for task in self._pending_tasks:
|
|
1795
1854
|
# A task must be assigned to be considered for posting
|
|
1796
1855
|
if task.id in self._task_dependencies:
|
|
1797
1856
|
dependencies = self._task_dependencies[task.id]
|
|
1798
1857
|
# Check if all dependencies for this task are in the completed
|
|
1799
|
-
# set
|
|
1858
|
+
# set and their state is DONE
|
|
1800
1859
|
if all(
|
|
1801
|
-
dep_id in
|
|
1860
|
+
dep_id in completed_tasks_info
|
|
1861
|
+
and completed_tasks_info[dep_id] == TaskState.DONE
|
|
1862
|
+
for dep_id in dependencies
|
|
1802
1863
|
):
|
|
1803
1864
|
assignee_id = self._assignees[task.id]
|
|
1804
1865
|
logger.debug(
|
|
@@ -1885,7 +1946,7 @@ class Workforce(BaseNode):
|
|
|
1885
1946
|
|
|
1886
1947
|
if task.get_depth() > 3:
|
|
1887
1948
|
# Create a new worker node and reassign
|
|
1888
|
-
assignee = self._create_worker_node_for_task(task)
|
|
1949
|
+
assignee = await self._create_worker_node_for_task(task)
|
|
1889
1950
|
|
|
1890
1951
|
# Sync shared memory after creating new worker to provide context
|
|
1891
1952
|
if self.share_memory:
|
|
@@ -1915,19 +1976,35 @@ class Workforce(BaseNode):
|
|
|
1915
1976
|
# Insert packets at the head of the queue
|
|
1916
1977
|
self._pending_tasks.extendleft(reversed(subtasks))
|
|
1917
1978
|
|
|
1979
|
+
await self._post_ready_tasks()
|
|
1980
|
+
action_taken = f"decomposed into {len(subtasks)} subtasks"
|
|
1981
|
+
|
|
1982
|
+
# Handle task completion differently for decomposed tasks
|
|
1983
|
+
if task.id in self._assignees:
|
|
1984
|
+
await self._channel.archive_task(task.id)
|
|
1985
|
+
|
|
1986
|
+
self._cleanup_task_tracking(task.id)
|
|
1987
|
+
logger.debug(
|
|
1988
|
+
f"Task {task.id} failed and was {action_taken}. "
|
|
1989
|
+
f"Dependencies updated for subtasks."
|
|
1990
|
+
)
|
|
1991
|
+
|
|
1918
1992
|
# Sync shared memory after task decomposition
|
|
1919
1993
|
if self.share_memory:
|
|
1920
1994
|
logger.info(
|
|
1921
|
-
f"Syncing shared memory after
|
|
1922
|
-
f"task {task.id}"
|
|
1995
|
+
f"Syncing shared memory after task {task.id} decomposition"
|
|
1923
1996
|
)
|
|
1924
1997
|
self._sync_shared_memory()
|
|
1925
1998
|
|
|
1999
|
+
# Check if any pending tasks are now ready to execute
|
|
1926
2000
|
await self._post_ready_tasks()
|
|
1927
|
-
|
|
2001
|
+
return False
|
|
2002
|
+
|
|
2003
|
+
# For reassigned tasks (depth > 3), handle normally
|
|
1928
2004
|
if task.id in self._assignees:
|
|
1929
2005
|
await self._channel.archive_task(task.id)
|
|
1930
2006
|
|
|
2007
|
+
self._cleanup_task_tracking(task.id)
|
|
1931
2008
|
logger.debug(
|
|
1932
2009
|
f"Task {task.id} failed and was {action_taken}. "
|
|
1933
2010
|
f"Updating dependency state."
|
|
@@ -2020,31 +2097,65 @@ class Workforce(BaseNode):
|
|
|
2020
2097
|
break
|
|
2021
2098
|
|
|
2022
2099
|
if not found_and_removed:
|
|
2023
|
-
# Task was already removed from pending queue (
|
|
2024
|
-
# it
|
|
2025
|
-
# draw user attention with a warning; record at debug level.
|
|
2100
|
+
# Task was already removed from pending queue (common case when
|
|
2101
|
+
# it was posted and removed immediately).
|
|
2026
2102
|
logger.debug(
|
|
2027
2103
|
f"Completed task {task.id} was already removed from pending "
|
|
2028
|
-
"queue."
|
|
2104
|
+
"queue (normal for posted tasks)."
|
|
2029
2105
|
)
|
|
2030
2106
|
|
|
2031
2107
|
# Archive the task and update dependency tracking
|
|
2032
2108
|
if task.id in self._assignees:
|
|
2033
2109
|
await self._channel.archive_task(task.id)
|
|
2034
2110
|
|
|
2035
|
-
# Ensure it's in completed tasks set
|
|
2036
|
-
|
|
2111
|
+
# Ensure it's in completed tasks set by updating if it exists or
|
|
2112
|
+
# appending if it's new.
|
|
2113
|
+
task_found_in_completed = False
|
|
2114
|
+
for i, t in enumerate(self._completed_tasks):
|
|
2115
|
+
if t.id == task.id:
|
|
2116
|
+
self._completed_tasks[i] = task
|
|
2117
|
+
task_found_in_completed = True
|
|
2118
|
+
break
|
|
2119
|
+
if not task_found_in_completed:
|
|
2120
|
+
self._completed_tasks.append(task)
|
|
2037
2121
|
|
|
2038
2122
|
# Handle parent task completion logic
|
|
2039
2123
|
parent = task.parent
|
|
2040
|
-
if parent
|
|
2124
|
+
if parent:
|
|
2125
|
+
# Check if all subtasks are completed and successful
|
|
2041
2126
|
all_subtasks_done = all(
|
|
2042
|
-
|
|
2127
|
+
any(
|
|
2128
|
+
t.id == sub.id and t.state == TaskState.DONE
|
|
2129
|
+
for t in self._completed_tasks
|
|
2130
|
+
)
|
|
2043
2131
|
for sub in parent.subtasks
|
|
2044
2132
|
)
|
|
2045
2133
|
if all_subtasks_done:
|
|
2046
|
-
#
|
|
2134
|
+
# Collect results from successful subtasks only
|
|
2135
|
+
successful_results = []
|
|
2136
|
+
for sub in parent.subtasks:
|
|
2137
|
+
completed_subtask = next(
|
|
2138
|
+
(
|
|
2139
|
+
t
|
|
2140
|
+
for t in self._completed_tasks
|
|
2141
|
+
if t.id == sub.id and t.state == TaskState.DONE
|
|
2142
|
+
),
|
|
2143
|
+
None,
|
|
2144
|
+
)
|
|
2145
|
+
if completed_subtask and completed_subtask.result:
|
|
2146
|
+
successful_results.append(
|
|
2147
|
+
f"--- Subtask {sub.id} Result ---\n"
|
|
2148
|
+
f"{completed_subtask.result}"
|
|
2149
|
+
)
|
|
2150
|
+
|
|
2151
|
+
# Set parent task state and result
|
|
2047
2152
|
parent.state = TaskState.DONE
|
|
2153
|
+
parent.result = (
|
|
2154
|
+
"\n\n".join(successful_results)
|
|
2155
|
+
if successful_results
|
|
2156
|
+
else "All subtasks completed"
|
|
2157
|
+
)
|
|
2158
|
+
|
|
2048
2159
|
logger.debug(
|
|
2049
2160
|
f"All subtasks of {parent.id} are done. "
|
|
2050
2161
|
f"Marking parent as complete."
|
camel/toolkits/__init__.py
CHANGED
|
@@ -23,7 +23,7 @@ from .open_api_specs.security_config import openapi_security_config
|
|
|
23
23
|
from .math_toolkit import MathToolkit
|
|
24
24
|
from .search_toolkit import SearchToolkit
|
|
25
25
|
from .weather_toolkit import WeatherToolkit
|
|
26
|
-
from .
|
|
26
|
+
from .openai_image_toolkit import OpenAIImageToolkit
|
|
27
27
|
from .ask_news_toolkit import AskNewsToolkit, AsyncAskNewsToolkit
|
|
28
28
|
from .linkedin_toolkit import LinkedInToolkit
|
|
29
29
|
from .reddit_toolkit import RedditToolkit
|
|
@@ -82,6 +82,7 @@ from .edgeone_pages_mcp_toolkit import EdgeOnePagesMCPToolkit
|
|
|
82
82
|
from .google_drive_mcp_toolkit import GoogleDriveMCPToolkit
|
|
83
83
|
from .craw4ai_toolkit import Crawl4AIToolkit
|
|
84
84
|
from .markitdown_toolkit import MarkItDownToolkit
|
|
85
|
+
from .note_taking_toolkit import NoteTakingToolkit
|
|
85
86
|
|
|
86
87
|
__all__ = [
|
|
87
88
|
'BaseToolkit',
|
|
@@ -96,7 +97,7 @@ __all__ = [
|
|
|
96
97
|
'SearchToolkit',
|
|
97
98
|
'SlackToolkit',
|
|
98
99
|
'WhatsAppToolkit',
|
|
99
|
-
'
|
|
100
|
+
'OpenAIImageToolkit',
|
|
100
101
|
'TwitterToolkit',
|
|
101
102
|
'WeatherToolkit',
|
|
102
103
|
'RetrievalToolkit',
|
|
@@ -145,10 +146,12 @@ __all__ = [
|
|
|
145
146
|
'PlaywrightMCPToolkit',
|
|
146
147
|
'WolframAlphaToolkit',
|
|
147
148
|
'BohriumToolkit',
|
|
149
|
+
'OpenAIImageToolkit',
|
|
148
150
|
'TaskPlanningToolkit',
|
|
149
151
|
'HybridBrowserToolkit',
|
|
150
152
|
'EdgeOnePagesMCPToolkit',
|
|
151
153
|
'GoogleDriveMCPToolkit',
|
|
152
154
|
'Crawl4AIToolkit',
|
|
153
155
|
'MarkItDownToolkit',
|
|
156
|
+
'NoteTakingToolkit',
|
|
154
157
|
]
|
|
@@ -71,11 +71,11 @@ class Crawl4AIToolkit(BaseToolkit):
|
|
|
71
71
|
return f"Error scraping {url}: {e}"
|
|
72
72
|
|
|
73
73
|
async def __aenter__(self):
|
|
74
|
-
"""Async context manager entry."""
|
|
74
|
+
r"""Async context manager entry."""
|
|
75
75
|
return self
|
|
76
76
|
|
|
77
77
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
78
|
-
"""Async context manager exit - cleanup the client."""
|
|
78
|
+
r"""Async context manager exit - cleanup the client."""
|
|
79
79
|
if self._client is not None:
|
|
80
80
|
await self._client.__aexit__(exc_type, exc_val, exc_tb)
|
|
81
81
|
self._client = None
|
|
@@ -160,9 +160,9 @@ class FileWriteToolkit(BaseToolkit):
|
|
|
160
160
|
file_path (Path): The target file path.
|
|
161
161
|
title (str): The title of the document.
|
|
162
162
|
content (str): The text content to write.
|
|
163
|
-
use_latex (bool): Whether to use LaTeX for rendering.
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
use_latex (bool): Whether to use LaTeX for rendering. Only
|
|
164
|
+
Recommended for documents with mathematical formulas or
|
|
165
|
+
complex typesetting needs. (default: :obj:`False`)
|
|
166
166
|
"""
|
|
167
167
|
# TODO: table generation need to be improved
|
|
168
168
|
if use_latex:
|
|
@@ -439,9 +439,9 @@ class FileWriteToolkit(BaseToolkit):
|
|
|
439
439
|
supplied, it is resolved to self.output_dir.
|
|
440
440
|
encoding (Optional[str]): The character encoding to use. (default:
|
|
441
441
|
:obj: `None`)
|
|
442
|
-
use_latex (bool): For PDF files, whether to use LaTeX rendering
|
|
443
|
-
|
|
444
|
-
|
|
442
|
+
use_latex (bool): For PDF files, whether to use LaTeX rendering.
|
|
443
|
+
Only recommended for documents with mathematical formulas or
|
|
444
|
+
complex typesetting needs. (default: :obj:`False`)
|
|
445
445
|
|
|
446
446
|
Returns:
|
|
447
447
|
str: A message indicating success or error details.
|
|
@@ -56,6 +56,7 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
56
56
|
"visit_page",
|
|
57
57
|
"click",
|
|
58
58
|
"type",
|
|
59
|
+
"enter",
|
|
59
60
|
]
|
|
60
61
|
|
|
61
62
|
# All available tools
|
|
@@ -509,17 +510,19 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
509
510
|
# Public API Methods
|
|
510
511
|
|
|
511
512
|
async def open_browser(
|
|
512
|
-
self, start_url: Optional[str] =
|
|
513
|
+
self, start_url: Optional[str] = "https://search.brave.com/"
|
|
513
514
|
) -> Dict[str, str]:
|
|
514
515
|
r"""Launches a new browser session, making it ready for web automation.
|
|
515
516
|
|
|
516
517
|
This method initializes the underlying browser instance. If a
|
|
517
|
-
`start_url` is provided, it will also navigate to that URL.
|
|
518
|
+
`start_url` is provided, it will also navigate to that URL. If you
|
|
519
|
+
don't have a specific URL to start with, you can use a search engine
|
|
520
|
+
like 'https://search.brave.com/'.
|
|
518
521
|
|
|
519
522
|
Args:
|
|
520
523
|
start_url (Optional[str]): The initial URL to navigate to after the
|
|
521
524
|
browser is launched. If not provided, the browser will start
|
|
522
|
-
with a blank page.
|
|
525
|
+
with a blank page. (default: :obj:`https://search.brave.com/`)
|
|
523
526
|
|
|
524
527
|
Returns:
|
|
525
528
|
Dict[str, str]: A dictionary containing:
|
|
@@ -590,6 +593,9 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
590
593
|
"snapshot": "",
|
|
591
594
|
}
|
|
592
595
|
|
|
596
|
+
if '://' not in url:
|
|
597
|
+
url = f'https://{url}'
|
|
598
|
+
|
|
593
599
|
logger.info(f"Navigating to URL: {url}")
|
|
594
600
|
|
|
595
601
|
# Navigate to page
|
|
@@ -10,18 +10,31 @@
|
|
|
10
10
|
// === Complete snapshot.js logic preservation ===
|
|
11
11
|
|
|
12
12
|
function isVisible(node) {
|
|
13
|
+
// Check if node is null or not a valid DOM node
|
|
14
|
+
if (!node || typeof node.nodeType === 'undefined') return false;
|
|
13
15
|
if (node.nodeType !== Node.ELEMENT_NODE) return true;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const style = window.getComputedStyle(node);
|
|
19
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
|
|
20
|
+
return false;
|
|
21
|
+
// An element with `display: contents` is not rendered itself, but its children are.
|
|
22
|
+
if (style.display === 'contents')
|
|
23
|
+
return true;
|
|
24
|
+
const rect = node.getBoundingClientRect();
|
|
25
|
+
return rect.width > 0 && rect.height > 0;
|
|
26
|
+
} catch (e) {
|
|
27
|
+
// If there's an error getting computed style or bounding rect, assume element is not visible
|
|
16
28
|
return false;
|
|
17
|
-
|
|
18
|
-
if (style.display === 'contents')
|
|
19
|
-
return true;
|
|
20
|
-
const rect = node.getBoundingClientRect();
|
|
21
|
-
return rect.width > 0 && rect.height > 0;
|
|
29
|
+
}
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
function getRole(node) {
|
|
33
|
+
// Check if node is null or doesn't have required properties
|
|
34
|
+
if (!node || !node.tagName || !node.getAttribute) {
|
|
35
|
+
return 'generic';
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
const role = node.getAttribute('role');
|
|
26
39
|
if (role) return role;
|
|
27
40
|
|
|
@@ -39,6 +52,9 @@
|
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
function getAccessibleName(node) {
|
|
55
|
+
// Check if node is null or doesn't have required methods
|
|
56
|
+
if (!node || !node.hasAttribute || !node.getAttribute) return '';
|
|
57
|
+
|
|
42
58
|
if (node.hasAttribute('aria-label')) return node.getAttribute('aria-label') || '';
|
|
43
59
|
if (node.hasAttribute('aria-labelledby')) {
|
|
44
60
|
const id = node.getAttribute('aria-labelledby');
|
|
@@ -55,6 +71,9 @@
|
|
|
55
71
|
|
|
56
72
|
const textCache = new Map();
|
|
57
73
|
function getVisibleTextContent(_node) {
|
|
74
|
+
// Check if node is null or doesn't have nodeType
|
|
75
|
+
if (!_node || typeof _node.nodeType === 'undefined') return '';
|
|
76
|
+
|
|
58
77
|
if (textCache.has(_node)) return textCache.get(_node);
|
|
59
78
|
|
|
60
79
|
if (_node.nodeType === Node.TEXT_NODE) {
|
|
@@ -85,6 +104,9 @@
|
|
|
85
104
|
const visited = new Set();
|
|
86
105
|
|
|
87
106
|
function toAriaNode(element) {
|
|
107
|
+
// Check if element is null or not a valid DOM element
|
|
108
|
+
if (!element || !element.tagName) return null;
|
|
109
|
+
|
|
88
110
|
// Only consider visible elements
|
|
89
111
|
if (!isVisible(element)) return null;
|
|
90
112
|
|
|
@@ -115,7 +137,8 @@
|
|
|
115
137
|
}
|
|
116
138
|
|
|
117
139
|
function traverse(element, parentNode) {
|
|
118
|
-
if
|
|
140
|
+
// Check if element is null or not a valid DOM element
|
|
141
|
+
if (!element || !element.tagName || visited.has(element)) return;
|
|
119
142
|
visited.add(element);
|
|
120
143
|
|
|
121
144
|
// FIX: Completely skip script and style tags and their children.
|