camel-ai 0.2.72a8__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.

Files changed (53) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +140 -345
  3. camel/memories/agent_memories.py +18 -17
  4. camel/societies/__init__.py +2 -0
  5. camel/societies/workforce/prompts.py +36 -10
  6. camel/societies/workforce/single_agent_worker.py +7 -5
  7. camel/societies/workforce/workforce.py +6 -4
  8. camel/storages/key_value_storages/mem0_cloud.py +48 -47
  9. camel/storages/vectordb_storages/__init__.py +1 -0
  10. camel/storages/vectordb_storages/surreal.py +100 -150
  11. camel/toolkits/__init__.py +6 -1
  12. camel/toolkits/base.py +60 -2
  13. camel/toolkits/excel_toolkit.py +153 -64
  14. camel/toolkits/file_write_toolkit.py +67 -0
  15. camel/toolkits/hybrid_browser_toolkit/config_loader.py +136 -413
  16. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +131 -1966
  17. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1177 -0
  18. camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4356 -0
  19. camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
  20. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
  21. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +945 -0
  22. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +226 -0
  23. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +522 -0
  24. camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
  25. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +110 -0
  26. camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +26 -0
  27. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +254 -0
  28. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +582 -0
  29. camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
  30. camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +447 -0
  31. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2077 -0
  32. camel/toolkits/mcp_toolkit.py +341 -46
  33. camel/toolkits/message_integration.py +719 -0
  34. camel/toolkits/note_taking_toolkit.py +18 -29
  35. camel/toolkits/notion_mcp_toolkit.py +234 -0
  36. camel/toolkits/screenshot_toolkit.py +116 -31
  37. camel/toolkits/search_toolkit.py +20 -2
  38. camel/toolkits/slack_toolkit.py +43 -48
  39. camel/toolkits/terminal_toolkit.py +288 -46
  40. camel/toolkits/video_analysis_toolkit.py +13 -13
  41. camel/toolkits/video_download_toolkit.py +11 -11
  42. camel/toolkits/web_deploy_toolkit.py +207 -12
  43. camel/types/enums.py +6 -0
  44. {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/METADATA +49 -9
  45. {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/RECORD +53 -36
  46. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/actions.py +0 -0
  47. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/agent.py +0 -0
  48. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/browser_session.py +0 -0
  49. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/snapshot.py +0 -0
  50. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/stealth_script.js +0 -0
  51. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/unified_analyzer.js +0 -0
  52. {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/WHEEL +0 -0
  53. {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73.dist-info}/licenses/LICENSE +0 -0
@@ -11,7 +11,6 @@
11
11
  # See the License for the specific language governing permissions and
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
- import fcntl
15
14
  import os
16
15
  import time
17
16
  from pathlib import Path
@@ -88,58 +87,48 @@ class NoteTakingToolkit(BaseToolkit):
88
87
  return f"Error appending note: {e}"
89
88
 
90
89
  def _load_registry(self) -> None:
91
- r"""Load the note registry from file with file locking."""
90
+ r"""Load the note registry from file."""
92
91
  max_retries = 5
93
92
  retry_delay = 0.1
94
93
 
95
94
  for attempt in range(max_retries):
96
95
  try:
97
96
  if self.registry_file.exists():
98
- with open(self.registry_file, 'r') as f:
99
- fcntl.flock(f.fileno(), fcntl.LOCK_SH)
100
- try:
101
- content = f.read().strip()
102
- self.registry = (
103
- content.split('\n') if content else []
104
- )
105
- finally:
106
- fcntl.flock(f.fileno(), fcntl.LOCK_UN)
107
- return
97
+ content = self.registry_file.read_text(
98
+ encoding='utf-8'
99
+ ).strip()
100
+ self.registry = content.split('\n') if content else []
108
101
  else:
109
102
  self.registry = []
110
- return
111
- except IOError:
103
+ return
104
+ except (IOError, OSError):
112
105
  if attempt < max_retries - 1:
113
106
  time.sleep(retry_delay * (attempt + 1))
114
107
  else:
108
+ # If all retries failed, initialize with empty registry
115
109
  self.registry = []
116
- except Exception:
117
- self.registry = []
118
- return
119
110
 
120
111
  def _save_registry(self) -> None:
121
- r"""Save the note registry to file with file locking."""
112
+ r"""Save the note registry to file using atomic write."""
122
113
  max_retries = 5
123
114
  retry_delay = 0.1
124
115
 
125
116
  for attempt in range(max_retries):
126
117
  try:
127
- with open(self.registry_file, 'w') as f:
128
- fcntl.flock(f.fileno(), fcntl.LOCK_EX)
129
- try:
130
- f.write('\n'.join(self.registry))
131
- f.flush()
132
- os.fsync(f.fileno())
133
- finally:
134
- fcntl.flock(f.fileno(), fcntl.LOCK_UN)
118
+ # Use atomic write with temporary file for all platforms
119
+ temp_file = self.registry_file.with_suffix('.tmp')
120
+ temp_file.write_text(
121
+ '\n'.join(self.registry), encoding='utf-8'
122
+ )
123
+
124
+ # Atomic rename - works on all platforms
125
+ temp_file.replace(self.registry_file)
135
126
  return
136
- except IOError:
127
+ except (IOError, OSError):
137
128
  if attempt < max_retries - 1:
138
129
  time.sleep(retry_delay * (attempt + 1))
139
130
  else:
140
131
  raise
141
- except Exception:
142
- pass
143
132
 
144
133
  def _register_note(self, note_name: str) -> None:
145
134
  r"""Register a new note in the registry with thread-safe operations."""
@@ -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 / "screenshots"
64
+ self.screenshots_dir = path
64
65
  self.screenshots_dir.mkdir(parents=True, exist_ok=True)
65
66
 
66
- def take_screenshot(
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
- ) -> ToolResult:
70
- r"""Take a screenshot of the entire screen and return it as a
71
- base64-encoded image.
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
- save_to_file (bool): Whether to save the screenshot to a file.
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
- ToolResult: An object containing:
79
- - text (str): A description of the screenshot.
80
- - images (List[str]): A list containing one base64-encoded
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
- # Generate filename with timestamp
94
- timestamp = int(time.time())
95
- filename = f"screenshot_{timestamp}.png"
96
- file_path = os.path.join(self.screenshots_dir, filename)
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
- return ToolResult(text=result_text, images=[img_data_url])
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 ToolResult(text=f"Error taking screenshot: {e}", images=[])
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.take_screenshot),
211
+ FunctionTool(self.take_screenshot_and_read_image),
212
+ FunctionTool(self.read_image),
128
213
  ]
@@ -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, query: str, search_type: str = "web"
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={query}&start="
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