camel-ai 0.2.71a5__py3-none-any.whl → 0.2.71a6__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.

@@ -12,7 +12,7 @@
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
  import asyncio
15
- from typing import TYPE_CHECKING, Any, Dict
15
+ from typing import TYPE_CHECKING, Any, Dict, Optional
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from playwright.async_api import Page
@@ -26,24 +26,33 @@ class ActionExecutor:
26
26
  SHORT_TIMEOUT = 2000 # 2 seconds
27
27
  MAX_SCROLL_AMOUNT = 5000 # Maximum scroll distance in pixels
28
28
 
29
- def __init__(self, page: "Page"):
29
+ def __init__(self, page: "Page", session: Optional[Any] = None):
30
30
  self.page = page
31
+ self.session = session # HybridBrowserSession instance
31
32
 
32
33
  # ------------------------------------------------------------------
33
34
  # Public helpers
34
35
  # ------------------------------------------------------------------
35
- async def execute(self, action: Dict[str, Any]) -> str:
36
- r"""Execute an action and return the result description."""
36
+ async def execute(self, action: Dict[str, Any]) -> Dict[str, Any]:
37
+ r"""Execute an action and return detailed result information."""
37
38
  if not action:
38
- return "No action to execute"
39
+ return {
40
+ "success": False,
41
+ "message": "No action to execute",
42
+ "details": {},
43
+ }
39
44
 
40
45
  action_type = action.get("type")
41
46
  if not action_type:
42
- return "Error: action has no type"
47
+ return {
48
+ "success": False,
49
+ "message": "Error: action has no type",
50
+ "details": {},
51
+ }
43
52
 
44
53
  try:
45
54
  # small helper to ensure basic stability
46
- await self._wait_dom_stable()
55
+ # await self._wait_dom_stable()
47
56
 
48
57
  handler = {
49
58
  "click": self._click,
@@ -56,24 +65,41 @@ class ActionExecutor:
56
65
  }.get(action_type)
57
66
 
58
67
  if handler is None:
59
- return f"Error: Unknown action type '{action_type}'"
68
+ return {
69
+ "success": False,
70
+ "message": f"Error: Unknown action type '{action_type}'",
71
+ "details": {"action_type": action_type},
72
+ }
60
73
 
61
- return await handler(action)
74
+ result = await handler(action)
75
+ return {
76
+ "success": True,
77
+ "message": result["message"],
78
+ "details": result.get("details", {}),
79
+ }
62
80
  except Exception as exc:
63
- return f"Error executing {action_type}: {exc}"
81
+ return {
82
+ "success": False,
83
+ "message": f"Error executing {action_type}: {exc}",
84
+ "details": {"action_type": action_type, "error": str(exc)},
85
+ }
64
86
 
65
87
  # ------------------------------------------------------------------
66
88
  # Internal handlers
67
89
  # ------------------------------------------------------------------
68
- async def _click(self, action: Dict[str, Any]) -> str:
69
- r"""Handle click actions with multiple fallback strategies."""
90
+ async def _click(self, action: Dict[str, Any]) -> Dict[str, Any]:
91
+ r"""Handle click actions with new tab support for any clickable
92
+ element."""
70
93
  ref = action.get("ref")
71
94
  text = action.get("text")
72
95
  selector = action.get("selector")
73
96
  if not (ref or text or selector):
74
- return "Error: click requires ref/text/selector"
97
+ return {
98
+ "message": "Error: click requires ref/text/selector",
99
+ "details": {"error": "missing_selector"},
100
+ }
75
101
 
76
- # Build strategies in priority order: ref > selector > text
102
+ # Build strategies in priority order
77
103
  strategies = []
78
104
  if ref:
79
105
  strategies.append(f"[aria-ref='{ref}']")
@@ -82,90 +108,230 @@ class ActionExecutor:
82
108
  if text:
83
109
  strategies.append(f'text="{text}"')
84
110
 
85
- # Strategy 1: Try Playwright force click for each selector
86
- for sel in strategies:
87
- try:
88
- if await self.page.locator(sel).count() > 0:
89
- await self.page.click(
90
- sel, timeout=self.DEFAULT_TIMEOUT, force=True
91
- )
92
- return f"Clicked element via force: {sel}"
93
- except Exception:
94
- continue
111
+ details: Dict[str, Any] = {
112
+ "ref": ref,
113
+ "selector": selector,
114
+ "text": text,
115
+ "strategies_tried": [],
116
+ "successful_strategy": None,
117
+ "click_method": None,
118
+ "new_tab_created": False,
119
+ }
95
120
 
96
- # Strategy 2: Try JavaScript click as fallback
121
+ # Find the first valid selector
122
+ found_selector = None
97
123
  for sel in strategies:
