camel-ai 0.2.58__py3-none-any.whl → 0.2.60__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 (44) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +126 -9
  3. camel/agents/critic_agent.py +73 -8
  4. camel/benchmarks/__init__.py +2 -0
  5. camel/benchmarks/browsecomp.py +854 -0
  6. camel/configs/cohere_config.py +1 -1
  7. camel/configs/mistral_config.py +1 -1
  8. camel/configs/openai_config.py +3 -0
  9. camel/configs/reka_config.py +1 -1
  10. camel/configs/samba_config.py +2 -2
  11. camel/datagen/cot_datagen.py +29 -34
  12. camel/embeddings/jina_embedding.py +8 -1
  13. camel/embeddings/sentence_transformers_embeddings.py +2 -2
  14. camel/embeddings/vlm_embedding.py +9 -2
  15. camel/human.py +14 -0
  16. camel/memories/records.py +3 -0
  17. camel/messages/base.py +15 -3
  18. camel/models/azure_openai_model.py +1 -0
  19. camel/models/model_factory.py +2 -2
  20. camel/retrievers/bm25_retriever.py +1 -2
  21. camel/retrievers/hybrid_retrival.py +2 -2
  22. camel/societies/role_playing.py +50 -0
  23. camel/societies/workforce/role_playing_worker.py +17 -8
  24. camel/societies/workforce/workforce.py +70 -14
  25. camel/storages/vectordb_storages/oceanbase.py +1 -2
  26. camel/toolkits/async_browser_toolkit.py +5 -1
  27. camel/toolkits/base.py +4 -2
  28. camel/toolkits/browser_toolkit.py +6 -3
  29. camel/toolkits/dalle_toolkit.py +4 -0
  30. camel/toolkits/excel_toolkit.py +11 -3
  31. camel/toolkits/github_toolkit.py +43 -25
  32. camel/toolkits/image_analysis_toolkit.py +3 -0
  33. camel/toolkits/jina_reranker_toolkit.py +194 -77
  34. camel/toolkits/mcp_toolkit.py +60 -16
  35. camel/toolkits/page_script.js +40 -28
  36. camel/toolkits/twitter_toolkit.py +6 -1
  37. camel/toolkits/video_analysis_toolkit.py +3 -0
  38. camel/toolkits/video_download_toolkit.py +3 -0
  39. camel/toolkits/wolfram_alpha_toolkit.py +46 -22
  40. camel/types/enums.py +14 -5
  41. {camel_ai-0.2.58.dist-info → camel_ai-0.2.60.dist-info}/METADATA +7 -9
  42. {camel_ai-0.2.58.dist-info → camel_ai-0.2.60.dist-info}/RECORD +44 -43
  43. {camel_ai-0.2.58.dist-info → camel_ai-0.2.60.dist-info}/WHEEL +0 -0
  44. {camel_ai-0.2.58.dist-info → camel_ai-0.2.60.dist-info}/licenses/LICENSE +0 -0
@@ -11,7 +11,11 @@
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
- from typing import List, Optional, Tuple
14
+ import json
15
+ import os
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ import requests
15
19
 
16
20
  from camel.toolkits import FunctionTool
17
21
  from camel.toolkits.base import BaseToolkit
@@ -30,7 +34,9 @@ class JinaRerankerToolkit(BaseToolkit):
30
34
  def __init__(
31
35
  self,
32
36
  timeout: Optional[float] = None,
37
+ model_name: Optional[str] = "jinaai/jina-reranker-m0",
33
38
  device: Optional[str] = None,
39
+ use_api: bool = True,
34
40
  ) -> None:
