camel-ai 0.2.71a11__py3-none-any.whl → 0.2.72__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 (46) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +261 -489
  3. camel/memories/agent_memories.py +39 -0
  4. camel/memories/base.py +8 -0
  5. camel/models/gemini_model.py +30 -2
  6. camel/models/moonshot_model.py +36 -4
  7. camel/models/openai_model.py +29 -15
  8. camel/societies/workforce/prompts.py +25 -15
  9. camel/societies/workforce/role_playing_worker.py +1 -1
  10. camel/societies/workforce/single_agent_worker.py +9 -7
  11. camel/societies/workforce/worker.py +1 -1
  12. camel/societies/workforce/workforce.py +97 -34
  13. camel/storages/vectordb_storages/__init__.py +1 -0
  14. camel/storages/vectordb_storages/surreal.py +415 -0
  15. camel/tasks/task.py +9 -5
  16. camel/toolkits/__init__.py +10 -1
  17. camel/toolkits/base.py +57 -1
  18. camel/toolkits/human_toolkit.py +5 -1
  19. camel/toolkits/hybrid_browser_toolkit/config_loader.py +127 -414
  20. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +783 -1626
  21. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +489 -0
  22. camel/toolkits/markitdown_toolkit.py +2 -2
  23. camel/toolkits/message_integration.py +592 -0
  24. camel/toolkits/note_taking_toolkit.py +195 -26
  25. camel/toolkits/openai_image_toolkit.py +5 -5
  26. camel/toolkits/origene_mcp_toolkit.py +97 -0
  27. camel/toolkits/screenshot_toolkit.py +213 -0
  28. camel/toolkits/search_toolkit.py +161 -79
  29. camel/toolkits/terminal_toolkit.py +379 -165
  30. camel/toolkits/video_analysis_toolkit.py +13 -13
  31. camel/toolkits/video_download_toolkit.py +11 -11
  32. camel/toolkits/web_deploy_toolkit.py +1024 -0
  33. camel/types/enums.py +6 -3
  34. camel/types/unified_model_type.py +16 -4
  35. camel/utils/mcp_client.py +8 -0
  36. camel/utils/tool_result.py +1 -1
  37. {camel_ai-0.2.71a11.dist-info → camel_ai-0.2.72.dist-info}/METADATA +6 -3
  38. {camel_ai-0.2.71a11.dist-info → camel_ai-0.2.72.dist-info}/RECORD +40 -40
  39. camel/toolkits/hybrid_browser_toolkit/actions.py +0 -417
  40. camel/toolkits/hybrid_browser_toolkit/agent.py +0 -311
  41. camel/toolkits/hybrid_browser_toolkit/browser_session.py +0 -739
  42. camel/toolkits/hybrid_browser_toolkit/snapshot.py +0 -227
  43. camel/toolkits/hybrid_browser_toolkit/stealth_script.js +0 -0
  44. camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +0 -1002
  45. {camel_ai-0.2.71a11.dist-info → camel_ai-0.2.72.dist-info}/WHEEL +0 -0
  46. {camel_ai-0.2.71a11.dist-info → camel_ai-0.2.72.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,489 @@
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
+
114
+ # Logging configuration
115
+ self.browser_log_to_file = (config or {}).get(
116
+ 'browser_log_to_file', False
117
+ )
118
+ self.session_id = (config or {}).get('session_id', 'default')
119
+ self.log_file_path: Optional[str] = None
120
+ self.log_buffer: List[Dict[str, Any]] = []
121
+
122
+ # Set up log file if needed
123
+ if self.browser_log_to_file:
124
+ log_dir = "browser_log"
125
+ os.makedirs(log_dir, exist_ok=True)
126
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
127
+ self.log_file_path = os.path.join(
128
+ log_dir,
129
+ f"hybrid_browser_toolkit_ws_{timestamp}_{self.session_id}.log",
130
+ )
131
+
132
+ async def __aenter__(self):
133
+ """Async context manager entry."""
134
+ await self.start()
135
+ return self
136
+
137
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
138
+ """Async context manager exit."""
139
+ await self.stop()
140
+
141
+ async def start(self):
142
+ """Start the WebSocket server and connect to it."""
143
+ # Ensure the TypeScript code is built
144
+ build_result = subprocess.run(
145
+ ['npm', 'run', 'build'],
146
+ cwd=self.ts_dir,
147
+ capture_output=True,
148
+ text=True,
149
+ )
150
+ if build_result.returncode != 0:
151
+ logger.error(f"TypeScript build failed: {build_result.stderr}")
152
+ raise RuntimeError(
153
+ f"TypeScript build failed: {build_result.stderr}"
154
+ )
155
+
156
+ # Start the WebSocket server
157
+ self.process = subprocess.Popen(
158
+ ['node', 'websocket-server.js'],
159
+ cwd=self.ts_dir,
160
+ stdout=subprocess.PIPE,
161
+ stderr=subprocess.PIPE,
162
+ text=True,
163
+ )
164
+
165
+ # Wait for server to output the port
166
+ server_ready = False
167
+ timeout = 10 # 10 seconds timeout
168
+ start_time = time.time()
169
+
170
+ while not server_ready and time.time() - start_time < timeout:
171
+ if self.process.poll() is not None:
172
+ # Process died
173
+ stderr = self.process.stderr.read()
174
+ raise RuntimeError(
175
+ f"WebSocket server failed to start: {stderr}"
176
+ )
177
+
178
+ try:
179
+ line = self.process.stdout.readline()
180
+ if line.startswith('SERVER_READY:'):
181
+ self.server_port = int(line.split(':')[1].strip())
182
+ server_ready = True
183
+ logger.info(
184
+ f"WebSocket server ready on port {self.server_port}"
185
+ )
186
+ except (ValueError, IndexError):
187
+ continue
188
+
189
+ if not server_ready:
190
+ self.process.kill()
191
+ raise RuntimeError(
192
+ "WebSocket server failed to start within timeout"
193
+ )
194
+
195
+ # Connect to the WebSocket server
196
+ try:
197
+ self.websocket = await websockets.connect(
198
+ f"ws://localhost:{self.server_port}",
199
+ ping_interval=30,
200
+ ping_timeout=10,
201
+ max_size=50 * 1024 * 1024, # 50MB limit to match server
202
+ )
203
+ logger.info("Connected to WebSocket server")
204
+ except Exception as e:
205
+ self.process.kill()
206
+ raise RuntimeError(
207
+ f"Failed to connect to WebSocket server: {e}"
208
+ ) from e
209
+
210
+ # Initialize the browser toolkit
211
+ await self._send_command('init', self.config)
212
+
213
+ async def stop(self):
214
+ """Stop the WebSocket connection and server."""
215
+ if self.websocket:
216
+ try:
217
+ await self._send_command('shutdown', {})
218
+ await self.websocket.close()
219
+ except Exception as e:
220
+ logger.warning(f"Error during websocket shutdown: {e}")
221
+ finally:
222
+ self.websocket = None
223
+
224
+ if self.process:
225
+ try:
226
+ self.process.terminate()
227
+ self.process.wait(timeout=5)
228
+ except subprocess.TimeoutExpired:
229
+ self.process.kill()
230
+ self.process.wait()
231
+ except Exception as e:
232
+ logger.warning(f"Error terminating process: {e}")
233
+ finally:
234
+ self.process = None
235
+
236
+ async def _log_action(
237
+ self,
238
+ action_name: str,
239
+ inputs: Dict[str, Any],
240
+ outputs: Any,
241
+ execution_time: float,
242
+ page_load_time: Optional[float] = None,
243
+ error: Optional[str] = None,
244
+ ) -> None:
245
+ """Log action details with comprehensive
246
+ information including detailed timing breakdown."""
247
+ if not self.browser_log_to_file or not self.log_file_path:
248
+ return
249
+
250
+ # Create log entry
251
+ log_entry = {
252
+ "timestamp": datetime.datetime.now().isoformat(),
253
+ "session_id": self.session_id,
254
+ "action": action_name,
255
+ "execution_time_ms": round(execution_time * 1000, 2),
256
+ "inputs": inputs,
257
+ }
258
+
259
+ if error:
260
+ log_entry["error"] = error
261
+ else:
262
+ # Handle ToolResult objects for JSON serialization
263
+ if hasattr(outputs, 'text') and hasattr(outputs, 'images'):
264
+ # This is a ToolResult object
265
+ log_entry["outputs"] = {
266
+ "text": outputs.text,
267
+ "images_count": len(outputs.images)
268
+ if outputs.images
269
+ else 0,
270
+ }
271
+ else:
272
+ log_entry["outputs"] = outputs
273
+
274
+ if page_load_time is not None:
275
+ log_entry["page_load_time_ms"] = round(page_load_time * 1000, 2)
276
+
277
+ # Write to log file
278
+ try:
279
+ with open(self.log_file_path, 'a', encoding='utf-8') as f:
280
+ f.write(
281
+ json.dumps(log_entry, ensure_ascii=False, indent=2) + '\n'
282
+ )
283
+ except Exception as e:
284
+ logger.error(f"Failed to write to log file: {e}")
285
+
286
+ async def _ensure_connection(self) -> None:
287
+ """Ensure WebSocket connection is alive."""
288
+ if not self.websocket:
289
+ raise RuntimeError("WebSocket not connected")
290
+
291
+ # Check if connection is still alive
292
+ try:
293
+ # Send a ping to check connection
294
+ await self.websocket.ping()
295
+ except Exception as e:
296
+ logger.warning(f"WebSocket ping failed: {e}")
297
+ self.websocket = None
298
+ raise RuntimeError("WebSocket connection lost")
299
+
300
+ async def _send_command(
301
+ self, command: str, params: Dict[str, Any]
302
+ ) -> Dict[str, Any]:
303
+ """Send a command to the WebSocket server and get response."""
304
+ await self._ensure_connection()
305
+
306
+ message_id = str(uuid.uuid4())
307
+ message = {'id': message_id, 'command': command, 'params': params}
308
+
309
+ try:
310
+ # Send command
311
+ if self.websocket is None:
312
+ raise RuntimeError("WebSocket connection not established")
313
+ await self.websocket.send(json.dumps(message))
314
+
315
+ # Wait for response with matching ID
316
+ while True:
317
+ try:
318
+ if self.websocket is None:
319
+ raise RuntimeError("WebSocket connection lost")
320
+ response_data = await asyncio.wait_for(
321
+ self.websocket.recv(), timeout=60.0
322
+ )
323
+ response = json.loads(response_data)
324
+
325
+ # Check if this is the response we're waiting for
326
+ if response.get('id') == message_id:
327
+ if not response.get('success'):
328
+ raise RuntimeError(
329
+ f"Command failed: {response.get('error')}"
330
+ )
331
+ return response['result']
332
+
333
+ except asyncio.TimeoutError:
334
+ raise RuntimeError(
335
+ f"Timeout waiting for response to command: {command}"
336
+ )
337
+ except json.JSONDecodeError as e:
338
+ logger.warning(f"Failed to decode WebSocket response: {e}")
339
+ continue
340
+
341
+ except Exception as e:
342
+ # Check if it's a connection closed error
343
+ if (
344
+ "close frame" in str(e)
345
+ or "connection closed" in str(e).lower()
346
+ ):
347
+ logger.error(f"WebSocket connection closed unexpectedly: {e}")
348
+ # Mark connection as closed
349
+ self.websocket = None
350
+ raise RuntimeError(
351
+ f"WebSocket connection lost "
352
+ f"during {command} operation: {e}"
353
+ )
354
+ else:
355
+ logger.error(f"WebSocket communication error: {e}")
356
+ raise
357
+
358
+ # Browser action methods
359
+ @action_logger
360
+ async def open_browser(
361
+ self, start_url: Optional[str] = None
362
+ ) -> Dict[str, Any]:
363
+ """Open browser."""
364
+ response = await self._send_command(
365
+ 'open_browser', {'startUrl': start_url}
366
+ )
367
+ return response
368
+
369
+ @action_logger
370
+ async def close_browser(self) -> str:
371
+ """Close browser."""
372
+ response = await self._send_command('close_browser', {})
373
+ return response['message']
374
+
375
+ @action_logger
376
+ async def visit_page(self, url: str) -> Dict[str, Any]:
377
+ """Visit a page."""
378
+ response = await self._send_command('visit_page', {'url': url})
379
+ return response
380
+
381
+ @action_logger
382
+ async def get_page_snapshot(self, viewport_limit: bool = False) -> str:
383
+ """Get page snapshot."""
384
+ response = await self._send_command(
385
+ 'get_page_snapshot', {'viewport_limit': viewport_limit}
386
+ )
387
+ # The backend returns the snapshot string directly,
388
+ # not wrapped in an object
389
+ if isinstance(response, str):
390
+ return response
391
+ # Fallback if wrapped in an object
392
+ return response.get('snapshot', '')
393
+
394
+ @action_logger
395
+ async def get_snapshot_for_ai(self) -> Dict[str, Any]:
396
+ """Get snapshot for AI with element details."""
397
+ response = await self._send_command('get_snapshot_for_ai', {})
398
+ return response
399
+
400
+ @action_logger
401
+ async def get_som_screenshot(self) -> ToolResult:
402
+ """Get screenshot."""
403
+ logger.info("Requesting screenshot via WebSocket...")
404
+ start_time = time.time()
405
+
406
+ response = await self._send_command('get_som_screenshot', {})
407
+
408
+ end_time = time.time()
409
+ logger.info(f"Screenshot completed in {end_time - start_time:.2f}s")
410
+
411
+ return ToolResult(text=response['text'], images=response['images'])
412
+
413
+ @action_logger
414
+ async def click(self, ref: str) -> Dict[str, Any]:
415
+ """Click an element."""
416
+ response = await self._send_command('click', {'ref': ref})
417
+ return response
418
+
419
+ @action_logger
420
+ async def type(self, ref: str, text: str) -> Dict[str, Any]:
421
+ """Type text into an element."""
422
+ response = await self._send_command('type', {'ref': ref, 'text': text})
423
+ return response
424
+
425
+ @action_logger
426
+ async def select(self, ref: str, value: str) -> Dict[str, Any]:
427
+ """Select an option."""
428
+ response = await self._send_command(
429
+ 'select', {'ref': ref, 'value': value}
430
+ )
431
+ return response
432
+
433
+ @action_logger
434
+ async def scroll(self, direction: str, amount: int) -> Dict[str, Any]:
435
+ """Scroll the page."""
436
+ response = await self._send_command(
437
+ 'scroll', {'direction': direction, 'amount': amount}
438
+ )
439
+ return response
440
+
441
+ @action_logger
442
+ async def enter(self) -> Dict[str, Any]:
443
+ """Press enter."""
444
+ response = await self._send_command('enter', {})
445
+ return response
446
+
447
+ @action_logger
448
+ async def back(self) -> Dict[str, Any]:
449
+ """Navigate back."""
450
+ response = await self._send_command('back', {})
451
+ return response
452
+
453
+ @action_logger
454
+ async def forward(self) -> Dict[str, Any]:
455
+ """Navigate forward."""
456
+ response = await self._send_command('forward', {})
457
+ return response
458
+
459
+ @action_logger
460
+ async def switch_tab(self, tab_id: str) -> Dict[str, Any]:
461
+ """Switch to a tab."""
462
+ response = await self._send_command('switch_tab', {'tabId': tab_id})
463
+ return response
464
+
465
+ @action_logger
466
+ async def close_tab(self, tab_id: str) -> Dict[str, Any]:
467
+ """Close a tab."""
468
+ response = await self._send_command('close_tab', {'tabId': tab_id})
469
+ return response
470
+
471
+ @action_logger
472
+ async def get_tab_info(self) -> List[Dict[str, Any]]:
473
+ """Get tab information."""
474
+ response = await self._send_command('get_tab_info', {})
475
+ # The backend returns the tab list directly, not wrapped in an object
476
+ if isinstance(response, list):
477
+ return response
478
+ # Fallback if wrapped in an object
479
+ return response.get('tabs', [])
480
+
481
+ @action_logger
482
+ async def wait_user(
483
+ self, timeout_sec: Optional[float] = None
484
+ ) -> Dict[str, Any]:
485
+ """Wait for user input."""
486
+ response = await self._send_command(
487
+ 'wait_user', {'timeout': timeout_sec}
488
+ )
489
+ return response
@@ -33,7 +33,7 @@ class MarkItDownToolkit(BaseToolkit):
33
33
  ):
34
34
  super().__init__(timeout=timeout)
35
35
 
36
- def load_files(self, file_paths: List[str]) -> Dict[str, str]:
36
+ def read_files(self, file_paths: List[str]) -> Dict[str, str]:
37
37
  r"""Scrapes content from a list of files and converts it to Markdown.
38
38
 
39
39
  This function takes a list of local file paths, attempts to convert
@@ -74,5 +74,5 @@ class MarkItDownToolkit(BaseToolkit):
74
74
  representing the functions in the toolkit.
75
75
  """
76
76
  return [
77
- FunctionTool(self.load_files),
77
+ FunctionTool(self.read_files),
78
78
  ]