98
- try:
99
- await self.page.locator(sel).first.evaluate("el => el.click()")
100
- await asyncio.sleep(0.1) # Brief wait for effects
101
- return f"Clicked element via JS: {sel}"
102
- except Exception:
103
- continue
124
+ if await self.page.locator(sel).count() > 0:
125
+ found_selector = sel
126
+ break
127
+
128
+ if not found_selector:
129
+ details['error'] = "Element not found with any strategy"
130
+ return {
131
+ "message": "Error: Click failed, element not found",
132
+ "details": details,
133
+ }
134
+
135
+ element = self.page.locator(found_selector).first
136
+ details['successful_strategy'] = found_selector
137
+
138
+ # Attempt ctrl+click first (always)
139
+ try:
140
+ if self.session:
141
+ async with self.page.context.expect_page(
142
+ timeout=self.SHORT_TIMEOUT
143
+ ) as new_page_info:
144
+ await element.click(modifiers=["ControlOrMeta"])
145
+ new_page = await new_page_info.value
146
+ await new_page.wait_for_load_state('domcontentloaded')
147
+ new_tab_index = await self.session.register_page(new_page)
148
+ if new_tab_index is not None:
149
+ await self.session.switch_to_tab(new_tab_index)
150
+ self.page = new_page
151
+ details.update(
152
+ {
153
+ "click_method": "ctrl_click_new_tab",
154
+ "new_tab_created": True,
155
+ "new_tab_index": new_tab_index,
156
+ }
157
+ )
158
+ return {
159
+ "message": f"Clicked element (ctrl click), opened in new "
160
+ f"tab {new_tab_index}",
161
+ "details": details,
162
+ }
163
+ else:
164
+ await element.click(modifiers=["ControlOrMeta"])
165
+ details["click_method"] = "ctrl_click_no_session"
166
+ return {
167
+ "message": f"Clicked element (ctrl click, no"
168
+ f" session): {found_selector}",
169
+ "details": details,
170
+ }
171
+ except asyncio.TimeoutError:
172
+ # No new tab was opened, click may have still worked
173
+ details["click_method"] = "ctrl_click_same_tab"
174
+ return {
175
+ "message": f"Clicked element (ctrl click, "
176
+ f"same tab): {found_selector}",
177
+ "details": details,
178
+ }
179
+ except Exception as e:
180
+ details['strategies_tried'].append(
181
+ {
182
+ 'selector': found_selector,
183
+ 'method': 'ctrl_click',
184
+ 'error': str(e),
185
+ }
186
+ )
187
+ # Fall through to fallback
104
188
 
105
- return "Error: All click strategies failed"
189
+ # Fallback to normal force click if ctrl+click fails
190
+ try:
191
+ await element.click(force=True, timeout=self.DEFAULT_TIMEOUT)
192
+ details["click_method"] = "playwright_force_click"
193
+ return {
194
+ "message": f"Fallback clicked element: {found_selector}",
195
+ "details": details,
196
+ }
197
+ except Exception as e:
198
+ details["click_method"] = "playwright_force_click_failed"
199
+ details["error"] = str(e)
200
+ return {
201
+ "message": f"Error: All click strategies "
202
+ f"failed for {found_selector}",
203
+ "details": details,
204
+ }
106
205
 
107
- async def _type(self, action: Dict[str, Any]) -> str:
206
+ async def _type(self, action: Dict[str, Any]) -> Dict[str, Any]:
108
207
  r"""Handle typing text into input fields."""
109
208
  ref = action.get("ref")
110
209
  selector = action.get("selector")
111
210
  text = action.get("text", "")
112
211
  if not (ref or selector):
113
- return "Error: type requires ref/selector"
212
+ return {
213
+ "message": "Error: type requires ref/selector",
214
+ "details": {"error": "missing_selector"},
215
+ }
216
+
114
217
  target = selector or f"[aria-ref='{ref}']"
218
+ details = {
219
+ "ref": ref,
220
+ "selector": selector,
221
+ "target": target,
222
+ "text": text,
223
+ "text_length": len(text),
224
+ }
225
+
115
226
  try:
116
227
  await self.page.fill(target, text, timeout=self.SHORT_TIMEOUT)
117
- return f"Typed '{text}' into {target}"
228
+ return {
229
+ "message": f"Typed '{text}' into {target}",
230
+ "details": details,
231
+ }
118
232
  except Exception as exc:
119
- return f"Type failed: {exc}"
233
+ details["error"] = str(exc)
234
+ return {"message": f"Type failed: {exc}", "details": details}
120
235
 
121
- async def _select(self, action: Dict[str, Any]) -> str:
236
+ async def _select(self, action: Dict[str, Any]) -> Dict[str, Any]:
122
237
  r"""Handle selecting options from dropdowns."""