35
41
  r"""Initializes a new instance of the JinaRerankerToolkit class.
36
42
 
@@ -38,31 +44,57 @@ class JinaRerankerToolkit(BaseToolkit):
38
44
  timeout (Optional[float]): The timeout value for API requests
39
45
  in seconds. If None, no timeout is applied.
40
46
  (default: :obj:`None`)
47
+ model_name (Optional[str]): The reranker model name. If None,
48
+ will use the default model.
49
+ (default: :obj:`None`)
41
50
  device (Optional[str]): Device to load the model on. If None,
42
51
  will use CUDA if available, otherwise CPU.
52
+ Only effective when use_api=False.
43
53
  (default: :obj:`None`)
54
+ use_api (bool): A flag to switch between local model and API.
55
+ (default: :obj:`True`)
44
56
  """
45
- import torch
46
- from transformers import AutoModel
47
57
 
48
58
  super().__init__(timeout=timeout)
49
59
 
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()
60
+ self.use_api = use_api
61
+ self.model_name = model_name
62
+
63
+ if self.use_api:
64
+ self.model = None
65
+ self._api_key = os.environ.get("JINA_API_KEY", "None")
66
+ if self._api_key == "None":
67
+ raise ValueError(
68
+ "Missing or empty required API keys in "
69
+ "environment variables\n"
70
+ "You can obtain the API key from https://jina.ai/reranker/"
71
+ )
72
+ self.url = 'https://api.jina.ai/v1/rerank'
73
+ self.headers = {
74
+ 'Content-Type': 'application/json',
75
+ 'Accept': 'application/json',
76
+ 'Authorization': f'Bearer {self._api_key}',
77
+ }
78
+ else:
79
+ import torch
80
+ from transformers import AutoModel
81
+
82
+ self.model = AutoModel.from_pretrained(
83
+ self.model_name,
84
+ torch_dtype="auto",
85
+ trust_remote_code=True,
86
+ )
87
+ self.device = (
88
+ device
89
+ if device is not None
90
+ else ("cuda" if torch.cuda.is_available() else "cpu")
91
+ )
92
+ self.model.to(self.device)
93
+ self.model.eval()
62
94
 
63
95
  def _sort_documents(
64
96
  self, documents: List[str], scores: List[float]
65
- ) -> List[Tuple[str, float]]:
97
+ ) -> List[Dict[str, object]]:
66
98
  r"""Sort documents by their scores in descending order.
67
99
 
68
100
  Args:
@@ -70,7 +102,7 @@ class JinaRerankerToolkit(BaseToolkit):
70
102
  scores (List[float]): Corresponding scores for each document.
71
103
 
72
104
  Returns:
73
- List[Tuple[str, float]]: Sorted list of (document, score) pairs.
105
+ List[Dict[str, object]]: Sorted list of (document, score) pairs.
74
106
 
75
107
  Raises:
76
108
  ValueError: If documents and scores have different lengths.
@@ -80,14 +112,45 @@ class JinaRerankerToolkit(BaseToolkit):
80
112
  doc_score_pairs = list(zip(documents, scores))
81
113
  doc_score_pairs.sort(key=lambda x: x[1], reverse=True)
82
114
 
83
- return doc_score_pairs
115
+ results = [
116
+ {'document': {'text': doc}, 'relevance_score': score}
117
+ for doc, score in doc_score_pairs
118
+ ]
119
+
120
+ return results
121
+
122
+ def _call_jina_api(self, data: Dict[str, Any]) -> List[Dict[str, object]]:
123
+ r"""Makes a call to the JINA API for reranking.
124
+
125
+ Args:
126
+ data (Dict[str]): The data to be passed into the api body.
127
+
128
+ Returns:
129
+ List[Dict[str, object]]: A list of dictionary containing
130
+ the reranked documents and their relevance scores.
131
+ """
132
+ try:
133
+ response = requests.post(
134
+ self.url,
135
+ headers=self.headers,
136
+ data=json.dumps(data),
137
+ timeout=self.timeout,
138
+ )
139
+ response.raise_for_status()
140
+ results = [
141
+ {key: value for key, value in _res.items() if key != 'index'}
142
+ for _res in response.json()['results']
143
+ ]
144
+ return results
145
+ except requests.exceptions.RequestException as e:
146
+ raise RuntimeError(f"Failed to get response from Jina AI: {e}")
84
147
 
85
148
  def rerank_text_documents(
86
149
  self,
87
150
  query: str,
88
151
  documents: List[str],
89
152
  max_length: int = 1024,
90
- ) -> List[Tuple[str, float]]:
153
+ ) -> List[Dict[str, object]]:
91
154
  r"""Reranks text documents based on their relevance to a text query.
