camel-ai 0.2.45__py3-none-any.whl → 0.2.47__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 (54) hide show
  1. camel/__init__.py +1 -1
  2. camel/configs/__init__.py +6 -0
  3. camel/configs/bedrock_config.py +73 -0
  4. camel/configs/lmstudio_config.py +94 -0
  5. camel/configs/qwen_config.py +3 -3
  6. camel/datasets/few_shot_generator.py +19 -3
  7. camel/datasets/models.py +1 -1
  8. camel/loaders/__init__.py +2 -0
  9. camel/loaders/scrapegraph_reader.py +96 -0
  10. camel/models/__init__.py +4 -0
  11. camel/models/aiml_model.py +11 -104
  12. camel/models/anthropic_model.py +11 -76
  13. camel/models/aws_bedrock_model.py +112 -0
  14. camel/models/deepseek_model.py +11 -44
  15. camel/models/gemini_model.py +10 -72
  16. camel/models/groq_model.py +11 -131
  17. camel/models/internlm_model.py +11 -61
  18. camel/models/lmstudio_model.py +82 -0
  19. camel/models/model_factory.py +7 -1
  20. camel/models/modelscope_model.py +11 -122
  21. camel/models/moonshot_model.py +10 -76
  22. camel/models/nemotron_model.py +4 -60
  23. camel/models/nvidia_model.py +11 -111
  24. camel/models/ollama_model.py +12 -205
  25. camel/models/openai_compatible_model.py +51 -12
  26. camel/models/openai_model.py +3 -1
  27. camel/models/openrouter_model.py +12 -131
  28. camel/models/ppio_model.py +10 -99
  29. camel/models/qwen_model.py +11 -122
  30. camel/models/reka_model.py +1 -1
  31. camel/models/sglang_model.py +5 -3
  32. camel/models/siliconflow_model.py +10 -58
  33. camel/models/togetherai_model.py +10 -177
  34. camel/models/vllm_model.py +11 -218
  35. camel/models/volcano_model.py +1 -15
  36. camel/models/yi_model.py +11 -98
  37. camel/models/zhipuai_model.py +11 -102
  38. camel/storages/__init__.py +2 -0
  39. camel/storages/vectordb_storages/__init__.py +2 -0
  40. camel/storages/vectordb_storages/oceanbase.py +458 -0
  41. camel/toolkits/__init__.py +4 -0
  42. camel/toolkits/browser_toolkit.py +4 -7
  43. camel/toolkits/jina_reranker_toolkit.py +231 -0
  44. camel/toolkits/pyautogui_toolkit.py +428 -0
  45. camel/toolkits/search_toolkit.py +167 -0
  46. camel/toolkits/video_analysis_toolkit.py +215 -80
  47. camel/toolkits/video_download_toolkit.py +10 -3
  48. camel/types/enums.py +70 -0
  49. camel/types/unified_model_type.py +10 -0
  50. camel/utils/token_counting.py +7 -3
  51. {camel_ai-0.2.45.dist-info → camel_ai-0.2.47.dist-info}/METADATA +13 -1
  52. {camel_ai-0.2.45.dist-info → camel_ai-0.2.47.dist-info}/RECORD +54 -46
  53. {camel_ai-0.2.45.dist-info → camel_ai-0.2.47.dist-info}/WHEEL +0 -0
  54. {camel_ai-0.2.45.dist-info → camel_ai-0.2.47.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,231 @@
1
+ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
+ from typing import List, Optional, Tuple
15
+
16
+ from camel.toolkits import FunctionTool
17
+ from camel.toolkits.base import BaseToolkit
18
+ from camel.utils import MCPServer
19
+
20
+
21
+ @MCPServer()
22
+ class JinaRerankerToolkit(BaseToolkit):
23
+ r"""A class representing a toolkit for reranking documents
24
+ using Jina Reranker.
25
+
26
+ This class provides methods for reranking documents (text or images)
27
+ based on their relevance to a given query using the Jina Reranker model.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ timeout: Optional[float] = None,
33
+ device: Optional[str] = None,
34
+ ) -> None:
35
+ r"""Initializes a new instance of the JinaRerankerToolkit class.
36
+
37
+ Args:
38
+ timeout (Optional[float]): The timeout value for API requests
39
+ in seconds. If None, no timeout is applied.
40
+ (default: :obj:`None`)
41
+ device (Optional[str]): Device to load the model on. If None,
42
+ will use CUDA if available, otherwise CPU.
43
+ (default: :obj:`None`)
44
+ """
45
+ import torch
46
+ from transformers import AutoModel
47
+
48
+ super().__init__(timeout=timeout)
49
+
50
+ self.model = AutoModel.from_pretrained(
51
+ 'jinaai/jina-reranker-m0',
52
+ torch_dtype="auto",
53
+ trust_remote_code=True,
54
+ )
55
+ DEVICE = (
56
+ device
57
+ if device is not None
58
+ else ("cuda" if torch.cuda.is_available() else "cpu")
59
+ )
60
+ self.model.to(DEVICE)
61
+ self.model.eval()
62
+
63
+ def _sort_documents(
64
+ self, documents: List[str], scores: List[float]
65
+ ) -> List[Tuple[str, float]]:
66
+ r"""Sort documents by their scores in descending order.
67
+
68
+ Args:
69
+ documents (List[str]): List of documents to sort.
70
+ scores (List[float]): Corresponding scores for each document.
71
+
72
+ Returns:
73
+ List[Tuple[str, float]]: Sorted list of (document, score) pairs.
74
+
75
+ Raises:
76
+ ValueError: If documents and scores have different lengths.
77
+ """
78
+ if len(documents) != len(scores):
79
+ raise ValueError("Number of documents must match number of scores")
80
+ doc_score_pairs = list(zip(documents, scores))
81
+ doc_score_pairs.sort(key=lambda x: x[1], reverse=True)
82
+
83
+ return doc_score_pairs
84
+
85
+ def rerank_text_documents(
86
+ self,
87
+ query: str,
88
+ documents: List[str],
89
+ max_length: int = 1024,
90
+ ) -> List[Tuple[str, float]]:
91
+ r"""Reranks text documents based on their relevance to a text query.
92
+
93
+ Args:
94
+ query (str): The text query for reranking.
95
+ documents (List[str]): List of text documents to be reranked.
96
+ max_length (int): Maximum token length for processing.
97
+ (default: :obj:`1024`)
98
+
99
+ Returns:
100
+ List[Tuple[str, float]]: A list of tuples containing
101
+ the reranked documents and their relevance scores.
102
+ """
103
+ import torch
104
+
105
+ if self.model is None:
106
+ raise ValueError(
107
+ "Model has not been initialized or failed to initialize."
108
+ )
109
+
110
+ with torch.inference_mode():
111
+ text_pairs = [[query, doc] for doc in documents]
112
+ scores = self.model.compute_score(
113
+ text_pairs, max_length=max_length, doc_type="text"
114
+ )
115
+
116
+ return self._sort_documents(documents, scores)
117
+
118
+ def rerank_image_documents(
119
+ self,
120
+ query: str,
121
+ documents: List[str],
122
+ max_length: int = 2048,
123
+ ) -> List[Tuple[str, float]]:
124
+ r"""Reranks image documents based on their relevance to a text query.
125
+
126
+ Args:
127
+ query (str): The text query for reranking.
128
+ documents (List[str]): List of image URLs or paths to be reranked.
129
+ max_length (int): Maximum token length for processing.
130
+ (default: :obj:`2048`)
131
+
132
+ Returns:
133
+ List[Tuple[str, float]]: A list of tuples containing
134
+ the reranked image URLs/paths and their relevance scores.
135
+ """
136
+ import torch
137
+
138
+ if self.model is None:
139
+ raise ValueError(
140
+ "Model has not been initialized or failed to initialize."
141
+ )
142
+
143
+ with torch.inference_mode():
144
+ image_pairs = [[query, doc] for doc in documents]
145
+ scores = self.model.compute_score(
146
+ image_pairs, max_length=max_length, doc_type="image"
147
+ )
148
+
149
+ return self._sort_documents(documents, scores)
150
+
151
+ def image_query_text_documents(
152
+ self,
153
+ image_query: str,
154
+ documents: List[str],
155
+ max_length: int = 2048,
156
+ ) -> List[Tuple[str, float]]:
157
+ r"""Reranks text documents based on their relevance to an image query.
158
+
159
+ Args:
160
+ image_query (str): The image URL or path used as query.
161
+ documents (List[str]): List of text documents to be reranked.
162
+ max_length (int): Maximum token length for processing.
163
+ (default: :obj:`2048`)
164
+
165
+ Returns:
166
+ List[Tuple[str, float]]: A list of tuples containing
167
+ the reranked documents and their relevance scores.
168
+ """
169
+ import torch
170
+
171
+ if self.model is None:
172
+ raise ValueError("Model has not been initialized.")
173
+ with torch.inference_mode():
174
+ image_pairs = [[image_query, doc] for doc in documents]
175
+ scores = self.model.compute_score(
176
+ image_pairs,
177
+ max_length=max_length,
178
+ query_type="image",
179
+ doc_type="text",
180
+ )
181
+
182
+ return self._sort_documents(documents, scores)
183
+
184
+ def image_query_image_documents(
185
+ self,
186
+ image_query: str,
187
+ documents: List[str],
188
+ max_length: int = 2048,
189
+ ) -> List[Tuple[str, float]]:
190
+ r"""Reranks image documents based on their relevance to an image query.
191
+
192
+ Args:
193
+ image_query (str): The image URL or path used as query.
194
+ documents (List[str]): List of image URLs or paths to be reranked.
195
+ max_length (int): Maximum token length for processing.
196
+ (default: :obj:`2048`)
197
+
198
+ Returns:
199
+ List[Tuple[str, float]]: A list of tuples containing
200
+ the reranked image URLs/paths and their relevance scores.
201
+ """
202
+ import torch
203
+
204
+ if self.model is None:
205
+ raise ValueError("Model has not been initialized.")
206
+
207
+ with torch.inference_mode():
208
+ image_pairs = [[image_query, doc] for doc in documents]
209
+ scores = self.model.compute_score(
210
+ image_pairs,
211
+ max_length=max_length,
212
+ query_type="image",
213
+ doc_type="image",
214
+ )
215
+
216
+ return self._sort_documents(documents, scores)
217
+
218
+ def get_tools(self) -> List[FunctionTool]:
219
+ r"""Returns a list of FunctionTool objects representing the
220
+ functions in the toolkit.
221
+
222
+ Returns:
223
+ List[FunctionTool]: A list of FunctionTool objects
224
+ representing the functions in the toolkit.
225
+ """
226
+ return [
227
+ FunctionTool(self.rerank_text_documents),
228
+ FunctionTool(self.rerank_image_documents),
229
+ FunctionTool(self.image_query_text_documents),
230
+ FunctionTool(self.image_query_image_documents),
231
+ ]
@@ -0,0 +1,428 @@
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 os
16
+ import time
17
+ from typing import List, Literal, Optional, Tuple, Union
18
+
19
+ from camel.logger import get_logger
20
+ from camel.toolkits import BaseToolkit, FunctionTool
21
+ from camel.utils import MCPServer, dependencies_required
22
+
23
+ # Set up logging
24
+ logger = get_logger(__name__)
25
+
26
+ DURATION = 0.1
27
+
28
+
29
+ @MCPServer()
30
+ class PyAutoGUIToolkit(BaseToolkit):
31
+ r"""A toolkit for automating GUI interactions using PyAutoGUI."""
32
+
33
+ @dependencies_required('pyautogui')
34
+ def __init__(
35
+ self,
36
+ timeout: Optional[float] = None,
37
+ screenshots_dir: str = "tmp",
38
+ ):
39
+ r"""Initializes the PyAutoGUIToolkit with optional timeout.
40
+
41
+ Args:
42
+ timeout (Optional[float]): Timeout for API requests in seconds.
43
+ (default: :obj:`None`)
44
+ screenshots_dir (str): Directory to save screenshots.
45
+ (default: :obj:`"tmp"`)
46
+ """
47
+ import pyautogui
48
+
49
+ super().__init__(timeout=timeout)
50
+ # Configure PyAutoGUI for safety
51
+ self.pyautogui = pyautogui
52
+
53
+ self.pyautogui.FAILSAFE = True # Move mouse to upper-left to abort
54
+
55
+ # Get screen size for safety boundaries
56
+ self.screen_width, self.screen_height = self.pyautogui.size()
57
+ # Define safe boundaries (10% margin from edges)
58
+ self.safe_margin = 0.1
59
+ self.safe_min_x = int(self.screen_width * self.safe_margin)
60
+ self.safe_max_x = int(self.screen_width * (1 - self.safe_margin))
61
+ self.safe_min_y = int(self.screen_height * self.safe_margin)
62
+ self.safe_max_y = int(self.screen_height * (1 - self.safe_margin))
63
+ self.screen_center = (self.screen_width // 2, self.screen_height // 2)
64
+ self.screenshots_dir = os.path.expanduser(screenshots_dir)
65
+
66
+ def _get_safe_coordinates(self, x: int, y: int) -> Tuple[int, int]:
67
+ r"""Ensure coordinates are within safe boundaries to prevent triggering
68
+ failsafe.
69
+
70
+ Args:
71
+ x (int): Original x-coordinate
72
+ y (int): Original y-coordinate
73
+
74
+ Returns:
75
+ Tuple[int, int]: Safe coordinates
76
+ """
77
+ # Clamp coordinates to safe boundaries
78
+ safe_x = max(self.safe_min_x, min(x, self.safe_max_x))
79
+ safe_y = max(self.safe_min_y, min(y, self.safe_max_y))
80
+
81
+ if safe_x != x or safe_y != y:
82
+ logger.info(
83
+ f"Safety: Adjusted coordinates from ({x}, {y}) to "
84
+ f"({safe_x}, {safe_y})"
85
+ )
86
+
87
+ return safe_x, safe_y
88
+
89
+ def mouse_move(self, x: int, y: int) -> str:
90
+ r"""Move mouse pointer to specified coordinates.
91
+
92
+ Args:
93
+ x (int): X-coordinate to move to.
94
+ y (int): Y-coordinate to move to.
95
+
96
+ Returns:
97
+ str: Success or error message.
98
+ """
99
+ try:
100
+ # Apply safety boundaries
101
+ safe_x, safe_y = self._get_safe_coordinates(x, y)
102
+ self.pyautogui.moveTo(safe_x, safe_y, duration=DURATION)
103
+ return f"Mouse moved to position ({safe_x}, {safe_y})"
104
+ except Exception as e:
105
+ logger.error(f"Error moving mouse: {e}")
106
+ return f"Error: {e}"
107
+
108
+ def mouse_click(
109
+ self,
110
+ button: Literal["left", "middle", "right"] = "left",
111
+ clicks: int = 1,
112
+ x: Optional[int] = None,
113
+ y: Optional[int] = None,
114
+ ) -> str:
115
+ r"""Performs a mouse click at the specified coordinates or current
116
+ position.
117
+
118
+ Args:
119
+ button (Literal["left", "middle", "right"]): The mouse button to
120
+ click.
121
+ - "left": Typically used for selecting items, activating
122
+ buttons, or placing the cursor.
123
+ - "middle": Often used for opening links in a new tab or
124
+ specific application functions.
125
+ - "right": Usually opens a context menu providing options
126
+ related to the clicked item or area.
127
+ (default: :obj:`"left"`)
128
+ clicks (int): The number of times to click the button.
129
+ - 1: A single click, the most common action.
130
+ - 2: A double-click, often used to open files/folders or
131
+ select words.
132
+ (default: :obj:`1`)
133
+ x (Optional[int]): The x-coordinate on the screen to move the mouse
134
+ to before clicking. If None, clicks at the current mouse
135
+ position. (default: :obj:`None`)
136
+ y (Optional[int]): The y-coordinate on the screen to move the mouse
137
+ to before clicking. If None, clicks at the current mouse
138
+ position. (default: :obj:`None`)
139
+
140
+ Returns:
141
+ str: A message indicating the action performed, e.g.,
142
+ "Clicked left button 1 time(s) at coordinates (100, 150)."
143
+ or "Clicked right button 2 time(s) at current position."
144
+ """
145
+ try:
146
+ # Apply safety boundaries if coordinates are specified
147
+ position_info = "at current position"
148
+ if x is not None and y is not None:
149
+ safe_x, safe_y = self._get_safe_coordinates(x, y)
150
+ self.pyautogui.click(
151
+ x=safe_x, y=safe_y, button=button, clicks=clicks
152
+ )
153
+ position_info = f"at position ({safe_x}, {safe_y})"
154
+ else:
155
+ self.pyautogui.click(button=button, clicks=clicks)
156
+
157
+ return f"Clicked {button} button {clicks} time(s) {position_info}"
158
+ except Exception as e:
159
+ logger.error(f"Error clicking mouse: {e}")
160
+ return f"Error: {e}"
161
+
162
+ def get_mouse_position(self) -> str:
163
+ r"""Get current mouse position.
164
+
165
+ Returns:
166
+ str: Current mouse X and Y coordinates.
167
+ """
168
+ try:
169
+ x, y = self.pyautogui.position()
170
+ return f"Mouse position: ({x}, {y})"
171
+ except Exception as e:
172
+ logger.error(f"Error getting mouse position: {e}")
173
+ return f"Error: {e}"
174
+
175
+ def take_screenshot(self) -> str:
176
+ r"""Take a screenshot.
177
+
178
+ Returns:
179
+ str: Path to the saved screenshot or error message.
180
+ """
181
+ try:
182
+ # Create directory for screenshots if it doesn't exist
183
+ os.makedirs(self.screenshots_dir, exist_ok=True)
184
+
185
+ # Take screenshot
186
+ screenshot = self.pyautogui.screenshot()
187
+
188
+ # Save screenshot to file
189
+ timestamp = int(time.time())
190
+ filename = f"screenshot_{timestamp}.png"
191
+ filepath = os.path.join(self.screenshots_dir, filename)
192
+ screenshot.save(filepath)
193
+
194
+ return f"Screenshot saved to {filepath}"
195
+ except Exception as e:
196
+ logger.error(f"Error taking screenshot: {e}")
197
+ return f"Error: {e}"
198
+
199
+ def mouse_drag(
200
+ self,
201
+ start_x: int,
202
+ start_y: int,
203
+ end_x: int,
204
+ end_y: int,
205
+ button: Literal["left", "middle", "right"] = "left",
206
+ ) -> str:
207
+ r"""Drag mouse from start position to end position.
208
+
209
+ Args:
210
+ start_x (int): Starting x-coordinate.
211
+ start_y (int): Starting y-coordinate.
212
+ end_x (int): Ending x-coordinate.
213
+ end_y (int): Ending y-coordinate.
214
+ button (Literal["left", "middle", "right"]): Mouse button to use
215
+ ('left', 'middle', 'right'). (default: :obj:`'left'`)
216
+
217
+ Returns:
218
+ str: Success or error message.
219
+ """
220
+ try:
221
+ # Apply safety boundaries to both start and end positions
222
+ safe_start_x, safe_start_y = self._get_safe_coordinates(
223
+ start_x, start_y
224
+ )
225
+ safe_end_x, safe_end_y = self._get_safe_coordinates(end_x, end_y)
226
+
227
+ # Break operation into smaller steps for safety
228
+ # First move to start position
229
+ self.pyautogui.moveTo(
230
+ safe_start_x, safe_start_y, duration=DURATION
231
+ )
232
+ # Then perform drag
233
+ self.pyautogui.dragTo(
234
+ safe_end_x, safe_end_y, duration=DURATION, button=button
235
+ )
236
+ # Finally, move to a safe position (screen center) afterwards
237
+ self.pyautogui.moveTo(
238
+ self.screen_center[0],
239
+ self.screen_center[1],
240
+ duration=DURATION,
241
+ )
242
+
243
+ return (
244
+ f"Dragged from ({safe_start_x}, {safe_start_y}) "
245
+ f"to ({safe_end_x}, {safe_end_y})"
246
+ )
247
+ except Exception as e:
248
+ logger.error(f"Error dragging mouse: {e}")
249
+ # Try to move to safe position even after error
250
+ try:
251
+ self.pyautogui.moveTo(
252
+ self.screen_center[0],
253
+ self.screen_center[1],
254
+ duration=DURATION,
255
+ )
256
+ except Exception as recovery_error:
257
+ logger.error(
258
+ f"Failed to move to safe position: {recovery_error}"
259
+ )
260
+ return f"Error: {e}"
261
+
262
+ def scroll(
263
+ self,
264
+ scroll_amount: int,
265
+ x: Optional[int] = None,
266
+ y: Optional[int] = None,
267
+ ) -> str:
268
+ r"""Scroll the mouse wheel.
269
+
270
+ Args:
271
+ scroll_amount (int): Amount to scroll. Positive values scroll up,
272
+ negative values scroll down.
273
+ x (Optional[int]): X-coordinate to scroll at. If None, uses current
274
+ position. (default: :obj:`None`)
275
+ y (Optional[int]): Y-coordinate to scroll at. If None, uses current
276
+ position. (default: :obj:`None`)
277
+
278
+ Returns:
279
+ str: Success or error message.
280
+ """
281
+ try:
282
+ # Get current mouse position if coordinates are not specified
283
+ if x is None or y is None:
284
+ current_x, current_y = self.pyautogui.position()
285
+ x = x if x is not None else current_x
286
+ y = y if y is not None else current_y
287
+
288
+ # Always apply safety boundaries
289
+ safe_x, safe_y = self._get_safe_coordinates(x, y)
290
+ self.pyautogui.scroll(scroll_amount, x=safe_x, y=safe_y)
291
+
292
+ # Move mouse back to screen center for added safety
293
+ self.pyautogui.moveTo(self.screen_center[0], self.screen_center[1])
294
+ logger.info(
295
+ f"Safety: Moving mouse back to screen center "
296
+ f"({self.screen_center[0]}, {self.screen_center[1]})"
297
+ )
298
+
299
+ return (
300
+ f"Scrolled {scroll_amount} clicks at position "
301
+ f"{safe_x}, {safe_y}"
302
+ )
303
+ except Exception as e:
304
+ logger.error(f"Error scrolling: {e}")
305
+ return f"Error: {e}"
306
+
307
+ def keyboard_type(self, text: str, interval: float = 0.0) -> str:
308
+ r"""Type text on the keyboard.
309
+
310
+ Args:
311
+ text (str): Text to type.
312
+ interval (float): Seconds to wait between keypresses.
313
+ (default: :obj:`0.0`)
314
+
315
+ Returns:
316
+ str: Success or error message.
317
+ """
318
+ try:
319
+ if not text:
320
+ return "Error: Empty text provided"
321
+
322
+ if len(text) > 1000: # Set a reasonable maximum length limit
323
+ warn_msg = (
324
+ f"Warning: Very long text ({len(text)} characters) may "
325
+ f"cause performance issues"
326
+ )
327
+ logger.warning(warn_msg)
328
+
329
+ # First, move mouse to a safe position to prevent potential issues
330
+ self.pyautogui.moveTo(
331
+ self.screen_center[0], self.screen_center[1], duration=DURATION
332
+ )
333
+
334
+ self.pyautogui.write(text, interval=interval)
335
+ return f"Typed text: {text[:20]}{'...' if len(text) > 20 else ''}"
336
+ except Exception as e:
337
+ logger.error(f"Error typing text: {e}")
338
+ return f"Error: {e}"
339
+
340
+ def press_key(self, key: Union[str, List[str]]) -> str:
341
+ r"""Press a key on the keyboard.
342
+
343
+ Args:
344
+ key (Union[str, List[str]]): The key to be pressed. Can also be a
345
+ list of such strings. Valid key names include:
346
+ - Basic characters: a-z, 0-9, and symbols like !, @, #, etc.
347
+ - Special keys: enter, esc, space, tab, backspace, delete
348
+ - Function keys: f1-f24
349
+ - Navigation: up, down, left, right, home, end, pageup,
350
+ pagedown
351
+ - Modifiers: shift, ctrl, alt, command, option, win
352
+ - Media keys: volumeup, volumedown, volumemute, playpause
353
+
354
+ Returns:
355
+ str: Success or error message.
356
+ """
357
+ if isinstance(key, str):
358
+ key = [key]
359
+ try:
360
+ for k in key:
361
+ # Length validation (most valid key names are short)
362
+ if len(k) > 20:
363
+ logger.warning(
364
+ f"Warning: Key name '{k}' is too long "
365
+ "(max 20 characters)"
366
+ )
367
+
368
+ # Special character validation
369
+ # (key names usually don't contain special characters)
370
+ import re
371
+
372
+ if re.search(r'[^\w+\-_]', k) and len(k) > 1:
373
+ logger.warning(
374
+ f"Warning: Key '{k}' contains unusual characters"
375
+ )
376
+
377
+ # First, move mouse to a safe position to prevent potential issues
378
+ self.pyautogui.moveTo(
379
+ self.screen_center[0], self.screen_center[1], duration=DURATION
380
+ )
381
+
382
+ self.pyautogui.press(key)
383
+ return f"Pressed key: {key}"
384
+ except Exception as e:
385
+ logger.error(f"Error pressing key: {e}")
386
+ return f"Error: Invalid key '{key}' or error pressing it. {e}"
387
+
388
+ def hotkey(self, keys: List[str]) -> str:
389
+ r"""Press keys in succession and release in reverse order.
390
+
391
+ Args:
392
+ keys (List[str]): The series of keys to press, in order. This can
393
+ be either:
394
+ - Multiple string arguments, e.g., hotkey('ctrl', 'c')
395
+ - A single list of strings, e.g., hotkey(['ctrl', 'c'])
396
+
397
+ Returns:
398
+ str: Success or error message.
399
+ """
400
+ try:
401
+ # First, move mouse to a safe position to prevent potential issues
402
+ self.pyautogui.moveTo(
403
+ self.screen_center[0], self.screen_center[1], duration=DURATION
404
+ )
405
+
406
+ self.pyautogui.hotkey(*keys)
407
+ return f"Pressed hotkey: {'+'.join(keys)}"
408
+ except Exception as e:
409
+ logger.error(f"Error pressing hotkey: {e}")
410
+ return f"Error: {e}"
411
+
412
+ def get_tools(self) -> List[FunctionTool]:
413
+ r"""Returns a list of FunctionTool objects for PyAutoGUI operations.
414
+
415
+ Returns:
416
+ List[FunctionTool]: List of PyAutoGUI functions.
417
+ """
418
+ return [
419
+ FunctionTool(self.mouse_move),
420
+ FunctionTool(self.mouse_click),
421
+ FunctionTool(self.keyboard_type),
422
+ FunctionTool(self.take_screenshot),
423
+ FunctionTool(self.get_mouse_position),
424
+ FunctionTool(self.press_key),
425
+ FunctionTool(self.hotkey),
426
+ FunctionTool(self.mouse_drag),
427
+ FunctionTool(self.scroll),
428
+ ]