123
238
  ref = action.get("ref")
124
239
  selector = action.get("selector")
125
240
  value = action.get("value", "")
126
241
  if not (ref or selector):
127
- return "Error: select requires ref/selector"
242
+ return {
243
+ "message": "Error: select requires ref/selector",
244
+ "details": {"error": "missing_selector"},
245
+ }
246
+
128
247
  target = selector or f"[aria-ref='{ref}']"
248
+ details = {
249
+ "ref": ref,
250
+ "selector": selector,
251
+ "target": target,
252
+ "value": value,
253
+ }
254
+
129
255
  try:
130
256
  await self.page.select_option(
131
257
  target, value, timeout=self.DEFAULT_TIMEOUT
132
258
  )
133
- return f"Selected '{value}' in {target}"
259
+ return {
260
+ "message": f"Selected '{value}' in {target}",
261
+ "details": details,
262
+ }
134
263
  except Exception as exc:
135
- return f"Select failed: {exc}"
264
+ details["error"] = str(exc)
265
+ return {"message": f"Select failed: {exc}", "details": details}
136
266
 
137
- async def _wait(self, action: Dict[str, Any]) -> str:
267
+ async def _wait(self, action: Dict[str, Any]) -> Dict[str, Any]:
138
268
  r"""Handle wait actions."""
269
+ details: Dict[str, Any] = {
270
+ "wait_type": None,
271
+ "timeout": None,
272
+ "selector": None,
273
+ }
274
+
139
275
  if "timeout" in action:
140
276
  ms = int(action["timeout"])
277
+ details["wait_type"] = "timeout"
278
+ details["timeout"] = ms
141
279
  await asyncio.sleep(ms / 1000)
142
- return f"Waited {ms}ms"
280
+ return {"message": f"Waited {ms}ms", "details": details}
143
281
  if "selector" in action:
144
282
  sel = action["selector"]
283
+ details["wait_type"] = "selector"
284
+ details["selector"] = sel
145
285
  await self.page.wait_for_selector(
146
286
  sel, timeout=self.DEFAULT_TIMEOUT
147
287
  )
148
- return f"Waited for {sel}"
149
- return "Error: wait requires timeout/selector"
288
+ return {"message": f"Waited for {sel}", "details": details}
289
+ return {
290
+ "message": "Error: wait requires timeout/selector",
291
+ "details": details,
292
+ }
150
293
 
151
- async def _extract(self, action: Dict[str, Any]) -> str:
294
+ async def _extract(self, action: Dict[str, Any]) -> Dict[str, Any]:
152
295
  r"""Handle text extraction from elements."""
153
296
  ref = action.get("ref")
154
297
  if not ref:
155
- return "Error: extract requires ref"
298
+ return {
299
+ "message": "Error: extract requires ref",
300
+ "details": {"error": "missing_ref"},
301
+ }
302
+
156
303
  target = f"[aria-ref='{ref}']"
304
+ details = {"ref": ref, "target": target}
305
+
157
306
  await self.page.wait_for_selector(target, timeout=self.DEFAULT_TIMEOUT)
158
307
  txt = await self.page.text_content(target)
159
- return f"Extracted: {txt[:100] if txt else 'None'}"
160
308
 
161
- async def _scroll(self, action: Dict[str, Any]) -> str:
309
+ details["extracted_text"] = txt
310
+ details["text_length"] = len(txt) if txt else 0
311
+
312
+ return {
313
+ "message": f"Extracted: {txt[:100] if txt else 'None'}",
314
+ "details": details,
315
+ }
316
+
317
+ async def _scroll(self, action: Dict[str, Any]) -> Dict[str, Any]:
162
318
  r"""Handle page scrolling with safe parameter validation."""
163
319
  direction = action.get("direction", "down")
164
320
  amount = action.get("amount", 300)
165
321
 
322
+ details = {
323
+ "direction": direction,
324
+ "requested_amount": amount,
325
+ "actual_amount": None,
326
+ "scroll_offset": None,
327
+ }
328
+
166
329
  # Validate inputs to prevent injection
167
330
  if direction not in ("up", "down"):
168
- return "Error: direction must be 'up' or 'down'"
331
+ return {
332
+ "message": "Error: direction must be 'up' or 'down'",
333
+ "details": details,
334
+ }
169
335
 
170
336
  try:
171
337
  # Safely convert amount to integer and clamp to reasonable range
@@ -174,28 +340,37 @@ class ActionExecutor:
174
340
  -self.MAX_SCROLL_AMOUNT,
175
341
  min(self.MAX_SCROLL_AMOUNT, amount_int),
176
342
  ) # Clamp to MAX_SCROLL_AMOUNT range
343
+ details["actual_amount"] = amount_int
177
344
  except (ValueError, TypeError):