92
155
 
93
156
  Args:
@@ -97,21 +160,34 @@ class JinaRerankerToolkit(BaseToolkit):
97
160
  (default: :obj:`1024`)
98
161
 
99
162
  Returns:
100
- List[Tuple[str, float]]: A list of tuples containing
163
+ List[Dict[str, object]]: A list of dictionary containing
101
164
  the reranked documents and their relevance scores.
102
165
  """
103
- import torch
104
166
 
105
- if self.model is None:
106
- raise ValueError(
107
- "Model has not been initialized or failed to initialize."
108
- )
167
+ data = {
168
+ 'model': self.model_name,
169
+ 'query': query,
170
+ 'top_n': len(documents),
171
+ 'documents': documents,
172
+ 'return_documents': True,
173
+ }
109
174
 
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
- )
175
+ if self.use_api:
176
+ return self._call_jina_api(data)
177
+
178
+ else:
179
+ import torch
180
+
181
+ if self.model is None:
182
+ raise ValueError(
183
+ "Model has not been initialized or failed to initialize."
184
+ )
185
+
186
+ with torch.inference_mode():
187
+ text_pairs = [[query, doc] for doc in documents]
188
+ scores = self.model.compute_score(
189
+ text_pairs, max_length=max_length, doc_type="text"
190
+ )
115
191
 
116
192
  return self._sort_documents(documents, scores)
117
193
 
@@ -120,7 +196,7 @@ class JinaRerankerToolkit(BaseToolkit):
120
196
  query: str,
121
197
  documents: List[str],
122
198
  max_length: int = 2048,
123
- ) -> List[Tuple[str, float]]:
199
+ ) -> List[Dict[str, object]]:
124
200
  r"""Reranks image documents based on their relevance to a text query.
125
201
 
126
202
  Args:
@@ -130,21 +206,33 @@ class JinaRerankerToolkit(BaseToolkit):
130
206
  (default: :obj:`2048`)
131
207
 
132
208
  Returns:
133
- List[Tuple[str, float]]: A list of tuples containing
209
+ List[Dict[str, object]]: A list of dictionary containing
134
210
  the reranked image URLs/paths and their relevance scores.
135
211
  """
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
- )
212
+ data = {
213
+ 'model': self.model_name,
214
+ 'query': query,
215
+ 'top_n': len(documents),
216
+ 'documents': documents,
217
+ 'return_documents': True,
218
+ }
219
+
220
+ if self.use_api:
221
+ return self._call_jina_api(data)
222
+
223
+ else:
224
+ import torch
225
+
226
+ if self.model is None:
227
+ raise ValueError(
228
+ "Model has not been initialized or failed to initialize."
229
+ )
230
+
231
+ with torch.inference_mode():
232
+ image_pairs = [[query, doc] for doc in documents]
233
+ scores = self.model.compute_score(
234
+ image_pairs, max_length=max_length, doc_type="image"
235
+ )
148
236
 
149
237
  return self._sort_documents(documents, scores)
150
238
 
@@ -153,7 +241,7 @@ class JinaRerankerToolkit(BaseToolkit):
153
241
  image_query: str,
154
242
  documents: List[str],
155
243
  max_length: int = 2048,
156
- ) -> List[Tuple[str, float]]:
244
+ ) -> List[Dict[str, object]]:
157
245
  r"""Reranks text documents based on their relevance to an image query.
158
246
 
159
247
  Args:
@@ -163,30 +251,45 @@ class JinaRerankerToolkit(BaseToolkit):
163
251
  (default: :obj:`2048`)
