camel-ai 0.2.72a6__py3-none-any.whl → 0.2.72a8__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 +25 -0
- camel/memories/agent_memories.py +38 -0
- camel/memories/base.py +8 -0
- camel/storages/vectordb_storages/__init__.py +1 -0
- camel/storages/vectordb_storages/surreal.py +415 -0
- camel/toolkits/__init__.py +4 -0
- camel/toolkits/human_toolkit.py +5 -1
- camel/toolkits/markitdown_toolkit.py +2 -2
- camel/toolkits/note_taking_toolkit.py +206 -26
- camel/toolkits/openai_image_toolkit.py +5 -5
- camel/toolkits/origene_mcp_toolkit.py +13 -11
- camel/toolkits/screenshot_toolkit.py +128 -0
- camel/toolkits/web_deploy_toolkit.py +1024 -0
- {camel_ai-0.2.72a6.dist-info → camel_ai-0.2.72a8.dist-info}/METADATA +3 -3
- {camel_ai-0.2.72a6.dist-info → camel_ai-0.2.72a8.dist-info}/RECORD +18 -15
- {camel_ai-0.2.72a6.dist-info → camel_ai-0.2.72a8.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.72a6.dist-info → camel_ai-0.2.72a8.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,7 +11,9 @@
|
|
|
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
|
|
14
15
|
import os
|
|
16
|
+
import time
|
|
15
17
|
from pathlib import Path
|
|
16
18
|
from typing import List, Optional
|
|
17
19
|
|
|
@@ -20,10 +22,11 @@ from camel.toolkits.function_tool import FunctionTool
|
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class NoteTakingToolkit(BaseToolkit):
|
|
23
|
-
r"""A toolkit for
|
|
25
|
+
r"""A toolkit for managing and interacting with markdown note files.
|
|
24
26
|
|
|
25
|
-
This toolkit
|
|
26
|
-
|
|
27
|
+
This toolkit provides tools for creating, reading, appending to, and
|
|
28
|
+
listing notes. All notes are stored as `.md` files in a dedicated working
|
|
29
|
+
directory and are tracked in a registry.
|
|
27
30
|
"""
|
|
28
31
|
|
|
29
32
|
def __init__(
|
|
@@ -34,12 +37,11 @@ class NoteTakingToolkit(BaseToolkit):
|
|
|
34
37
|
r"""Initialize the NoteTakingToolkit.
|
|
35
38
|
|
|
36
39
|
Args:
|
|
37
|
-
working_directory (str, optional): The path
|
|
38
|
-
If not provided, it will be determined by the
|
|
39
|
-
`CAMEL_WORKDIR` environment variable (if set)
|
|
40
|
-
the note as `notes.md` in that directory. If the
|
|
40
|
+
working_directory (str, optional): The directory path where notes
|
|
41
|
+
will be stored. If not provided, it will be determined by the
|
|
42
|
+
`CAMEL_WORKDIR` environment variable (if set). If the
|
|
41
43
|
environment variable is not set, it defaults to
|
|
42
|
-
`camel_working_dir
|
|
44
|
+
`camel_working_dir`.
|
|
43
45
|
timeout (Optional[float]): The timeout for the toolkit.
|
|
44
46
|
"""
|
|
45
47
|
super().__init__(timeout=timeout)
|
|
@@ -47,42 +49,218 @@ class NoteTakingToolkit(BaseToolkit):
|
|
|
47
49
|
if working_directory:
|
|
48
50
|
path = Path(working_directory)
|
|
49
51
|
elif camel_workdir:
|
|
50
|
-
path = Path(camel_workdir)
|
|
52
|
+
path = Path(camel_workdir)
|
|
51
53
|
else:
|
|
52
|
-
path = Path("camel_working_dir")
|
|
54
|
+
path = Path("camel_working_dir")
|
|
53
55
|
|
|
54
56
|
self.working_directory = path
|
|
55
|
-
self.working_directory.
|
|
57
|
+
self.working_directory.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
self.registry_file = self.working_directory / ".note_register"
|
|
59
|
+
self._load_registry()
|
|
56
60
|
|
|
57
|
-
def append_note(self, content: str) -> str:
|
|
58
|
-
r"""Appends
|
|
61
|
+
def append_note(self, note_name: str, content: str) -> str:
|
|
62
|
+
r"""Appends content to a note.
|
|
63
|
+
|
|
64
|
+
If the note does not exist, it will be created with the given content.
|
|
65
|
+
If the note already exists, the new content will be added to the end of
|
|
66
|
+
the note.
|
|
59
67
|
|
|
60
68
|
Args:
|
|
61
|
-
|
|
69
|
+
note_name (str): The name of the note (without the .md extension).
|
|
70
|
+
content (str): The content to append to the note.
|
|
62
71
|
|
|
63
72
|
Returns:
|
|
64
|
-
str: A message
|
|
73
|
+
str: A message confirming that the content was appended or the note
|
|
74
|
+
was created.
|
|
65
75
|
"""
|
|
66
76
|
try:
|
|
67
|
-
|
|
77
|
+
# Reload registry to get latest state
|
|
78
|
+
self._load_registry()
|
|
79
|
+
note_path = self.working_directory / f"{note_name}.md"
|
|
80
|
+
if note_name not in self.registry or not note_path.exists():
|
|
81
|
+
self.create_note(note_name, content)
|
|
82
|
+
return f"Note '{note_name}' created with content added."
|
|
83
|
+
|
|
84
|
+
with note_path.open("a", encoding="utf-8") as f:
|
|
68
85
|
f.write(content + "\n")
|
|
69
|
-
return
|
|
70
|
-
f"Note successfully appended to in {self.working_directory}."
|
|
71
|
-
)
|
|
86
|
+
return f"Content successfully appended to '{note_name}.md'."
|
|
72
87
|
except Exception as e:
|
|
73
88
|
return f"Error appending note: {e}"
|
|
74
89
|
|
|
75
|
-
def
|
|
76
|
-
r"""
|
|
90
|
+
def _load_registry(self) -> None:
|
|
91
|
+
r"""Load the note registry from file with file locking."""
|
|
92
|
+
max_retries = 5
|
|
93
|
+
retry_delay = 0.1
|
|
94
|
+
|
|
95
|
+
for attempt in range(max_retries):
|
|
96
|
+
try:
|
|
97
|
+
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
|
|
108
|
+
else:
|
|
109
|
+
self.registry = []
|
|
110
|
+
return
|
|
111
|
+
except IOError:
|
|
112
|
+
if attempt < max_retries - 1:
|
|
113
|
+
time.sleep(retry_delay * (attempt + 1))
|
|
114
|
+
else:
|
|
115
|
+
self.registry = []
|
|
116
|
+
except Exception:
|
|
117
|
+
self.registry = []
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
def _save_registry(self) -> None:
|
|
121
|
+
r"""Save the note registry to file with file locking."""
|
|
122
|
+
max_retries = 5
|
|
123
|
+
retry_delay = 0.1
|
|
124
|
+
|
|
125
|
+
for attempt in range(max_retries):
|
|
126
|
+
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)
|
|
135
|
+
return
|
|
136
|
+
except IOError:
|
|
137
|
+
if attempt < max_retries - 1:
|
|
138
|
+
time.sleep(retry_delay * (attempt + 1))
|
|
139
|
+
else:
|
|
140
|
+
raise
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
def _register_note(self, note_name: str) -> None:
|
|
145
|
+
r"""Register a new note in the registry with thread-safe operations."""
|
|
146
|
+
# Reload registry to get latest state
|
|
147
|
+
self._load_registry()
|
|
148
|
+
if note_name not in self.registry:
|
|
149
|
+
self.registry.append(note_name)
|
|
150
|
+
self._save_registry()
|
|
151
|
+
|
|
152
|
+
def create_note(self, note_name: str, content: str = "") -> str:
|
|
153
|
+
r"""Creates a new note with a unique name.
|
|
154
|
+
|
|
155
|
+
This function will create a new file for your note.
|
|
156
|
+
You must provide a `note_name` that does not already exist. If you want
|
|
157
|
+
to add content to an existing note, use the `append_note` function
|
|
158
|
+
instead.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
note_name (str): The name for your new note (without the .md
|
|
162
|
+
extension). This name must be unique.
|
|
163
|
+
content (str, optional): The initial content to write in the note.
|
|
164
|
+
If not provided, an empty note will be created. Defaults to "".
|
|
77
165
|
|
|
78
166
|
Returns:
|
|
79
|
-
str:
|
|
80
|
-
|
|
167
|
+
str: A message confirming the creation of the note or an error if
|
|
168
|
+
the note name is not valid or already exists.
|
|
81
169
|
"""
|
|
82
170
|
try:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
171
|
+
note_path = self.working_directory / f"{note_name}.md"
|
|
172
|
+
|
|
173
|
+
if note_path.exists():
|
|
174
|
+
return f"Error: Note '{note_name}.md' already exists."
|
|
175
|
+
|
|
176
|
+
note_path.write_text(content, encoding="utf-8")
|
|
177
|
+
self._register_note(note_name)
|
|
178
|
+
|
|
179
|
+
return f"Note '{note_name}.md' successfully created."
|
|
180
|
+
except Exception as e:
|
|
181
|
+
return f"Error creating note: {e}"
|
|
182
|
+
|
|
183
|
+
def list_note(self) -> str:
|
|
184
|
+
r"""Lists all the notes you have created.
|
|
185
|
+
|
|
186
|
+
This function will show you a list of all your notes, along with their
|
|
187
|
+
sizes in bytes. This is useful for seeing what notes you have available
|
|
188
|
+
to read or append to.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
str: A string containing a list of available notes and their sizes,
|
|
192
|
+
or a message indicating that no notes have been created yet.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
# Reload registry to get latest state
|
|
196
|
+
self._load_registry()
|
|
197
|
+
if not self.registry:
|
|
198
|
+
return "No notes have been created yet."
|
|
199
|
+
|
|
200
|
+
notes_info = []
|
|
201
|
+
for note_name in self.registry:
|
|
202
|
+
note_path = self.working_directory / f"{note_name}.md"
|
|
203
|
+
if note_path.exists():
|
|
204
|
+
size = note_path.stat().st_size
|
|
205
|
+
notes_info.append(f"- {note_name}.md ({size} bytes)")
|
|
206
|
+
else:
|
|
207
|
+
notes_info.append(f"- {note_name}.md (file missing)")
|
|
208
|
+
|
|
209
|
+
return "Available notes:\n" + "\n".join(notes_info)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
return f"Error listing notes: {e}"
|
|
212
|
+
|
|
213
|
+
def read_note(self, note_name: Optional[str] = "all_notes") -> str:
|
|
214
|
+
r"""Reads the content of a specific note or all notes.
|
|
215
|
+
|
|
216
|
+
You can use this function in two ways:
|
|
217
|
+
1. **Read a specific note:** Provide the `note_name` (without the .md
|
|
218
|
+
extension) to get the content of that single note.
|
|
219
|
+
2. **Read all notes:** Use `note_name="all_notes"` (default), and this
|
|
220
|
+
function will return the content of all your notes, concatenated
|
|
221
|
+
together.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
note_name (str, optional): The name of the note you want to read.
|
|
225
|
+
Defaults to "all_notes" which reads all notes.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
str: The content of the specified note(s), or an error message if
|
|
229
|
+
a note cannot be read.
|
|
230
|
+
"""
|
|
231
|
+
try:
|
|
232
|
+
# Reload registry to get latest state
|
|
233
|
+
self._load_registry()
|
|
234
|
+
if note_name and note_name != "all_notes":
|
|
235
|
+
if note_name not in self.registry:
|
|
236
|
+
return (
|
|
237
|
+
f"Error: Note '{note_name}' is not registered "
|
|
238
|
+
f"or was not created by this toolkit."
|
|
239
|
+
)
|
|
240
|
+
note_path = self.working_directory / f"{note_name}.md"
|
|
241
|
+
if not note_path.exists():
|
|
242
|
+
return f"Note file '{note_path.name}' does not exist."
|
|
243
|
+
return note_path.read_text(encoding="utf-8")
|
|
244
|
+
else:
|
|
245
|
+
if not self.registry:
|
|
246
|
+
return "No notes have been created yet."
|
|
247
|
+
|
|
248
|
+
all_notes = []
|
|
249
|
+
for registered_note in self.registry:
|
|
250
|
+
note_path = (
|
|
251
|
+
self.working_directory / f"{registered_note}.md"
|
|
252
|
+
)
|
|
253
|
+
if note_path.exists():
|
|
254
|
+
content = note_path.read_text(encoding="utf-8")
|
|
255
|
+
all_notes.append(
|
|
256
|
+
f"=== {registered_note}.md ===\n{content}"
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
all_notes.append(
|
|
260
|
+
f"=== {registered_note}.md ===\n[File not found]"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return "\n\n".join(all_notes)
|
|
86
264
|
except Exception as e:
|
|
87
265
|
return f"Error reading note: {e}"
|
|
88
266
|
|
|
@@ -96,4 +274,6 @@ class NoteTakingToolkit(BaseToolkit):
|
|
|
96
274
|
return [
|
|
97
275
|
FunctionTool(self.append_note),
|
|
98
276
|
FunctionTool(self.read_note),
|
|
277
|
+
FunctionTool(self.create_note),
|
|
278
|
+
FunctionTool(self.list_note),
|
|
99
279
|
]
|
|
@@ -69,7 +69,7 @@ class OpenAIImageToolkit(BaseToolkit):
|
|
|
69
69
|
Literal["transparent", "opaque", "auto"]
|
|
70
70
|
] = "auto",
|
|
71
71
|
style: Optional[Literal["vivid", "natural"]] = None,
|
|
72
|
-
|
|
72
|
+
working_directory: Optional[str] = "image_save",
|
|
73
73
|
):
|
|
74
74
|
r"""Initializes a new instance of the OpenAIImageToolkit class.
|
|
75
75
|
|
|
@@ -100,7 +100,7 @@ class OpenAIImageToolkit(BaseToolkit):
|
|
|
100
100
|
The background of the image.(default: :obj:`"auto"`)
|
|
101
101
|
style (Optional[Literal["vivid", "natural"]]): The style of the
|
|
102
102
|
image.(default: :obj:`None`)
|
|
103
|
-
|
|
103
|
+
working_directory (Optional[str]): The path to save the generated
|
|
104
104
|
image.(default: :obj:`"image_save"`)
|
|
105
105
|
"""
|
|
106
106
|
super().__init__(timeout=timeout)
|
|
@@ -114,7 +114,7 @@ class OpenAIImageToolkit(BaseToolkit):
|
|
|
114
114
|
self.n = n
|
|
115
115
|
self.background = background
|
|
116
116
|
self.style = style
|
|
117
|
-
self.
|
|
117
|
+
self.working_directory: str = working_directory or "image_save"
|
|
118
118
|
|
|
119
119
|
def base64_to_image(self, base64_string: str) -> Optional[Image.Image]:
|
|
120
120
|
r"""Converts a base64 encoded string into a PIL Image object.
|
|
@@ -213,7 +213,7 @@ class OpenAIImageToolkit(BaseToolkit):
|
|
|
213
213
|
|
|
214
214
|
# Save the image from base64
|
|
215
215
|
image_bytes = base64.b64decode(image_b64)
|
|
216
|
-
os.makedirs(self.
|
|
216
|
+
os.makedirs(self.working_directory, exist_ok=True)
|
|
217
217
|
|
|
218
218
|
# Add index to filename when multiple images
|
|
219
219
|
if len(response.data) > 1:
|
|
@@ -221,7 +221,7 @@ class OpenAIImageToolkit(BaseToolkit):
|
|
|
221
221
|
else:
|
|
222
222
|
filename = f"{image_name}_{uuid.uuid4().hex}.png"
|
|
223
223
|
|
|
224
|
-
image_path = os.path.join(self.
|
|
224
|
+
image_path = os.path.join(self.working_directory, filename)
|
|
225
225
|
|
|
226
226
|
with open(image_path, "wb") as f:
|
|
227
227
|
f.write(image_bytes)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
14
|
|
|
15
|
-
from typing import List, Optional
|
|
15
|
+
from typing import Dict, List, Optional
|
|
16
16
|
|
|
17
17
|
from camel.toolkits import BaseToolkit, FunctionTool, MCPToolkit
|
|
18
18
|
|
|
@@ -24,36 +24,38 @@ class OrigeneToolkit(BaseToolkit):
|
|
|
24
24
|
This toolkit can be used as an async context manager for automatic
|
|
25
25
|
connection management:
|
|
26
26
|
|
|
27
|
-
async with OrigeneToolkit() as toolkit:
|
|
27
|
+
async with OrigeneToolkit(config_dict=config) as toolkit:
|
|
28
28
|
tools = toolkit.get_tools()
|
|
29
29
|
# Toolkit is automatically disconnected when exiting
|
|
30
30
|
|
|
31
31
|
Attributes:
|
|
32
|
+
config_dict (Dict): Configuration dictionary for MCP servers.
|
|
32
33
|
timeout (Optional[float]): Connection timeout in seconds.
|
|
33
34
|
(default: :obj:`None`)
|
|
34
35
|
"""
|
|
35
36
|
|
|
36
37
|
def __init__(
|
|
37
38
|
self,
|
|
39
|
+
config_dict: Optional[Dict] = None,
|
|
38
40
|
timeout: Optional[float] = None,
|
|
39
41
|
) -> None:
|
|
40
42
|
r"""Initializes the OrigeneToolkit.
|
|
41
43
|
|
|
42
44
|
Args:
|
|
45
|
+
config_dict (Optional[Dict]): Configuration dictionary for MCP
|
|
46
|
+
servers. If None, uses default configuration for chembl_mcp.
|
|
47
|
+
(default: :obj:`None`)
|
|
43
48
|
timeout (Optional[float]): Connection timeout in seconds.
|
|
44
49
|
(default: :obj:`None`)
|
|
45
50
|
"""
|
|
46
51
|
super().__init__(timeout=timeout)
|
|
47
52
|
|
|
53
|
+
# Use default configuration if none provided
|
|
54
|
+
if config_dict is None:
|
|
55
|
+
raise ValueError("config_dict must be provided")
|
|
56
|
+
|
|
48
57
|
self._mcp_toolkit = MCPToolkit(
|
|
49
|
-
config_dict=
|
|
50
|
-
"mcpServers": {
|
|
51
|
-
"pubchem_mcp": {
|
|
52
|
-
"url": "http://127.0.0.1:8791/mcp/",
|
|
53
|
-
"mode": "streamable-http",
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
},
|
|
58
|
+
config_dict=config_dict,
|
|
57
59
|
timeout=timeout,
|
|
58
60
|
)
|
|
59
61
|
|
|
@@ -72,7 +74,7 @@ class OrigeneToolkit(BaseToolkit):
|
|
|
72
74
|
OrigeneToolkit: The connected toolkit instance.
|
|
73
75
|
|
|
74
76
|
Example:
|
|
75
|
-
async with OrigeneToolkit() as toolkit:
|
|
77
|
+
async with OrigeneToolkit(config_dict=config) as toolkit:
|
|
76
78
|
tools = toolkit.get_tools()
|
|
77
79
|
"""
|
|
78
80
|
await self.connect()
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
import base64
|
|
16
|
+
import io
|
|
17
|
+
import os
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import List, Optional
|
|
21
|
+
|
|
22
|
+
from camel.logger import get_logger
|
|
23
|
+
from camel.toolkits import BaseToolkit, FunctionTool
|
|
24
|
+
from camel.utils import dependencies_required
|
|
25
|
+
from camel.utils.tool_result import ToolResult
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ScreenshotToolkit(BaseToolkit):
|
|
31
|
+
r"""A toolkit for taking screenshots."""
|
|
32
|
+
|
|
33
|
+
@dependencies_required('PIL')
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
working_directory: Optional[str] = None,
|
|
37
|
+
timeout: Optional[float] = None,
|
|
38
|
+
):
|
|
39
|
+
r"""Initializes the ScreenshotToolkit.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
working_directory (str, optional): The directory path where notes
|
|
43
|
+
will be stored. If not provided, it will be determined by the
|
|
44
|
+
`CAMEL_WORKDIR` environment variable (if set). If the
|
|
45
|
+
environment variable is not set, it defaults to
|
|
46
|
+
`camel_working_dir`.
|
|
47
|
+
timeout (Optional[float]): Timeout for API requests in seconds.
|
|
48
|
+
(default: :obj:`None`)
|
|
49
|
+
"""
|
|
50
|
+
from PIL import ImageGrab
|
|
51
|
+
|
|
52
|
+
super().__init__(timeout=timeout)
|
|
53
|
+
|
|
54
|
+
camel_workdir = os.environ.get("CAMEL_WORKDIR")
|
|
55
|
+
if working_directory:
|
|
56
|
+
path = Path(working_directory)
|
|
57
|
+
elif camel_workdir:
|
|
58
|
+
path = Path(camel_workdir)
|
|
59
|
+
else:
|
|
60
|
+
path = Path("camel_working_dir")
|
|
61
|
+
|
|
62
|
+
self.ImageGrab = ImageGrab
|
|
63
|
+
self.screenshots_dir = path / "screenshots"
|
|
64
|
+
self.screenshots_dir.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
def take_screenshot(
|
|
67
|
+
self,
|
|
68
|
+
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.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
save_to_file (bool): Whether to save the screenshot to a file.
|
|
75
|
+
(default: :obj:`True`)
|
|
76
|
+
|
|
77
|
+
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.
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
# Take screenshot of entire screen
|
|
85
|
+
screenshot = self.ImageGrab.grab()
|
|
86
|
+
|
|
87
|
+
# Save to file if requested
|
|
88
|
+
file_path = None
|
|
89
|
+
if save_to_file:
|
|
90
|
+
# Create directory if it doesn't exist
|
|
91
|
+
os.makedirs(self.screenshots_dir, exist_ok=True)
|
|
92
|
+
|
|
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)
|
|
97
|
+
screenshot.save(file_path)
|
|
98
|
+
logger.info(f"Screenshot saved to {file_path}")
|
|
99
|
+
|
|
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
|
+
# Create result text
|
|
110
|
+
result_text = "Screenshot captured successfully"
|
|
111
|
+
if file_path:
|
|
112
|
+
result_text += f" and saved to {file_path}"
|
|
113
|
+
|
|
114
|
+
return ToolResult(text=result_text, images=[img_data_url])
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f"Error taking screenshot: {e}")
|
|
118
|
+
return ToolResult(text=f"Error taking screenshot: {e}", images=[])
|
|
119
|
+
|
|
120
|
+
def get_tools(self) -> List[FunctionTool]:
|
|
121
|
+
r"""Returns a list of FunctionTool objects for screenshot operations.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List[FunctionTool]: List of screenshot functions.
|
|
125
|
+
"""
|
|
126
|
+
return [
|
|
127
|
+
FunctionTool(self.take_screenshot),
|
|
128
|
+
]
|