camel-ai 0.2.72a10__py3-none-any.whl → 0.2.73__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 +140 -345
- camel/memories/agent_memories.py +18 -17
- camel/societies/__init__.py +2 -0
- camel/societies/workforce/prompts.py +36 -10
- camel/societies/workforce/single_agent_worker.py +7 -5
- camel/societies/workforce/workforce.py +6 -4
- camel/storages/key_value_storages/mem0_cloud.py +48 -47
- camel/storages/vectordb_storages/__init__.py +1 -0
- camel/storages/vectordb_storages/surreal.py +100 -150
- camel/toolkits/__init__.py +6 -1
- camel/toolkits/base.py +60 -2
- camel/toolkits/excel_toolkit.py +153 -64
- camel/toolkits/file_write_toolkit.py +67 -0
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +136 -413
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +131 -1966
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1177 -0
- 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 +254 -0
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +582 -0
- camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
- camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +447 -0
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2077 -0
- camel/toolkits/mcp_toolkit.py +341 -46
- camel/toolkits/message_integration.py +719 -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/slack_toolkit.py +43 -48
- camel/toolkits/terminal_toolkit.py +288 -46
- camel/toolkits/video_analysis_toolkit.py +13 -13
- camel/toolkits/video_download_toolkit.py +11 -11
- camel/toolkits/web_deploy_toolkit.py +207 -12
- camel/types/enums.py +6 -0
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73.dist-info}/METADATA +49 -9
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73.dist-info}/RECORD +52 -35
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/actions.py +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/agent.py +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/browser_session.py +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/snapshot.py +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/stealth_script.js +0 -0
- /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/unified_analyzer.js +0 -0
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73.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
|
|
camel/toolkits/slack_toolkit.py
CHANGED
|
@@ -128,18 +128,15 @@ class SlackToolkit(BaseToolkit):
|
|
|
128
128
|
return f"Error creating conversation: {e.response['error']}"
|
|
129
129
|
|
|
130
130
|
def join_slack_channel(self, channel_id: str) -> str:
|
|
131
|
-
r"""Joins an existing Slack channel.
|
|
131
|
+
r"""Joins an existing Slack channel. When use this function you must
|
|
132
|
+
call `get_slack_channel_information` function first to get the
|
|
133
|
+
`channel id`.
|
|
132
134
|
|
|
133
135
|
Args:
|
|
134
136
|
channel_id (str): The ID of the Slack channel to join.
|
|
135
137
|
|
|
136
138
|
Returns:
|
|
137
|
-
str: A
|
|
138
|
-
or an error message.
|
|
139
|
-
|
|
140
|
-
Raises:
|
|
141
|
-
SlackApiError: If there is an error during get slack channel
|
|
142
|
-
information.
|
|
139
|
+
str: A string containing the API response from Slack.
|
|
143
140
|
"""
|
|
144
141
|
from slack_sdk.errors import SlackApiError
|
|
145
142
|
|
|
@@ -148,21 +145,18 @@ class SlackToolkit(BaseToolkit):
|
|
|
148
145
|
response = slack_client.conversations_join(channel=channel_id)
|
|
149
146
|
return str(response)
|
|
150
147
|
except SlackApiError as e:
|
|
151
|
-
return f"Error
|
|
148
|
+
return f"Error joining channel: {e.response['error']}"
|
|
152
149
|
|
|
153
150
|
def leave_slack_channel(self, channel_id: str) -> str:
|
|
154
|
-
r"""Leaves an existing Slack channel.
|
|
151
|
+
r"""Leaves an existing Slack channel. When use this function you must
|
|
152
|
+
call `get_slack_channel_information` function first to get the
|
|
153
|
+
`channel id`.
|
|
155
154
|
|
|
156
155
|
Args:
|
|
157
156
|
channel_id (str): The ID of the Slack channel to leave.
|
|
158
157
|
|
|
159
158
|
Returns:
|
|
160
|
-
str: A
|
|
161
|
-
or an error message.
|
|
162
|
-
|
|
163
|
-
Raises:
|
|
164
|
-
SlackApiError: If there is an error during get slack channel
|
|
165
|
-
information.
|
|
159
|
+
str: A string containing the API response from Slack.
|
|
166
160
|
"""
|
|
167
161
|
from slack_sdk.errors import SlackApiError
|
|
168
162
|
|
|
@@ -171,18 +165,18 @@ class SlackToolkit(BaseToolkit):
|
|
|
171
165
|
response = slack_client.conversations_leave(channel=channel_id)
|
|
172
166
|
return str(response)
|
|
173
167
|
except SlackApiError as e:
|
|
174
|
-
return f"Error
|
|
168
|
+
return f"Error leaving channel: {e.response['error']}"
|
|
175
169
|
|
|
176
170
|
def get_slack_channel_information(self) -> str:
|
|
177
|
-
r"""Retrieve
|
|
178
|
-
format.
|
|
171
|
+
r"""Retrieve a list of all public channels in the Slack workspace.
|
|
179
172
|
|
|
180
|
-
|
|
181
|
-
|
|
173
|
+
This function is crucial for discovering available channels and their
|
|
174
|
+
`channel_id`s, which are required by many other functions.
|
|
182
175
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
176
|
+
Returns:
|
|
177
|
+
str: A JSON string representing a list of channels. Each channel
|
|
178
|
+
object in the list contains 'id', 'name', 'created', and
|
|
179
|
+
'num_members'. Returns an error message string on failure.
|
|
186
180
|
"""
|
|
187
181
|
from slack_sdk.errors import SlackApiError
|
|
188
182
|
|
|
@@ -204,21 +198,20 @@ class SlackToolkit(BaseToolkit):
|
|
|
204
198
|
]
|
|
205
199
|
return json.dumps(filtered_result, ensure_ascii=False)
|
|
206
200
|
except SlackApiError as e:
|
|
207
|
-
return f"Error
|
|
201
|
+
return f"Error retrieving channel list: {e.response['error']}"
|
|
208
202
|
|
|
209
203
|
def get_slack_channel_message(self, channel_id: str) -> str:
|
|
210
|
-
r"""Retrieve messages from a Slack channel.
|
|
204
|
+
r"""Retrieve messages from a Slack channel. When use this function you
|
|
205
|
+
must call `get_slack_channel_information` function first to get the
|
|
206
|
+
`channel id`.
|
|
211
207
|
|
|
212
208
|
Args:
|
|
213
209
|
channel_id (str): The ID of the Slack channel to retrieve messages
|
|
214
210
|
from.
|
|
215
211
|
|
|
216
212
|
Returns:
|
|
217
|
-
str: JSON string
|
|
218
|
-
|
|
219
|
-
Raises:
|
|
220
|
-
SlackApiError: If there is an error during get
|
|
221
|
-
slack channel message.
|
|
213
|
+
str: A JSON string representing a list of messages. Each message
|
|
214
|
+
object contains 'user', 'text', and 'ts' (timestamp).
|
|
222
215
|
"""
|
|
223
216
|
from slack_sdk.errors import SlackApiError
|
|
224
217
|
|
|
@@ -242,19 +235,21 @@ class SlackToolkit(BaseToolkit):
|
|
|
242
235
|
file_path: Optional[str] = None,
|
|
243
236
|
user: Optional[str] = None,
|
|
244
237
|
) -> str:
|
|
245
|
-
r"""Send a message to a Slack channel.
|
|
238
|
+
r"""Send a message to a Slack channel. When use this function you must
|
|
239
|
+
call `get_slack_channel_information` function first to get the
|
|
240
|
+
`channel id`.
|
|
246
241
|
|
|
247
242
|
Args:
|
|
248
243
|
message (str): The message to send.
|
|
249
|
-
channel_id (str): The ID of the
|
|
250
|
-
file_path (Optional[str]): The path of
|
|
251
|
-
|
|
252
|
-
user (Optional[str]): The
|
|
253
|
-
|
|
244
|
+
channel_id (str): The ID of the channel to send the message to.
|
|
245
|
+
file_path (Optional[str]): The local path of a file to upload
|
|
246
|
+
with the message.
|
|
247
|
+
user (Optional[str]): The ID of a user to send an ephemeral
|
|
248
|
+
message to (visible only to that user).
|
|
254
249
|
|
|
255
250
|
Returns:
|
|
256
|
-
str: A confirmation message indicating
|
|
257
|
-
|
|
251
|
+
str: A confirmation message indicating success or an error
|
|
252
|
+
message.
|
|
258
253
|
"""
|
|
259
254
|
from slack_sdk.errors import SlackApiError
|
|
260
255
|
|
|
@@ -280,25 +275,25 @@ class SlackToolkit(BaseToolkit):
|
|
|
280
275
|
f"got response: {response}"
|
|
281
276
|
)
|
|
282
277
|
except SlackApiError as e:
|
|
283
|
-
return f"Error
|
|
278
|
+
return f"Error sending message: {e.response['error']}"
|
|
284
279
|
|
|
285
280
|
def delete_slack_message(
|
|
286
281
|
self,
|
|
287
282
|
time_stamp: str,
|
|
288
283
|
channel_id: str,
|
|
289
284
|
) -> str:
|
|
290
|
-
r"""Delete a message
|
|
285
|
+
r"""Delete a message from a Slack channel. When use this function you
|
|
286
|
+
must call `get_slack_channel_information` function first to get the
|
|
287
|
+
`channel id`.
|
|
291
288
|
|
|
292
289
|
Args:
|
|
293
|
-
time_stamp (str):
|
|
294
|
-
|
|
290
|
+
time_stamp (str): The 'ts' value of the message to be deleted.
|
|
291
|
+
You can get this from the `get_slack_channel_message` function.
|
|
292
|
+
channel_id (str): The ID of the channel where the message is. Use
|
|
293
|
+
`get_slack_channel_information` to find the `channel_id`.
|
|
295
294
|
|
|
296
295
|
Returns:
|
|
297
|
-
str: A
|
|
298
|
-
was delete successfully or an error message.
|
|
299
|
-
|
|
300
|
-
Raises:
|
|
301
|
-
SlackApiError: If an error occurs while sending the message.
|
|
296
|
+
str: A string containing the API response from Slack.
|
|
302
297
|
"""
|
|
303
298
|
from slack_sdk.errors import SlackApiError
|
|
304
299
|
|
|
@@ -309,7 +304,7 @@ class SlackToolkit(BaseToolkit):
|
|
|
309
304
|
)
|
|
310
305
|
return str(response)
|
|
311
306
|
except SlackApiError as e:
|
|
312
|
-
return f"Error
|
|
307
|
+
return f"Error deleting message: {e.response['error']}"
|
|
313
308
|
|
|
314
309
|
def get_tools(self) -> List[FunctionTool]:
|
|
315
310
|
r"""Returns a list of FunctionTool objects representing the
|