164
252
 
165
253
  Returns:
166
- List[Tuple[str, float]]: A list of tuples containing
254
+ List[Dict[str, object]]: A list of dictionary containing
167
255
  the reranked documents and their relevance scores.
168
256
  """
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)
257
+ data = {
258
+ 'model': self.model_name,
259
+ 'query': image_query,
260
+ 'top_n': len(documents),
261
+ 'documents': documents,
262
+ 'return_documents': True,
263
+ }
264
+
265
+ if self.use_api:
266
+ return self._call_jina_api(data)
267
+
268
+ else:
269
+ import torch
270
+
271
+ if self.model is None:
272
+ raise ValueError(
273
+ "Model has not been initialized or failed to initialize."
274
+ )
275
+
276
+ with torch.inference_mode():
277
+ image_pairs = [[image_query, doc] for doc in documents]
278
+ scores = self.model.compute_score(
279
+ image_pairs,
280
+ max_length=max_length,
281
+ query_type="image",
282
+ doc_type="text",
283
+ )
284
+
285
+ return self._sort_documents(documents, scores)
183
286
 
184
287
  def image_query_image_documents(
185
288
  self,
186
289
  image_query: str,
187
290
  documents: List[str],
188
291
  max_length: int = 2048,
189
- ) -> List[Tuple[str, float]]:
292
+ ) -> List[Dict[str, object]]:
190
293
  r"""Reranks image documents based on their relevance to an image query.
191
294
 
192
295
  Args:
@@ -196,24 +299,38 @@ class JinaRerankerToolkit(BaseToolkit):
196
299
  (default: :obj:`2048`)
197
300
 
198
301
  Returns:
199
- List[Tuple[str, float]]: A list of tuples containing
302
+ List[Dict[str, object]]: A list of dictionary containing
200
303
  the reranked image URLs/paths and their relevance scores.
201
304
  """
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)
305
+ data = {
306
+ 'model': self.model_name,
307
+ 'query': image_query,
308
+ 'top_n': len(documents),
309
+ 'documents': documents,
310
+ 'return_documents': True,
311
+ }
312
+
313
+ if self.use_api:
314
+ return self._call_jina_api(data)
315
+
316
+ else:
317
+ import torch
318
+
319
+ if self.model is None:
320
+ raise ValueError(
321
+ "Model has not been initialized or failed to initialize."
322
+ )
323
+
324
+ with torch.inference_mode():
325
+ image_pairs = [[image_query, doc] for doc in documents]
326
+ scores = self.model.compute_score(
327
+ image_pairs,
328
+ max_length=max_length,
329
+ query_type="image",
330
+ doc_type="image",
331
+ )
332
+
333
+ return self._sort_documents(documents, scores)
217
334
 
218
335
  def get_tools(self) -> List[FunctionTool]:
219
336
  r"""Returns a list of FunctionTool objects representing the
@@ -34,6 +34,7 @@ from urllib.parse import urlparse
34
34
  if TYPE_CHECKING:
35
35
  from mcp import ClientSession, ListToolsResult, Tool
36
36
 
37
+
37
38
  from camel.logger import get_logger
38
39
  from camel.toolkits import BaseToolkit, FunctionTool
39
40
 
@@ -84,7 +85,6 @@ class MCPClient(BaseToolkit):
84
85
  await client.disconnect()