178
- return "Error: amount must be a valid number"
345
+ return {
346
+ "message": "Error: amount must be a valid number",
347
+ "details": details,
348
+ }
179
349
 
180
350
  # Use safe evaluation with bound parameters
181
351
  scroll_offset = amount_int if direction == "down" else -amount_int
352
+ details["scroll_offset"] = scroll_offset
353
+
182
354
  await self.page.evaluate(
183
355
  "offset => window.scrollBy(0, offset)", scroll_offset
184
356
  )
185
357
  await asyncio.sleep(0.5)
186
- return f"Scrolled {direction} by {abs(amount_int)}px"
358
+ return {
359
+ "message": f"Scrolled {direction} by {abs(amount_int)}px",
360
+ "details": details,
361
+ }
187
362
 
188
- async def _enter(self, action: Dict[str, Any]) -> str:
189
- r"""Handle Enter key press actions."""
190
- ref = action.get("ref")
191
- selector = action.get("selector")
192
- if ref:
193
- await self.page.focus(f"[aria-ref='{ref}']")
194
- elif selector:
195
- await self.page.focus(selector)
363
+ async def _enter(self, action: Dict[str, Any]) -> Dict[str, Any]:
364
+ r"""Handle Enter key press on the currently focused element."""
365
+ details = {"action_type": "enter", "target": "focused_element"}
366
+
367
+ # Press Enter on whatever element currently has focus
196
368
  await self.page.keyboard.press("Enter")
197
369
  await asyncio.sleep(0.3)
198
- return "Pressed Enter"
370
+ return {
371
+ "message": "Pressed Enter on focused element",
372
+ "details": details,
373
+ }
199
374
 
200
375
  # utilities
201
376
  async def _wait_dom_stable(self) -> None:
@@ -13,14 +13,14 @@
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
  import json
15
15
  import re
16
- from typing import TYPE_CHECKING, Any, Dict, List, Optional
16
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
17
17
 
18
18
  from camel.logger import get_logger
19
19
  from camel.models import BaseModelBackend, ModelFactory
20
20
  from camel.types import ModelPlatformType, ModelType
21
21
 
22
22
  from .actions import ActionExecutor
23
- from .browser_session import NVBrowserSession
23
+ from .browser_session import HybridBrowserSession
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  from camel.agents import ChatAgent
@@ -79,10 +79,11 @@ what was accomplished
79
79
  *,
80
80
  user_data_dir: Optional[str] = None,
81
81
  headless: bool = False,
82
+ stealth: bool = False,
82
83
  model_backend: Optional[BaseModelBackend] = None,
83
84
  ):
84
- self._session = NVBrowserSession(
85
- headless=headless, user_data_dir=user_data_dir
85
+ self._session = HybridBrowserSession(
86
+ headless=headless, user_data_dir=user_data_dir, stealth=stealth
86
87
  )
87
88
  from camel.agents import ChatAgent
88
89
 
@@ -101,7 +102,7 @@ what was accomplished
101
102
  async def navigate(self, url: str) -> str:
102
103
  r"""Navigate to a URL and return the snapshot."""
103
104
  try:
104
- # NVBrowserSession handles waits internally
105
+ # HybridBrowserSession handles waits internally
105
106
  logger.debug("Navigated to URL: %s", url)
106
107
  await self._session.visit(url)
107
108
  return await self._session.get_snapshot(force_refresh=True)
@@ -240,11 +241,25 @@ what was accomplished
240
241
  result = await self._run_action(action)
241
242
  logger.debug("Executed action: %s | Result: %s", action, result)
242
243
 
244
+ success = False
245
+ result_for_history = ""
246
+
247
+ if isinstance(result, str):
248
+ success = "Error" not in result
249
+ result_for_history = result
250
+ elif isinstance(result, dict):
251
+ success = result.get('success', False)
252
+ result_for_history = result.get('message', str(result))
253
+ else:
254
+ # Fallback case
255
+ success = False
256
+ result_for_history = str(result)
257
+
243
258
  self.action_history.append(
244
259
  {
245
260
  "action": action,
246
- "result": result,
247
- "success": "Error" not in result,
261
+ "result": result_for_history,
262
+ "success": success,
248
263
  }
249
264
  )
250
265
 
@@ -277,7 +292,9 @@ what was accomplished
277
292
 
278
293
  logger.info("Process completed with %d steps", steps)
279
294
 
280
- async def _run_action(self, action: Dict[str, Any]) -> str:
295
+ async def _run_action(
296
+ self, action: Dict[str, Any]
297
+ ) -> Union[str, Dict[str, Any]]:
281
298
  r"""Execute a single action and return the result."""
282
299
  if action.get("type") == "navigate":
283
300
  return await self.navigate(action.get("url", ""))