camel-ai 0.2.72a10__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 (52) 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/notion_mcp_toolkit.py +234 -0
  35. camel/toolkits/screenshot_toolkit.py +116 -31
  36. camel/toolkits/search_toolkit.py +20 -2
  37. camel/toolkits/slack_toolkit.py +43 -48
  38. camel/toolkits/terminal_toolkit.py +288 -46
  39. camel/toolkits/video_analysis_toolkit.py +13 -13
  40. camel/toolkits/video_download_toolkit.py +11 -11
  41. camel/toolkits/web_deploy_toolkit.py +207 -12
  42. camel/types/enums.py +6 -0
  43. {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73.dist-info}/METADATA +49 -9
  44. {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73.dist-info}/RECORD +52 -35
  45. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/actions.py +0 -0
  46. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/agent.py +0 -0
  47. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/browser_session.py +0 -0
  48. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/snapshot.py +0 -0
  49. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/stealth_script.js +0 -0
  50. /camel/toolkits/{hybrid_browser_toolkit → hybrid_browser_toolkit_py}/unified_analyzer.js +0 -0
  51. {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73.dist-info}/WHEEL +0 -0
  52. {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,582 @@
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 asyncio
16
+ import datetime
17
+ import json
18
+ import os
19
+ import subprocess
20
+ import time
21
+ import uuid
22
+ from functools import wraps
23
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
24
+
25
+ if TYPE_CHECKING:
26
+ import websockets
27
+ else:
28
+ try:
29
+ import websockets
30
+ except ImportError:
31
+ websockets = None
32
+
33
+ from camel.logger import get_logger
34
+ from camel.utils.tool_result import ToolResult
35
+
36
+ logger = get_logger(__name__)
37
+
38
+
39
+ def action_logger(func):
40
+ """Decorator to add logging to action methods."""
41
+
42
+ @wraps(func)
43
+ async def wrapper(self, *args, **kwargs):
44
+ action_name = func.__name__
45
+ start_time = time.time()
46
+
47
+ # Log inputs (skip self)
48
+ inputs = {
49
+ "args": args,
50
+ "kwargs": kwargs,
51
+ }
52
+
53
+ try:
54
+ # Execute the original function
55
+ result = await func(self, *args, **kwargs)
56
+ execution_time = time.time() - start_time
57
+
58
+ # Extract page load time if available
59
+ page_load_time = None
60
+ if isinstance(result, dict) and 'page_load_time_ms' in result:
61
+ page_load_time = result['page_load_time_ms'] / 1000.0
62
+
63
+ # Log success
64
+ await self._log_action(
65
+ action_name=action_name,
66
+ inputs=inputs,
67
+ outputs=result,
68
+ execution_time=execution_time,
69
+ page_load_time=page_load_time,
70
+ )
71
+
72
+ return result
73
+
74
+ except Exception as e:
75
+ execution_time = time.time() - start_time
76
+ error_msg = f"{type(e).__name__}: {e!s}"
77
+
78
+ # Log error
79
+ await self._log_action(
80
+ action_name=action_name,
81
+ inputs=inputs,
82
+ outputs=None,
83
+ execution_time=execution_time,
84
+ error=error_msg,
85
+ )
86
+
87
+ raise
88
+
89
+ return wrapper
90
+
91
+
92
+ class WebSocketBrowserWrapper:
93
+ """Python wrapper for the TypeScript hybrid browser
94
+ toolkit implementation using WebSocket."""
95
+
96
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
97
+ """Initialize the wrapper.
98
+
99
+ Args:
100
+ config: Configuration dictionary for the browser toolkit
101
+ """
102
+ if websockets is None:
103
+ raise ImportError(
104
+ "websockets package is required for WebSocket communication. "
105
+ "Install with: pip install websockets"
106
+ )
107
+
108
+ self.config = config or {}
109
+ self.ts_dir = os.path.join(os.path.dirname(__file__), 'ts')
110
+ self.process: Optional[subprocess.Popen] = None
111
+ self.websocket = None
112
+ self.server_port = None
113
+ self._send_lock = asyncio.Lock() # Lock for sending messages
114
+ self._receive_task = None # Background task for receiving messages
115
+ self._pending_responses: Dict[
116
+ str, asyncio.Future[Dict[str, Any]]
117
+ ] = {} # Message ID -> Future
118
+
119
+ # Logging configuration
120
+ self.browser_log_to_file = (config or {}).get(
121
+ 'browser_log_to_file', False
122
+ )
123
+ self.session_id = (config or {}).get('session_id', 'default')
124
+ self.log_file_path: Optional[str] = None
125
+ self.log_buffer: List[Dict[str, Any]] = []
126
+
127
+ # Set up log file if needed
128
+ if self.browser_log_to_file:
129
+ log_dir = "browser_log"
130
+ os.makedirs(log_dir, exist_ok=True)
131
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
132
+ self.log_file_path = os.path.join(
133
+ log_dir,
134
+ f"hybrid_browser_toolkit_ws_{timestamp}_{self.session_id}.log",
135
+ )
136
+
137
+ async def __aenter__(self):
138
+ """Async context manager entry."""
139
+ await self.start()
140
+ return self
141
+
142
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
143
+ """Async context manager exit."""
144
+ await self.stop()
145
+
146
+ async def start(self):
147
+ """Start the WebSocket server and connect to it."""
148
+ # Check if npm is installed
149
+ npm_check = subprocess.run(
150
+ ['npm', '--version'],
151
+ capture_output=True,
152
+ text=True,
153
+ )
154
+ if npm_check.returncode != 0:
155
+ raise RuntimeError(
156
+ "npm is not installed or not in PATH. "
157
+ "Please install Node.js and npm from https://nodejs.org/ "
158
+ "to use the hybrid browser toolkit."
159
+ )
160
+
161
+ # Check if node is installed
162
+ node_check = subprocess.run(
163
+ ['node', '--version'],
164
+ capture_output=True,
165
+ text=True,
166
+ )
167
+ if node_check.returncode != 0:
168
+ raise RuntimeError(
169
+ "node is not installed or not in PATH. "
170
+ "Please install Node.js from https://nodejs.org/ "
171
+ "to use the hybrid browser toolkit."
172
+ )
173
+
174
+ # Check if node_modules exists (dependencies installed)
175
+ node_modules_path = os.path.join(self.ts_dir, 'node_modules')
176
+ if not os.path.exists(node_modules_path):
177
+ logger.warning("Node modules not found. Running npm install...")
178
+ install_result = subprocess.run(
179
+ ['npm', 'install'],
180
+ cwd=self.ts_dir,
181
+ capture_output=True,
182
+ text=True,
183
+ )
184
+ if install_result.returncode != 0:
185
+ logger.error(f"npm install failed: {install_result.stderr}")
186
+ raise RuntimeError(
187
+ f"Failed to install npm dependencies: {install_result.stderr}\n" # noqa:E501
188
+ f"Please run 'npm install' in {self.ts_dir} manually."
189
+ )
190
+ logger.info("npm dependencies installed successfully")
191
+
192
+ # Ensure the TypeScript code is built
193
+ build_result = subprocess.run(
194
+ ['npm', 'run', 'build'],
195
+ cwd=self.ts_dir,
196
+ capture_output=True,
197
+ text=True,
198
+ )
199
+ if build_result.returncode != 0:
200
+ logger.error(f"TypeScript build failed: {build_result.stderr}")
201
+ raise RuntimeError(
202
+ f"TypeScript build failed: {build_result.stderr}"
203
+ )
204
+
205
+ # Start the WebSocket server
206
+ self.process = subprocess.Popen(
207
+ ['node', 'websocket-server.js'],
208
+ cwd=self.ts_dir,
209
+ stdout=subprocess.PIPE,
210
+ stderr=subprocess.PIPE,
211
+ text=True,
212
+ )
213
+
214
+ # Wait for server to output the port
215
+ server_ready = False
216
+ timeout = 10 # 10 seconds timeout
217
+ start_time = time.time()
218
+
219
+ while not server_ready and time.time() - start_time < timeout:
220
+ if self.process.poll() is not None:
221
+ # Process died
222
+ stderr = self.process.stderr.read()
223
+ raise RuntimeError(
224
+ f"WebSocket server failed to start: {stderr}"
225
+ )
226
+
227
+ try:
228
+ line = self.process.stdout.readline()
229
+ if line.startswith('SERVER_READY:'):
230
+ self.server_port = int(line.split(':')[1].strip())
231
+ server_ready = True
232
+ logger.info(
233
+ f"WebSocket server ready on port {self.server_port}"
234
+ )
235
+ except (ValueError, IndexError):
236
+ continue
237
+
238
+ if not server_ready:
239
+ self.process.kill()
240
+ raise RuntimeError(
241
+ "WebSocket server failed to start within timeout"
242
+ )
243
+
244
+ # Connect to the WebSocket server
245
+ try:
246
+ self.websocket = await websockets.connect(
247
+ f"ws://localhost:{self.server_port}",
248
+ ping_interval=30,
249
+ ping_timeout=10,
250
+ max_size=50 * 1024 * 1024, # 50MB limit to match server
251
+ )
252
+ logger.info("Connected to WebSocket server")
253
+ except Exception as e:
254
+ self.process.kill()
255
+ raise RuntimeError(
256
+ f"Failed to connect to WebSocket server: {e}"
257
+ ) from e
258
+
259
+ # Start the background receiver task
260
+ self._receive_task = asyncio.create_task(self._receive_loop())
261
+
262
+ # Initialize the browser toolkit
263
+ await self._send_command('init', self.config)
264
+
265
+ async def stop(self):
266
+ """Stop the WebSocket connection and server."""
267
+ # Cancel the receiver task
268
+ if self._receive_task and not self._receive_task.done():
269
+ self._receive_task.cancel()
270
+ try:
271
+ await self._receive_task
272
+ except asyncio.CancelledError:
273
+ pass
274
+
275
+ if self.websocket:
276
+ try:
277
+ await self._send_command('shutdown', {})
278
+ await self.websocket.close()
279
+ except Exception as e:
280
+ logger.warning(f"Error during websocket shutdown: {e}")
281
+ finally:
282
+ self.websocket = None
283
+
284
+ if self.process:
285
+ try:
286
+ self.process.terminate()
287
+ self.process.wait(timeout=5)
288
+ except subprocess.TimeoutExpired:
289
+ self.process.kill()
290
+ self.process.wait()
291
+ except Exception as e:
292
+ logger.warning(f"Error terminating process: {e}")
293
+ finally:
294
+ self.process = None
295
+
296
+ async def _log_action(
297
+ self,
298
+ action_name: str,
299
+ inputs: Dict[str, Any],
300
+ outputs: Any,
301
+ execution_time: float,
302
+ page_load_time: Optional[float] = None,
303
+ error: Optional[str] = None,
304
+ ) -> None:
305
+ """Log action details with comprehensive
306
+ information including detailed timing breakdown."""
307
+ if not self.browser_log_to_file or not self.log_file_path:
308
+ return
309
+
310
+ # Create log entry
311
+ log_entry = {
312
+ "timestamp": datetime.datetime.now().isoformat(),
313
+ "session_id": self.session_id,
314
+ "action": action_name,
315
+ "execution_time_ms": round(execution_time * 1000, 2),
316
+ "inputs": inputs,
317
+ }
318
+
319
+ if error:
320
+ log_entry["error"] = error
321
+ else:
322
+ # Handle ToolResult objects for JSON serialization
323
+ if hasattr(outputs, 'text') and hasattr(outputs, 'images'):
324
+ # This is a ToolResult object
325
+ log_entry["outputs"] = {
326
+ "text": outputs.text,
327
+ "images_count": len(outputs.images)
328
+ if outputs.images
329
+ else 0,
330
+ }
331
+ else:
332
+ log_entry["outputs"] = outputs
333
+
334
+ if page_load_time is not None:
335
+ log_entry["page_load_time_ms"] = round(page_load_time * 1000, 2)
336
+
337
+ # Write to log file
338
+ try:
339
+ with open(self.log_file_path, 'a', encoding='utf-8') as f:
340
+ f.write(
341
+ json.dumps(log_entry, ensure_ascii=False, indent=2) + '\n'
342
+ )
343
+ except Exception as e:
344
+ logger.error(f"Failed to write to log file: {e}")
345
+
346
+ async def _receive_loop(self):
347
+ r"""Background task to receive messages from WebSocket."""
348
+ try:
349
+ while self.websocket:
350
+ try:
351
+ response_data = await self.websocket.recv()
352
+ response = json.loads(response_data)
353
+
354
+ message_id = response.get('id')
355
+ if message_id and message_id in self._pending_responses:
356
+ # Set the result for the waiting coroutine
357
+ future = self._pending_responses.pop(message_id)
358
+ if not future.done():
359
+ future.set_result(response)
360
+ else:
361
+ # Log unexpected messages
362
+ logger.warning(
363
+ f"Received unexpected message: {response}"
364
+ )
365
+
366
+ except asyncio.CancelledError:
367
+ break
368
+ except Exception as e:
369
+ logger.error(f"Error in receive loop: {e}")
370
+ # Notify all pending futures of the error
371
+ for future in self._pending_responses.values():
372
+ if not future.done():
373
+ future.set_exception(e)
374
+ self._pending_responses.clear()
375
+ break
376
+ finally:
377
+ logger.debug("Receive loop terminated")
378
+
379
+ async def _ensure_connection(self) -> None:
380
+ """Ensure WebSocket connection is alive."""
381
+ if not self.websocket:
382
+ raise RuntimeError("WebSocket not connected")
383
+
384
+ # Check if connection is still alive
385
+ try:
386
+ # Send a ping to check connection
387
+ await self.websocket.ping()
388
+ except Exception as e:
389
+ logger.warning(f"WebSocket ping failed: {e}")
390
+ self.websocket = None
391
+ raise RuntimeError("WebSocket connection lost")
392
+
393
+ async def _send_command(
394
+ self, command: str, params: Dict[str, Any]
395
+ ) -> Dict[str, Any]:
396
+ """Send a command to the WebSocket server and get response."""
397
+ await self._ensure_connection()
398
+
399
+ message_id = str(uuid.uuid4())
400
+ message = {'id': message_id, 'command': command, 'params': params}
401
+
402
+ # Create a future for this message
403
+ future: asyncio.Future[Dict[str, Any]] = asyncio.Future()
404
+ self._pending_responses[message_id] = future
405
+
406
+ try:
407
+ # Use lock only for sending to prevent interleaved messages
408
+ async with self._send_lock:
409
+ if self.websocket is None:
410
+ raise RuntimeError("WebSocket connection not established")
411
+ await self.websocket.send(json.dumps(message))
412
+
413
+ # Wait for response (no lock needed, handled by background
414
+ # receiver)
415
+ try:
416
+ response = await asyncio.wait_for(future, timeout=60.0)
417
+
418
+ if not response.get('success'):
419
+ raise RuntimeError(
420
+ f"Command failed: {response.get('error')}"
421
+ )
422
+ return response['result']
423
+
424
+ except asyncio.TimeoutError:
425
+ # Remove from pending if timeout
426
+ self._pending_responses.pop(message_id, None)
427
+ raise RuntimeError(
428
+ f"Timeout waiting for response to command: {command}"
429
+ )
430
+
431
+ except Exception as e:
432
+ # Clean up the pending response
433
+ self._pending_responses.pop(message_id, None)
434
+
435
+ # Check if it's a connection closed error
436
+ if (
437
+ "close frame" in str(e)
438
+ or "connection closed" in str(e).lower()
439
+ ):
440
+ logger.error(f"WebSocket connection closed unexpectedly: {e}")
441
+ # Mark connection as closed
442
+ self.websocket = None
443
+ raise RuntimeError(
444
+ f"WebSocket connection lost "
445
+ f"during {command} operation: {e}"
446
+ )
447
+ else:
448
+ logger.error(f"WebSocket communication error: {e}")
449
+ raise
450
+
451
+ # Browser action methods
452
+ @action_logger
453
+ async def open_browser(
454
+ self, start_url: Optional[str] = None
455
+ ) -> Dict[str, Any]:
456
+ """Open browser."""
457
+ response = await self._send_command(
458
+ 'open_browser', {'startUrl': start_url}
459
+ )
460
+ return response
461
+
462
+ @action_logger
463
+ async def close_browser(self) -> str:
464
+ """Close browser."""
465
+ response = await self._send_command('close_browser', {})
466
+ return response['message']
467
+
468
+ @action_logger
469
+ async def visit_page(self, url: str) -> Dict[str, Any]:
470
+ """Visit a page."""
471
+ response = await self._send_command('visit_page', {'url': url})
472
+ return response
473
+
474
+ @action_logger
475
+ async def get_page_snapshot(self, viewport_limit: bool = False) -> str:
476
+ """Get page snapshot."""
477
+ response = await self._send_command(
478
+ 'get_page_snapshot', {'viewport_limit': viewport_limit}
479
+ )
480
+ # The backend returns the snapshot string directly,
481
+ # not wrapped in an object
482
+ if isinstance(response, str):
483
+ return response
484
+ # Fallback if wrapped in an object
485
+ return response.get('snapshot', '')
486
+
487
+ @action_logger
488
+ async def get_snapshot_for_ai(self) -> Dict[str, Any]:
489
+ """Get snapshot for AI with element details."""
490
+ response = await self._send_command('get_snapshot_for_ai', {})
491
+ return response
492
+
493
+ @action_logger
494
+ async def get_som_screenshot(self) -> ToolResult:
495
+ """Get screenshot."""
496
+ logger.info("Requesting screenshot via WebSocket...")
497
+ start_time = time.time()
498
+
499
+ response = await self._send_command('get_som_screenshot', {})
500
+
501
+ end_time = time.time()
502
+ logger.info(f"Screenshot completed in {end_time - start_time:.2f}s")
503
+
504
+ return ToolResult(text=response['text'], images=response['images'])
505
+
506
+ @action_logger
507
+ async def click(self, ref: str) -> Dict[str, Any]:
508
+ """Click an element."""
509
+ response = await self._send_command('click', {'ref': ref})
510
+ return response
511
+
512
+ @action_logger
513
+ async def type(self, ref: str, text: str) -> Dict[str, Any]:
514
+ """Type text into an element."""
515
+ response = await self._send_command('type', {'ref': ref, 'text': text})
516
+ return response
517
+
518
+ @action_logger
519
+ async def select(self, ref: str, value: str) -> Dict[str, Any]:
520
+ """Select an option."""
521
+ response = await self._send_command(
522
+ 'select', {'ref': ref, 'value': value}
523
+ )
524
+ return response
525
+
526
+ @action_logger
527
+ async def scroll(self, direction: str, amount: int) -> Dict[str, Any]:
528
+ """Scroll the page."""
529
+ response = await self._send_command(
530
+ 'scroll', {'direction': direction, 'amount': amount}
531
+ )
532
+ return response
533
+
534
+ @action_logger
535
+ async def enter(self) -> Dict[str, Any]:
536
+ """Press enter."""
537
+ response = await self._send_command('enter', {})
538
+ return response
539
+
540
+ @action_logger
541
+ async def back(self) -> Dict[str, Any]:
542
+ """Navigate back."""
543
+ response = await self._send_command('back', {})
544
+ return response
545
+
546
+ @action_logger
547
+ async def forward(self) -> Dict[str, Any]:
548
+ """Navigate forward."""
549
+ response = await self._send_command('forward', {})
550
+ return response
551
+
552
+ @action_logger
553
+ async def switch_tab(self, tab_id: str) -> Dict[str, Any]:
554
+ """Switch to a tab."""
555
+ response = await self._send_command('switch_tab', {'tabId': tab_id})
556
+ return response
557
+
558
+ @action_logger
559
+ async def close_tab(self, tab_id: str) -> Dict[str, Any]:
560
+ """Close a tab."""
561
+ response = await self._send_command('close_tab', {'tabId': tab_id})
562
+ return response
563
+
564
+ @action_logger
565
+ async def get_tab_info(self) -> List[Dict[str, Any]]:
566
+ """Get tab information."""
567
+ response = await self._send_command('get_tab_info', {})
568
+ # The backend returns the tab list directly, not wrapped in an object
569
+ if isinstance(response, list):
570
+ return response
571
+ # Fallback if wrapped in an object
572
+ return response.get('tabs', [])
573
+
574
+ @action_logger
575
+ async def wait_user(
576
+ self, timeout_sec: Optional[float] = None
577
+ ) -> Dict[str, Any]:
578
+ """Wait for user input."""
579
+ response = await self._send_command(
580
+ 'wait_user', {'timeout': timeout_sec}
581
+ )
582
+ return response
@@ -0,0 +1,17 @@
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 .hybrid_browser_toolkit import HybridBrowserToolkit
16
+
17
+ __all__ = ["HybridBrowserToolkit"]