85
86
  ```
86
87
 
87
-
88
88
  Attributes:
89
89
  command_or_url (str): URL for SSE mode or command executable for stdio
90
90
  mode. (default: :obj:`None`)
@@ -96,6 +96,10 @@ class MCPClient(BaseToolkit):
96
96
  (default: :obj:`None`)
97
97
  headers (Dict[str, str]): Headers for the HTTP request.
98
98
  (default: :obj:`None`)
99
+ mode (Optional[str]): Connection mode. Can be "sse" for Server-Sent
100
+ Events, "streamable-http" for streaming HTTP,
101
+ or None for stdio mode.
102
+ (default: :obj:`None`)
99
103
  strict (Optional[bool]): Whether to enforce strict mode for the
100
104
  function call. (default: :obj:`False`)
101
105
  """
@@ -107,6 +111,7 @@ class MCPClient(BaseToolkit):
107
111
  env: Optional[Dict[str, str]] = None,
108
112
  timeout: Optional[float] = None,
109
113
  headers: Optional[Dict[str, str]] = None,
114
+ mode: Optional[str] = None,
110
115
  strict: Optional[bool] = False,
111
116
  ):
112
117
  from mcp import Tool
@@ -118,6 +123,7 @@ class MCPClient(BaseToolkit):
118
123
  self.env = env or {}
119
124
  self.headers = headers or {}
120
125
  self.strict = strict
126
+ self.mode = mode
121
127
 
122
128
  self._mcp_tools: List[Tool] = []
123
129
  self._session: Optional['ClientSession'] = None
@@ -133,6 +139,7 @@ class MCPClient(BaseToolkit):
133
139
  from mcp.client.session import ClientSession
134
140
  from mcp.client.sse import sse_client
135
141
  from mcp.client.stdio import StdioServerParameters, stdio_client
142
+ from mcp.client.streamable_http import streamablehttp_client
136
143
 
137
144
  if self._is_connected:
138
145
  logger.warning("Server is already connected")
@@ -140,16 +147,37 @@ class MCPClient(BaseToolkit):
140
147
 
141
148
  try:
142
149
  if urlparse(self.command_or_url).scheme in ("http", "https"):
143
- (
144
- read_stream,
145
- write_stream,
146
- ) = await self._exit_stack.enter_async_context(
147
- sse_client(
148
- self.command_or_url,
149
- headers=self.headers,
150
- timeout=self.timeout,
150
+ if self.mode == "sse" or self.mode is None:
151
+ (
152
+ read_stream,
153
+ write_stream,
154
+ ) = await self._exit_stack.enter_async_context(
155
+ sse_client(
156
+ self.command_or_url,
157
+ headers=self.headers,
158
+ timeout=self.timeout,
159
+ )
160
+ )
161
+ elif self.mode == "streamable-http":
162
+ try:
163
+ (
164
+ read_stream,
165
+ write_stream,
166
+ _,
167
+ ) = await self._exit_stack.enter_async_context(
168
+ streamablehttp_client(
169
+ self.command_or_url,
170
+ headers=self.headers,
171
+ timeout=timedelta(seconds=self.timeout),
172
+ )
173
+ )
174
+ except Exception as e:
175
+ # Handle anyio task group errors
176
+ logger.error(f"Streamable HTTP client error: {e}")
177
+ else:
178
+ raise ValueError(
179
+ f"Invalid mode '{self.mode}' for HTTP URL"
151
180
  )
152
- )
153
181
  else:
154
182
  command = self.command_or_url
155
183
  arguments = self.args
@@ -194,9 +222,18 @@ class MCPClient(BaseToolkit):
194
222
 
195
223
  async def disconnect(self):
196
224
  r"""Explicitly disconnect from the MCP server."""
225
+ # If the server is not connected, do nothing
226
+ if not self._is_connected:
227
+ return
197
228
  self._is_connected = False
198
- await self._exit_stack.aclose()
199
- self._session = None
229
+
230
+ try:
231
+ await self._exit_stack.aclose()
232
+ except Exception as e:
233
+ logger.warning(f"{e}")
234
+ finally:
235
+ self._exit_stack = AsyncExitStack()
236
+ self._session = None
200
237
 
201
238
  @asynccontextmanager
202
239
  async def connection(self):
@@ -212,7 +249,10 @@ class MCPClient(BaseToolkit):
212
249
  await self.connect()
213
250
  yield self
214
251
  finally:
215
- await self.disconnect()
252
+ try:
253
+ await self.disconnect()
254
+ except Exception as e:
255
+ logger.warning(f"Error: {e}")
216
256
 
217
257
  async def list_mcp_tools(self) -> Union[str, "ListToolsResult"]:
218
258
  r"""Retrieves the list of available tools from the connected MCP
@@ -435,6 +475,7 @@ class MCPClient(BaseToolkit):
435
475
  env: Optional[Dict[str, str]] = None,
436
476
  timeout: Optional[float] = None,
437
477
  headers: Optional[Dict[str, str]] = None,
478
+ mode: Optional[str] = None,
438
479
  ) -> "MCPClient":
439
480
  r"""Factory method that creates and connects to the MCP server.
440
481
 
@@ -452,6 +493,10 @@ class MCPClient(BaseToolkit):
452
493
  (default: :obj:`None`)
453
494
  headers (Optional[Dict[str, str]]): Headers for the HTTP request.
454
495
  (default: :obj:`None`)
496
+ mode (Optional[str]): Connection mode. Can be "sse" for
497
+ Server-Sent Events, "streamable-http" for
498
+ streaming HTTP, or None for stdio mode.
499
+ (default: :obj:`None`)
455
500
 
456
501
  Returns:
457
502
  MCPClient: A fully initialized and connected MCPClient instance.
@@ -465,6 +510,7 @@ class MCPClient(BaseToolkit):
465
510
  env=env,
466
511
  timeout=timeout,
467
512
  headers=headers,
513
+ mode=mode,
468
514
  )
469
515
  try:
470
516
  await client.connect()
@@ -599,7 +645,6 @@ class MCPToolkit(BaseToolkit):
599
645
  if config_dict:
600
646
  self.servers.extend(self._load_servers_from_dict(config_dict))
601
647
 
602
- self._exit_stack = AsyncExitStack()
603
648
  self._connected = False
604
649
 
605
650
  def _load_servers_from_config(
@@ -675,6 +720,7 @@ class MCPToolkit(BaseToolkit):
675
720
  env={**os.environ, **cfg.get("env", {})},
676
721
  timeout=cfg.get("timeout", None),
677
722
  headers=headers,
723
+ mode=cfg.get("mode", None),
678
724
  strict=strict,
679
725
  )
680
726
  all_servers.append(server)
@@ -691,7 +737,6 @@ class MCPToolkit(BaseToolkit):
691
737
  logger.warning("MCPToolkit is already connected")
692
738
  return self
693
739
 
694
- self._exit_stack = AsyncExitStack()
695
740
  try:
696
741
  # Sequentially connect to each server
697
742
  for server in self.servers:
@@ -712,7 +757,6 @@ class MCPToolkit(BaseToolkit):
712
757
  for server in self.servers:
713
758
  await server.disconnect()
714
759
  self._connected = False
715
- await self._exit_stack.aclose()
716
760
 
717
761
  @asynccontextmanager
718
762
  async def connection(self) -> AsyncGenerator["MCPToolkit", None]:
@@ -191,35 +191,47 @@ var MultimodalWebSurfer = MultimodalWebSurfer || (function() {
191
191
  labelElements(getInteractiveElements());
192
192
  let elements = document.querySelectorAll("[__elementId]");
193
193
  let results = {};
194
- for (let i=0; i<elements.length; i++) {
195
- let key = elements[i].getAttribute("__elementId");
196
- let rects = elements[i].getClientRects();
197
- let ariaRole = getApproximateAriaRole(elements[i]);
198
- let ariaName = getApproximateAriaName(elements[i]);
199
- let vScrollable = elements[i].scrollHeight - elements[i].clientHeight >= 1;
200
-
201
- let record = {
202
- "tag_name": ariaRole[1],
203
- "role": ariaRole[0],
204
- "aria-name": ariaName,
205
- "v-scrollable": vScrollable,
206
- "rects": []
207
- };
208
-
209
- for (const rect of rects) {
210
- let x = rect.left + rect.width/2;
211
- let y = rect.top + rect.height/2;
212
- if (isTopmost(elements[i], x, y)) {
213
- record["rects"].push(JSON.parse(JSON.stringify(rect)));
214
- }
215
- }
216
-
217
- if (record["rects"].length > 0) {
218
- results[key] = record;
219
- }
194
+ let scale = window.devicePixelRatio || 1;
195
+
196
+ for (let i = 0; i < elements.length; i++) {
197
+ let key = elements[i].getAttribute("__elementId");
198
+ let rects = elements[i].getClientRects();
199
+ let ariaRole = getApproximateAriaRole(elements[i]);
200
+ let ariaName = getApproximateAriaName(elements[i]);
201
+ let vScrollable = elements[i].scrollHeight - elements[i].clientHeight >= 1;
202
+
203
+ let record = {
204
+ "tag_name": ariaRole[1],
205
+ "role": ariaRole[0],
206
+ "aria-name": ariaName,
207
+ "v-scrollable": vScrollable,
208
+ "rects": []
209
+ };
210
+
211
+ for (const rect of rects) {
212
+ let x = rect.left + rect.width / 2;
213
+ let y = rect.top + rect.height / 2;
214
+ if (isTopmost(elements[i], x, y)) {
215
+ record["rects"].push({
216
+ x: rect.x * scale,
217
+ y: rect.y * scale,
218
+ width: rect.width * scale,
219
+ height: rect.height * scale,
220
+ top: rect.top * scale,
221
+ left: rect.left * scale,
222
+ right: rect.right * scale,
223
+ bottom: rect.bottom * scale
224
+ });
225
+ }
226
+ }
227
+
228
+ if (record["rects"].length > 0) {
229
+ results[key] = record;
230
+ }
220
231
  }
232
+
221
233
  return results;
222
- };
234
+ };
223
235
 
224
236
  let getVisualViewport = function() {
225
237
  let vv = window.visualViewport;
@@ -373,4 +385,4 @@ var MultimodalWebSurfer = MultimodalWebSurfer || (function() {
373
385
  getFocusedElementId: getFocusedElementId,
374
386
  getPageMetadata: getPageMetadata,
375
387
  };
376
- })();
388
+ })();
@@ -18,7 +18,6 @@ from http.client import responses
18
18
  from typing import Any, Dict, List, Optional, Union
19
19
 
20
20
  import requests
21
- from requests_oauthlib import OAuth1
22
21
 
23
22
  from camel.logger import get_logger
24
23
  from camel.toolkits import FunctionTool
@@ -76,6 +75,8 @@ def create_tweet(
76
75
  Reference:
77
76
  https://developer.x.com/en/docs/x-api/tweets/manage-tweets/api-reference/post-tweets
78
77
  """
78
+ from requests_oauthlib import OAuth1
79
+
79
80
  auth = OAuth1(
80
81
  os.getenv("TWITTER_CONSUMER_KEY"),
81
82
  os.getenv("TWITTER_CONSUMER_SECRET"),
@@ -160,6 +161,8 @@ def delete_tweet(tweet_id: str) -> str:
160
161
  Reference:
161
162
  https://developer.x.com/en/docs/x-api/tweets/manage-tweets/api-reference/delete-tweets-id
162
163
  """
164
+ from requests_oauthlib import OAuth1
165
+
163
166
  auth = OAuth1(
164
167
  os.getenv("TWITTER_CONSUMER_KEY"),
165
168
  os.getenv("TWITTER_CONSUMER_SECRET"),
@@ -263,6 +266,8 @@ def _get_user_info(username: Optional[str] = None) -> str:
263
266
  Returns:
264
267
  str: A formatted report of the user's Twitter profile information.
265
268
  """
269
+ from requests_oauthlib import OAuth1
270
+
266
271
  oauth = OAuth1(
267
272
  os.getenv("TWITTER_CONSUMER_KEY"),
268
273
  os.getenv("TWITTER_CONSUMER_SECRET"),