camel-ai 0.2.72a10__py3-none-any.whl → 0.2.73a1__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 +113 -338
- camel/memories/agent_memories.py +18 -17
- camel/societies/workforce/prompts.py +10 -4
- camel/societies/workforce/single_agent_worker.py +7 -5
- camel/toolkits/__init__.py +6 -1
- camel/toolkits/base.py +57 -1
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +136 -413
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +796 -1631
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4356 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +945 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +522 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +110 -0
- camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +26 -0
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +210 -0
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +533 -0
- camel/toolkits/message_integration.py +592 -0
- camel/toolkits/notion_mcp_toolkit.py +234 -0
- camel/toolkits/screenshot_toolkit.py +116 -31
- camel/toolkits/search_toolkit.py +20 -2
- camel/toolkits/terminal_toolkit.py +16 -2
- camel/toolkits/video_analysis_toolkit.py +13 -13
- camel/toolkits/video_download_toolkit.py +11 -11
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73a1.dist-info}/METADATA +12 -6
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73a1.dist-info}/RECORD +31 -24
- camel/toolkits/hybrid_browser_toolkit/actions.py +0 -417
- camel/toolkits/hybrid_browser_toolkit/agent.py +0 -311
- camel/toolkits/hybrid_browser_toolkit/browser_session.py +0 -740
- camel/toolkits/hybrid_browser_toolkit/snapshot.py +0 -227
- camel/toolkits/hybrid_browser_toolkit/stealth_script.js +0 -0
- camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +0 -1002
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73a1.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73a1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
|
|
15
|
+
from typing import Any, ClassVar, Dict, List, Optional, Set
|
|
16
|
+
|
|
17
|
+
from camel.toolkits import BaseToolkit, FunctionTool
|
|
18
|
+
|
|
19
|
+
from .mcp_toolkit import MCPToolkit
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NotionMCPToolkit(BaseToolkit):
|
|
23
|
+
r"""NotionMCPToolkit provides an interface for interacting with Notion
|
|
24
|
+
through the Model Context Protocol (MCP).
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
timeout (Optional[float]): Connection timeout in seconds.
|
|
28
|
+
(default: :obj:`None`)
|
|
29
|
+
|
|
30
|
+
Note:
|
|
31
|
+
Currently only supports asynchronous operation mode.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# TODO: Create unified method to validate and fix the schema
|
|
35
|
+
SCHEMA_KEYWORDS: ClassVar[Set[str]] = {
|
|
36
|
+
"type",
|
|
37
|
+
"properties",
|
|
38
|
+
"items",
|
|
39
|
+
"required",
|
|
40
|
+
"additionalProperties",
|
|
41
|
+
"description",
|
|
42
|
+
"title",
|
|
43
|
+
"default",
|
|
44
|
+
"enum",
|
|
45
|
+
"const",
|
|
46
|
+
"examples",
|
|
47
|
+
"$ref",
|
|
48
|
+
"$defs",
|
|
49
|
+
"definitions",
|
|
50
|
+
"allOf",
|
|
51
|
+
"oneOf",
|
|
52
|
+
"anyOf",
|
|
53
|
+
"not",
|
|
54
|
+
"if",
|
|
55
|
+
"then",
|
|
56
|
+
"else",
|
|
57
|
+
"format",
|
|
58
|
+
"pattern",
|
|
59
|
+
"minimum",
|
|
60
|
+
"maximum",
|
|
61
|
+
"minLength",
|
|
62
|
+
"maxLength",
|
|
63
|
+
"minItems",
|
|
64
|
+
"maxItems",
|
|
65
|
+
"uniqueItems",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
timeout: Optional[float] = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
r"""Initializes the NotionMCPToolkit.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
timeout (Optional[float]): Connection timeout in seconds.
|
|
76
|
+
(default: :obj:`None`)
|
|
77
|
+
"""
|
|
78
|
+
super().__init__(timeout=timeout)
|
|
79
|
+
|
|
80
|
+
self._mcp_toolkit = MCPToolkit(
|
|
81
|
+
config_dict={
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"notionMCP": {
|
|
84
|
+
"command": "npx",
|
|
85
|
+
"args": [
|
|
86
|
+
"-y",
|
|
87
|
+
"mcp-remote",
|
|
88
|
+
"https://mcp.notion.com/mcp",
|
|
89
|
+
],
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
timeout=timeout,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def connect(self):
|
|
97
|
+
r"""Explicitly connect to the Notion MCP server."""
|
|
98
|
+
await self._mcp_toolkit.connect()
|
|
99
|
+
|
|
100
|
+
async def disconnect(self):
|
|
101
|
+
r"""Explicitly disconnect from the Notion MCP server."""
|
|
102
|
+
await self._mcp_toolkit.disconnect()
|
|
103
|
+
|
|
104
|
+
def get_tools(self) -> List[FunctionTool]:
|
|
105
|
+
r"""Returns a list of tools provided by the NotionMCPToolkit.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
List[FunctionTool]: List of available tools.
|
|
109
|
+
"""
|
|
110
|
+
all_tools = []
|
|
111
|
+
for client in self._mcp_toolkit.clients:
|
|
112
|
+
try:
|
|
113
|
+
original_build_schema = client._build_tool_schema
|
|
114
|
+
|
|
115
|
+
def create_wrapper(orig_func):
|
|
116
|
+
def wrapper(mcp_tool):
|
|
117
|
+
return self._build_notion_tool_schema(
|
|
118
|
+
mcp_tool, orig_func
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return wrapper
|
|
122
|
+
|
|
123
|
+
client._build_tool_schema = create_wrapper( # type: ignore[method-assign]
|
|
124
|
+
original_build_schema
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
client_tools = client.get_tools()
|
|
128
|
+
all_tools.extend(client_tools)
|
|
129
|
+
|
|
130
|
+
client._build_tool_schema = original_build_schema # type: ignore[method-assign]
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
from camel.logger import get_logger
|
|
134
|
+
|
|
135
|
+
logger = get_logger(__name__)
|
|
136
|
+
logger.error(f"Failed to get tools from client: {e}")
|
|
137
|
+
|
|
138
|
+
return all_tools
|
|
139
|
+
|
|
140
|
+
def _build_notion_tool_schema(self, mcp_tool, original_build_schema):
|
|
141
|
+
r"""Build tool schema with Notion-specific fixes."""
|
|
142
|
+
schema = original_build_schema(mcp_tool)
|
|
143
|
+
self._fix_notion_schema_recursively(schema)
|
|
144
|
+
return schema
|
|
145
|
+
|
|
146
|
+
def _fix_notion_schema_recursively(self, obj: Any) -> None:
|
|
147
|
+
r"""Recursively fix Notion MCP schema issues."""
|
|
148
|
+
if isinstance(obj, dict):
|
|
149
|
+
self._fix_dict_schema(obj)
|
|
150
|
+
self._process_nested_structures(obj)
|
|
151
|
+
elif isinstance(obj, list):
|
|
152
|
+
for item in obj:
|
|
153
|
+
self._fix_notion_schema_recursively(item)
|
|
154
|
+
|
|
155
|
+
def _fix_dict_schema(self, obj: Dict[str, Any]) -> None:
|
|
156
|
+
r"""Fix dictionary schema issues."""
|
|
157
|
+
if "properties" in obj and "type" not in obj:
|
|
158
|
+
self._fix_missing_type_with_properties(obj)
|
|
159
|
+
elif obj.get("type") == "object" and "properties" in obj:
|
|
160
|
+
self._fix_object_with_properties(obj)
|
|
161
|
+
|
|
162
|
+
def _fix_missing_type_with_properties(self, obj: Dict[str, Any]) -> None:
|
|
163
|
+
r"""Fix objects with properties but missing type field."""
|
|
164
|
+
properties = obj.get("properties", {})
|
|
165
|
+
if properties and isinstance(properties, dict):
|
|
166
|
+
obj["type"] = "object"
|
|
167
|
+
obj["additionalProperties"] = False
|
|
168
|
+
|
|
169
|
+
required_properties = self._get_required_properties(
|
|
170
|
+
properties, conservative=True
|
|
171
|
+
)
|
|
172
|
+
if required_properties:
|
|
173
|
+
obj["required"] = required_properties
|
|
174
|
+
|
|
175
|
+
def _fix_object_with_properties(self, obj: Dict[str, Any]) -> None:
|
|
176
|
+
r"""Fix objects with type="object" and properties."""
|
|
177
|
+
properties = obj.get("properties", {})
|
|
178
|
+
if properties and isinstance(properties, dict):
|
|
179
|
+
existing_required = obj.get("required", [])
|
|
180
|
+
|
|
181
|
+
for prop_name, prop_schema in properties.items():
|
|
182
|
+
if (
|
|
183
|
+
prop_name not in existing_required
|
|
184
|
+
and prop_name not in self.SCHEMA_KEYWORDS
|
|
185
|
+
and self._is_property_required(prop_schema)
|
|
186
|
+
):
|
|
187
|
+
existing_required.append(prop_name)
|
|
188
|
+
|
|
189
|
+
if existing_required:
|
|
190
|
+
obj["required"] = existing_required
|
|
191
|
+
|
|
192
|
+
if "additionalProperties" not in obj:
|
|
193
|
+
obj["additionalProperties"] = False
|
|
194
|
+
|
|
195
|
+
def _get_required_properties(
|
|
196
|
+
self, properties: Dict[str, Any], conservative: bool = False
|
|
197
|
+
) -> List[str]:
|
|
198
|
+
r"""Get list of required properties from a properties dict."""
|
|
199
|
+
required = []
|
|
200
|
+
for prop_name, prop_schema in properties.items():
|
|
201
|
+
if (
|
|
202
|
+
prop_name not in self.SCHEMA_KEYWORDS
|
|
203
|
+
and isinstance(prop_schema, dict)
|
|
204
|
+
and self._is_property_required(prop_schema)
|
|
205
|
+
):
|
|
206
|
+
required.append(prop_name)
|
|
207
|
+
return required
|
|
208
|
+
|
|
209
|
+
def _is_property_required(self, prop_schema: Dict[str, Any]) -> bool:
|
|
210
|
+
r"""Check if a property should be marked as required."""
|
|
211
|
+
prop_type = prop_schema.get("type")
|
|
212
|
+
return (
|
|
213
|
+
prop_type is not None
|
|
214
|
+
and prop_type != "null"
|
|
215
|
+
and "default" not in prop_schema
|
|
216
|
+
and not (isinstance(prop_type, list) and "null" in prop_type)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def _process_nested_structures(self, obj: Dict[str, Any]) -> None:
|
|
220
|
+
r"""Process all nested structures in a schema object."""
|
|
221
|
+
for key, value in obj.items():
|
|
222
|
+
if key in ["anyOf", "oneOf", "allOf"] and isinstance(value, list):
|
|
223
|
+
for item in value:
|
|
224
|
+
self._fix_notion_schema_recursively(item)
|
|
225
|
+
elif key == "items" and isinstance(value, dict):
|
|
226
|
+
self._fix_notion_schema_recursively(value)
|
|
227
|
+
elif key == "properties" and isinstance(value, dict):
|
|
228
|
+
for prop_value in value.values():
|
|
229
|
+
self._fix_notion_schema_recursively(prop_value)
|
|
230
|
+
elif key == "$defs" and isinstance(value, dict):
|
|
231
|
+
for def_value in value.values():
|
|
232
|
+
self._fix_notion_schema_recursively(def_value)
|
|
233
|
+
elif isinstance(value, (dict, list)):
|
|
234
|
+
self._fix_notion_schema_recursively(value)
|
|
@@ -12,22 +12,22 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
14
|
|
|
15
|
-
import base64
|
|
16
|
-
import io
|
|
17
15
|
import os
|
|
18
|
-
import time
|
|
19
16
|
from pathlib import Path
|
|
20
17
|
from typing import List, Optional
|
|
21
18
|
|
|
19
|
+
from PIL import Image
|
|
20
|
+
|
|
22
21
|
from camel.logger import get_logger
|
|
22
|
+
from camel.messages import BaseMessage
|
|
23
23
|
from camel.toolkits import BaseToolkit, FunctionTool
|
|
24
|
+
from camel.toolkits.base import RegisteredAgentToolkit
|
|
24
25
|
from camel.utils import dependencies_required
|
|
25
|
-
from camel.utils.tool_result import ToolResult
|
|
26
26
|
|
|
27
27
|
logger = get_logger(__name__)
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
class ScreenshotToolkit(BaseToolkit):
|
|
30
|
+
class ScreenshotToolkit(BaseToolkit, RegisteredAgentToolkit):
|
|
31
31
|
r"""A toolkit for taking screenshots."""
|
|
32
32
|
|
|
33
33
|
@dependencies_required('PIL')
|
|
@@ -50,6 +50,7 @@ class ScreenshotToolkit(BaseToolkit):
|
|
|
50
50
|
from PIL import ImageGrab
|
|
51
51
|
|
|
52
52
|
super().__init__(timeout=timeout)
|
|
53
|
+
RegisteredAgentToolkit.__init__(self)
|
|
53
54
|
|
|
54
55
|
camel_workdir = os.environ.get("CAMEL_WORKDIR")
|
|
55
56
|
if working_directory:
|
|
@@ -60,25 +61,101 @@ class ScreenshotToolkit(BaseToolkit):
|
|
|
60
61
|
path = Path("camel_working_dir")
|
|
61
62
|
|
|
62
63
|
self.ImageGrab = ImageGrab
|
|
63
|
-
self.screenshots_dir = path
|
|
64
|
+
self.screenshots_dir = path
|
|
64
65
|
self.screenshots_dir.mkdir(parents=True, exist_ok=True)
|
|
65
66
|
|
|
66
|
-
def
|
|
67
|
+
def read_image(
|
|
68
|
+
self,
|
|
69
|
+
image_path: str,
|
|
70
|
+
instruction: str = "",
|
|
71
|
+
) -> str:
|
|
72
|
+
r"""Analyzes an image from a local file path.
|
|
73
|
+
|
|
74
|
+
This function enables you to "see" and interpret an image from a
|
|
75
|
+
file. It's useful for tasks where you need to understand visual
|
|
76
|
+
information, such as reading a screenshot of a webpage or a diagram.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
image_path (str): The local file path to the image.
|
|
80
|
+
For example: 'screenshots/login_page.png'.
|
|
81
|
+
instruction (str, optional): Specific instructions for what to look
|
|
82
|
+
for or what to do with the image. For example: "What is the
|
|
83
|
+
main headline on this page?" or "Find the 'Submit' button.".
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
str: The response after analyzing the image, which could be a
|
|
87
|
+
description, an answer, or a confirmation of an action.
|
|
88
|
+
"""
|
|
89
|
+
if self.agent is None:
|
|
90
|
+
logger.error(
|
|
91
|
+
"Cannot record screenshot in memory: No agent registered. "
|
|
92
|
+
"Please pass this toolkit to ChatAgent via "
|
|
93
|
+
"toolkits_to_register_agent parameter."
|
|
94
|
+
)
|
|
95
|
+
return (
|
|
96
|
+
"Error: No agent registered. Please pass this toolkit to "
|
|
97
|
+
"ChatAgent via toolkits_to_register_agent parameter."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
image_path = str(Path(image_path).absolute())
|
|
102
|
+
|
|
103
|
+
# Check if file exists before trying to open
|
|
104
|
+
if not os.path.exists(image_path):
|
|
105
|
+
error_msg = f"Screenshot file not found: {image_path}"
|
|
106
|
+
logger.error(error_msg)
|
|
107
|
+
return f"Error: {error_msg}"
|
|
108
|
+
|
|
109
|
+
# Load the image from the path
|
|
110
|
+
img = Image.open(image_path)
|
|
111
|
+
|
|
112
|
+
# Create a message with the screenshot image
|
|
113
|
+
message = BaseMessage.make_user_message(
|
|
114
|
+
role_name="User",
|
|
115
|
+
content=instruction,
|
|
116
|
+
image_list=[img],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Record the message in agent's memory
|
|
120
|
+
response = self.agent.step(message)
|
|
121
|
+
return response.msgs[0].content
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(f"Error reading screenshot: {e}")
|
|
125
|
+
return f"Error reading screenshot: {e}"
|
|
126
|
+
|
|
127
|
+
def take_screenshot_and_read_image(
|
|
67
128
|
self,
|
|
129
|
+
filename: str,
|
|
68
130
|
save_to_file: bool = True,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
131
|
+
read_image: bool = True,
|
|
132
|
+
instruction: Optional[str] = None,
|
|
133
|
+
) -> str:
|
|
134
|
+
r"""Captures a screenshot of the entire screen.
|
|
135
|
+
|
|
136
|
+
This function can save the screenshot to a file and optionally analyze
|
|
137
|
+
it. It's useful for capturing the current state of the UI for
|
|
138
|
+
documentation, analysis, or to guide subsequent actions.
|
|
72
139
|
|
|
73
140
|
Args:
|
|
74
|
-
|
|
141
|
+
filename (str): The name for the screenshot file (e.g.,
|
|
142
|
+
"homepage.png"). The file is saved in a `screenshots`
|
|
143
|
+
subdirectory within the working directory. Must end with
|
|
144
|
+
`.png`. (default: :obj:`None`)
|
|
145
|
+
save_to_file (bool, optional): If `True`, saves the screenshot to
|
|
146
|
+
a file. (default: :obj:`True`)
|
|
147
|
+
read_image (bool, optional): If `True`, the agent will analyze
|
|
148
|
+
the screenshot. `save_to_file` must also be `True`.
|
|
75
149
|
(default: :obj:`True`)
|
|
150
|
+
instruction (Optional[str], optional): A specific question or
|
|
151
|
+
command for the agent regarding the screenshot, used only if
|
|
152
|
+
`read_image` is `True`. For example: "Confirm that the
|
|
153
|
+
user is logged in.".
|
|
76
154
|
|
|
77
155
|
Returns:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
PNG image data URL.
|
|
156
|
+
str: A confirmation message indicating success or failure,
|
|
157
|
+
including the file path if saved, and the agent's response
|
|
158
|
+
if `read_image` is `True`.
|
|
82
159
|
"""
|
|
83
160
|
try:
|
|
84
161
|
# Take screenshot of entire screen
|
|
@@ -90,32 +167,39 @@ class ScreenshotToolkit(BaseToolkit):
|
|
|
90
167
|
# Create directory if it doesn't exist
|
|
91
168
|
os.makedirs(self.screenshots_dir, exist_ok=True)
|
|
92
169
|
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
170
|
+
# Create unique filename if file already exists
|
|
171
|
+
base_path = os.path.join(self.screenshots_dir, filename)
|
|
172
|
+
file_path = base_path
|
|
173
|
+
counter = 1
|
|
174
|
+
while os.path.exists(file_path):
|
|
175
|
+
name, ext = os.path.splitext(filename)
|
|
176
|
+
unique_filename = f"{name}_{counter}{ext}"
|
|
177
|
+
file_path = os.path.join(
|
|
178
|
+
self.screenshots_dir, unique_filename
|
|
179
|
+
)
|
|
180
|
+
counter += 1
|
|
181
|
+
|
|
97
182
|
screenshot.save(file_path)
|
|
98
183
|
logger.info(f"Screenshot saved to {file_path}")
|
|
99
184
|
|
|
100
|
-
# Convert to base64
|
|
101
|
-
img_buffer = io.BytesIO()
|
|
102
|
-
screenshot.save(img_buffer, format="PNG")
|
|
103
|
-
img_buffer.seek(0)
|
|
104
|
-
img_base64 = base64.b64encode(img_buffer.getvalue()).decode(
|
|
105
|
-
'utf-8'
|
|
106
|
-
)
|
|
107
|
-
img_data_url = f"data:image/png;base64,{img_base64}"
|
|
108
|
-
|
|
109
185
|
# Create result text
|
|
110
186
|
result_text = "Screenshot captured successfully"
|
|
111
187
|
if file_path:
|
|
112
188
|
result_text += f" and saved to {file_path}"
|
|
113
189
|
|
|
114
|
-
|
|
190
|
+
# Record in agent memory if requested
|
|
191
|
+
if read_image and file_path is not None:
|
|
192
|
+
inst = instruction if instruction is not None else ""
|
|
193
|
+
response = self.read_image(
|
|
194
|
+
str(Path(file_path).absolute()), inst
|
|
195
|
+
)
|
|
196
|
+
result_text += f". Agent response: {response}"
|
|
197
|
+
|
|
198
|
+
return result_text
|
|
115
199
|
|
|
116
200
|
except Exception as e:
|
|
117
201
|
logger.error(f"Error taking screenshot: {e}")
|
|
118
|
-
return
|
|
202
|
+
return f"Error taking screenshot: {e}"
|
|
119
203
|
|
|
120
204
|
def get_tools(self) -> List[FunctionTool]:
|
|
121
205
|
r"""Returns a list of FunctionTool objects for screenshot operations.
|
|
@@ -124,5 +208,6 @@ class ScreenshotToolkit(BaseToolkit):
|
|
|
124
208
|
List[FunctionTool]: List of screenshot functions.
|
|
125
209
|
"""
|
|
126
210
|
return [
|
|
127
|
-
FunctionTool(self.
|
|
211
|
+
FunctionTool(self.take_screenshot_and_read_image),
|
|
212
|
+
FunctionTool(self.read_image),
|
|
128
213
|
]
|
camel/toolkits/search_toolkit.py
CHANGED
|
@@ -36,6 +36,7 @@ class SearchToolkit(BaseToolkit):
|
|
|
36
36
|
self,
|
|
37
37
|
timeout: Optional[float] = None,
|
|
38
38
|
number_of_result_pages: int = 10,
|
|
39
|
+
exclude_domains: Optional[List[str]] = None,
|
|
39
40
|
):
|
|
40
41
|
r"""Initializes the RedditToolkit with the specified number of retries
|
|
41
42
|
and delay.
|
|
@@ -45,9 +46,14 @@ class SearchToolkit(BaseToolkit):
|
|
|
45
46
|
(default: :obj:`None`)
|
|
46
47
|
number_of_result_pages (int): The number of result pages to
|
|
47
48
|
retrieve. (default: :obj:`10`)
|
|
49
|
+
exclude_domains (Optional[List[str]]): List of domains to
|
|
50
|
+
exclude from search results. Currently only supported
|
|
51
|
+
by the `search_google` function.
|
|
52
|
+
(default: :obj:`None`)
|
|
48
53
|
"""
|
|
49
54
|
super().__init__(timeout=timeout)
|
|
50
55
|
self.number_of_result_pages = number_of_result_pages
|
|
56
|
+
self.exclude_domains = exclude_domains
|
|
51
57
|
|
|
52
58
|
@dependencies_required("wikipedia")
|
|
53
59
|
def search_wiki(self, entity: str) -> str:
|
|
@@ -435,7 +441,9 @@ class SearchToolkit(BaseToolkit):
|
|
|
435
441
|
]
|
|
436
442
|
)
|
|
437
443
|
def search_google(
|
|
438
|
-
self,
|
|
444
|
+
self,
|
|
445
|
+
query: str,
|
|
446
|
+
search_type: str = "web",
|
|
439
447
|
) -> List[Dict[str, Any]]:
|
|
440
448
|
r"""Use Google search engine to search information for the given query.
|
|
441
449
|
|
|
@@ -499,11 +507,21 @@ class SearchToolkit(BaseToolkit):
|
|
|
499
507
|
start_page_idx = 1
|
|
500
508
|
# Different language may get different result
|
|
501
509
|
search_language = "en"
|
|
510
|
+
|
|
511
|
+
modified_query = query
|
|
512
|
+
if self.exclude_domains:
|
|
513
|
+
# Use Google's -site: operator to exclude domains
|
|
514
|
+
exclusion_terms = " ".join(
|
|
515
|
+
[f"-site:{domain}" for domain in self.exclude_domains]
|
|
516
|
+
)
|
|
517
|
+
modified_query = f"{query} {exclusion_terms}"
|
|
518
|
+
logger.debug(f"Excluded domains, modified query: {modified_query}")
|
|
519
|
+
|
|
502
520
|
# Constructing the URL
|
|
503
521
|
# Doc: https://developers.google.com/custom-search/v1/using_rest
|
|
504
522
|
base_url = (
|
|
505
523
|
f"https://www.googleapis.com/customsearch/v1?"
|
|
506
|
-
f"key={GOOGLE_API_KEY}&cx={SEARCH_ENGINE_ID}&q={
|
|
524
|
+
f"key={GOOGLE_API_KEY}&cx={SEARCH_ENGINE_ID}&q={modified_query}&start="
|
|
507
525
|
f"{start_page_idx}&lr={search_language}&num={self.number_of_result_pages}"
|
|
508
526
|
)
|
|
509
527
|
|
|
@@ -319,7 +319,14 @@ class TerminalToolkit(BaseToolkit):
|
|
|
319
319
|
)
|
|
320
320
|
|
|
321
321
|
# Install essential packages using uv
|
|
322
|
-
essential_packages = [
|
|
322
|
+
essential_packages = [
|
|
323
|
+
"pip",
|
|
324
|
+
"setuptools",
|
|
325
|
+
"wheel",
|
|
326
|
+
"pyautogui",
|
|
327
|
+
"plotly",
|
|
328
|
+
"ffmpeg",
|
|
329
|
+
]
|
|
323
330
|
subprocess.run(
|
|
324
331
|
[
|
|
325
332
|
"uv",
|
|
@@ -371,7 +378,14 @@ class TerminalToolkit(BaseToolkit):
|
|
|
371
378
|
pip_path = os.path.join(self.initial_env_path, "bin", "pip")
|
|
372
379
|
|
|
373
380
|
# Upgrade pip and install essential packages
|
|
374
|
-
essential_packages = [
|
|
381
|
+
essential_packages = [
|
|
382
|
+
"pip",
|
|
383
|
+
"setuptools",
|
|
384
|
+
"wheel",
|
|
385
|
+
"pyautogui",
|
|
386
|
+
"plotly",
|
|
387
|
+
"ffmpeg",
|
|
388
|
+
]
|
|
375
389
|
subprocess.run(
|
|
376
390
|
[pip_path, "install", "--upgrade", *essential_packages],
|
|
377
391
|
check=True,
|
|
@@ -97,7 +97,7 @@ class VideoAnalysisToolkit(BaseToolkit):
|
|
|
97
97
|
r"""A class for analysing videos with vision-language model.
|
|
98
98
|
|
|
99
99
|
Args:
|
|
100
|
-
|
|
100
|
+
working_directory (Optional[str], optional): The directory where the
|
|
101
101
|
video will be downloaded to. If not provided, video will be stored
|
|
102
102
|
in a temporary directory and will be cleaned up after use.
|
|
103
103
|
(default: :obj:`None`)
|
|
@@ -123,7 +123,7 @@ class VideoAnalysisToolkit(BaseToolkit):
|
|
|
123
123
|
@dependencies_required("ffmpeg", "scenedetect")
|
|
124
124
|
def __init__(
|
|
125
125
|
self,
|
|
126
|
-
|
|
126
|
+
working_directory: Optional[str] = None,
|
|
127
127
|
model: Optional[BaseModelBackend] = None,
|
|
128
128
|
use_audio_transcription: bool = False,
|
|
129
129
|
use_ocr: bool = False,
|
|
@@ -133,30 +133,30 @@ class VideoAnalysisToolkit(BaseToolkit):
|
|
|
133
133
|
timeout: Optional[float] = None,
|
|
134
134
|
) -> None:
|
|
135
135
|
super().__init__(timeout=timeout)
|
|
136
|
-
self._cleanup =
|
|
136
|
+
self._cleanup = working_directory is None
|
|
137
137
|
self._temp_files: list[str] = [] # Track temporary files for cleanup
|
|
138
138
|
self._use_audio_transcription = use_audio_transcription
|
|
139
139
|
self._use_ocr = use_ocr
|
|
140
140
|
self.output_language = output_language
|
|
141
141
|
self.frame_interval = frame_interval
|
|
142
142
|
|
|
143
|
-
self.
|
|
144
|
-
|
|
143
|
+
self._working_directory = Path(
|
|
144
|
+
working_directory or tempfile.mkdtemp()
|
|
145
145
|
).resolve()
|
|
146
146
|
|
|
147
147
|
self.video_downloader_toolkit = VideoDownloaderToolkit(
|
|
148
|
-
|
|
148
|
+
working_directory=str(self._working_directory),
|
|
149
149
|
cookies_path=cookies_path,
|
|
150
150
|
)
|
|
151
151
|
|
|
152
152
|
try:
|
|
153
|
-
self.
|
|
153
|
+
self._working_directory.mkdir(parents=True, exist_ok=True)
|
|
154
154
|
except OSError as e:
|
|
155
155
|
raise ValueError(
|
|
156
|
-
f"Error creating directory {self.
|
|
156
|
+
f"Error creating directory {self._working_directory}: {e}"
|
|
157
157
|
)
|
|
158
158
|
|
|
159
|
-
logger.info(f"Video will be downloaded to {self.
|
|
159
|
+
logger.info(f"Video will be downloaded to {self._working_directory}")
|
|
160
160
|
|
|
161
161
|
self.vl_model = model
|
|
162
162
|
# Ensure ChatAgent is initialized with a model if provided
|
|
@@ -206,16 +206,16 @@ class VideoAnalysisToolkit(BaseToolkit):
|
|
|
206
206
|
)
|
|
207
207
|
|
|
208
208
|
# Clean up temporary directory if needed
|
|
209
|
-
if self._cleanup and os.path.exists(self.
|
|
209
|
+
if self._cleanup and os.path.exists(self._working_directory):
|
|
210
210
|
try:
|
|
211
211
|
import sys
|
|
212
212
|
|
|
213
213
|
if getattr(sys, 'modules', None) is not None:
|
|
214
214
|
import shutil
|
|
215
215
|
|
|
216
|
-
shutil.rmtree(self.
|
|
216
|
+
shutil.rmtree(self._working_directory)
|
|
217
217
|
logger.debug(
|
|
218
|
-
f"Removed temp directory: {self.
|
|
218
|
+
f"Removed temp directory: {self._working_directory}"
|
|
219
219
|
)
|
|
220
220
|
except (ImportError, AttributeError):
|
|
221
221
|
# Skip cleanup if interpreter is shutting down
|
|
@@ -223,7 +223,7 @@ class VideoAnalysisToolkit(BaseToolkit):
|
|
|
223
223
|
except OSError as e:
|
|
224
224
|
logger.warning(
|
|
225
225
|
f"Failed to remove temporary directory "
|
|
226
|
-
f"{self.
|
|
226
|
+
f"{self._working_directory}: {e}"
|
|
227
227
|
)
|
|
228
228
|
|
|
229
229
|
@dependencies_required("pytesseract", "cv2", "numpy")
|
|
@@ -62,7 +62,7 @@ class VideoDownloaderToolkit(BaseToolkit):
|
|
|
62
62
|
chunks.
|
|
63
63
|
|
|
64
64
|
Args:
|
|
65
|
-
|
|
65
|
+
working_directory (Optional[str], optional): The directory where the
|
|
66
66
|
video will be downloaded to. If not provided, video will be stored
|
|
67
67
|
in a temporary directory and will be cleaned up after use.
|
|
68
68
|
(default: :obj:`None`)
|
|
@@ -73,30 +73,30 @@ class VideoDownloaderToolkit(BaseToolkit):
|
|
|
73
73
|
@dependencies_required("yt_dlp", "ffmpeg")
|
|
74
74
|
def __init__(
|
|
75
75
|
self,
|
|
76
|
-
|
|
76
|
+
working_directory: Optional[str] = None,
|
|
77
77
|
cookies_path: Optional[str] = None,
|
|
78
78
|
timeout: Optional[float] = None,
|
|
79
79
|
) -> None:
|
|
80
80
|
super().__init__(timeout=timeout)
|
|
81
|
-
self._cleanup =
|
|
81
|
+
self._cleanup = working_directory is None
|
|
82
82
|
self._cookies_path = cookies_path
|
|
83
83
|
|
|
84
|
-
self.
|
|
85
|
-
|
|
84
|
+
self._working_directory = Path(
|
|
85
|
+
working_directory or tempfile.mkdtemp()
|
|
86
86
|
).resolve()
|
|
87
87
|
|
|
88
88
|
try:
|
|
89
|
-
self.
|
|
89
|
+
self._working_directory.mkdir(parents=True, exist_ok=True)
|
|
90
90
|
except FileExistsError:
|
|
91
91
|
raise ValueError(
|
|
92
|
-
f"{self.
|
|
92
|
+
f"{self._working_directory} is not a valid directory."
|
|
93
93
|
)
|
|
94
94
|
except OSError as e:
|
|
95
95
|
raise ValueError(
|
|
96
|
-
f"Error creating directory {self.
|
|
96
|
+
f"Error creating directory {self._working_directory}: {e}"
|
|
97
97
|
)
|
|
98
98
|
|
|
99
|
-
logger.info(f"Video will be downloaded to {self.
|
|
99
|
+
logger.info(f"Video will be downloaded to {self._working_directory}")
|
|
100
100
|
|
|
101
101
|
def __del__(self) -> None:
|
|
102
102
|
r"""Deconstructor for the VideoDownloaderToolkit class.
|
|
@@ -111,7 +111,7 @@ class VideoDownloaderToolkit(BaseToolkit):
|
|
|
111
111
|
if getattr(sys, 'modules', None) is not None:
|
|
112
112
|
import shutil
|
|
113
113
|
|
|
114
|
-
shutil.rmtree(self.
|
|
114
|
+
shutil.rmtree(self._working_directory, ignore_errors=True)
|
|
115
115
|
except (ImportError, AttributeError):
|
|
116
116
|
# Skip cleanup if interpreter is shutting down
|
|
117
117
|
pass
|
|
@@ -130,7 +130,7 @@ class VideoDownloaderToolkit(BaseToolkit):
|
|
|
130
130
|
"""
|
|
131
131
|
import yt_dlp
|
|
132
132
|
|
|
133
|
-
video_template = self.
|
|
133
|
+
video_template = self._working_directory / "%(title)s.%(ext)s"
|
|
134
134
|
ydl_opts = {
|
|
135
135
|
'format': 'bestvideo+bestaudio/best',
|
|
136
136
|
'outtmpl': str(video_template),
|