camel-ai 0.2.76a0__py3-none-any.whl → 0.2.76a2__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.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +8 -1
- camel/environments/tic_tac_toe.py +1 -1
- camel/memories/__init__.py +2 -1
- camel/memories/agent_memories.py +3 -1
- camel/memories/blocks/chat_history_block.py +17 -2
- camel/models/base_model.py +30 -0
- camel/societies/workforce/single_agent_worker.py +44 -38
- camel/societies/workforce/workforce.py +10 -1
- camel/storages/object_storages/google_cloud.py +1 -1
- camel/toolkits/__init__.py +9 -2
- camel/toolkits/aci_toolkit.py +45 -0
- camel/toolkits/context_summarizer_toolkit.py +683 -0
- camel/toolkits/{file_write_toolkit.py → file_toolkit.py} +194 -34
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +4 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +67 -2
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +62 -45
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +489 -60
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +5 -2
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +72 -12
- camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +2 -14
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +1 -0
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +228 -62
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +4 -4
- camel/toolkits/markitdown_toolkit.py +27 -1
- camel/toolkits/note_taking_toolkit.py +18 -8
- camel/toolkits/slack_toolkit.py +50 -1
- camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
- camel/toolkits/wechat_official_toolkit.py +483 -0
- camel/utils/context_utils.py +395 -0
- {camel_ai-0.2.76a0.dist-info → camel_ai-0.2.76a2.dist-info}/METADATA +84 -6
- {camel_ai-0.2.76a0.dist-info → camel_ai-0.2.76a2.dist-info}/RECORD +34 -30
- {camel_ai-0.2.76a0.dist-info → camel_ai-0.2.76a2.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.76a0.dist-info → camel_ai-0.2.76a2.dist-info}/licenses/LICENSE +0 -0
|
@@ -45,23 +45,19 @@ def action_logger(func):
|
|
|
45
45
|
action_name = func.__name__
|
|
46
46
|
start_time = time.time()
|
|
47
47
|
|
|
48
|
-
# Log inputs (skip self)
|
|
49
48
|
inputs = {
|
|
50
49
|
"args": args,
|
|
51
50
|
"kwargs": kwargs,
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
try:
|
|
55
|
-
# Execute the original function
|
|
56
54
|
result = await func(self, *args, **kwargs)
|
|
57
55
|
execution_time = time.time() - start_time
|
|
58
56
|
|
|
59
|
-
# Extract page load time if available
|
|
60
57
|
page_load_time = None
|
|
61
58
|
if isinstance(result, dict) and 'page_load_time_ms' in result:
|
|
62
59
|
page_load_time = result['page_load_time_ms'] / 1000.0
|
|
63
60
|
|
|
64
|
-
# Log success
|
|
65
61
|
await self._log_action(
|
|
66
62
|
action_name=action_name,
|
|
67
63
|
inputs=inputs,
|
|
@@ -76,7 +72,6 @@ def action_logger(func):
|
|
|
76
72
|
execution_time = time.time() - start_time
|
|
77
73
|
error_msg = f"{type(e).__name__}: {e!s}"
|
|
78
74
|
|
|
79
|
-
# Log error
|
|
80
75
|
await self._log_action(
|
|
81
76
|
action_name=action_name,
|
|
82
77
|
inputs=inputs,
|
|
@@ -111,14 +106,12 @@ class WebSocketBrowserWrapper:
|
|
|
111
106
|
self.process: Optional[subprocess.Popen] = None
|
|
112
107
|
self.websocket = None
|
|
113
108
|
self.server_port = None
|
|
114
|
-
self._send_lock = asyncio.Lock()
|
|
115
|
-
self._receive_task = None
|
|
116
|
-
self._pending_responses: Dict[
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# Logging configuration
|
|
109
|
+
self._send_lock = asyncio.Lock()
|
|
110
|
+
self._receive_task = None
|
|
111
|
+
self._pending_responses: Dict[str, asyncio.Future[Dict[str, Any]]] = {}
|
|
112
|
+
self._browser_opened = False
|
|
113
|
+
self._server_ready_future = None
|
|
114
|
+
|
|
122
115
|
self.browser_log_to_file = (config or {}).get(
|
|
123
116
|
'browser_log_to_file', False
|
|
124
117
|
)
|
|
@@ -127,10 +120,9 @@ class WebSocketBrowserWrapper:
|
|
|
127
120
|
self.log_file_path: Optional[str] = None
|
|
128
121
|
self.log_buffer: List[Dict[str, Any]] = []
|
|
129
122
|
self.ts_log_file_path: Optional[str] = None
|
|
130
|
-
self.ts_log_file = None
|
|
131
|
-
self._log_reader_task = None
|
|
123
|
+
self.ts_log_file = None
|
|
124
|
+
self._log_reader_task = None
|
|
132
125
|
|
|
133
|
-
# Set up log files if needed
|
|
134
126
|
if self.browser_log_to_file:
|
|
135
127
|
log_dir = self.log_dir if self.log_dir else "browser_log"
|
|
136
128
|
os.makedirs(log_dir, exist_ok=True)
|
|
@@ -139,7 +131,6 @@ class WebSocketBrowserWrapper:
|
|
|
139
131
|
log_dir,
|
|
140
132
|
f"hybrid_browser_toolkit_ws_{timestamp}_{self.session_id}.log",
|
|
141
133
|
)
|
|
142
|
-
# Add TypeScript console log file
|
|
143
134
|
self.ts_log_file_path = os.path.join(
|
|
144
135
|
log_dir,
|
|
145
136
|
f"typescript_console_{timestamp}_{self.session_id}.log",
|
|
@@ -154,13 +145,61 @@ class WebSocketBrowserWrapper:
|
|
|
154
145
|
"""Async context manager exit."""
|
|
155
146
|
await self.stop()
|
|
156
147
|
|
|
148
|
+
async def _cleanup_existing_processes(self):
|
|
149
|
+
"""Clean up any existing Node.js WebSocket server processes."""
|
|
150
|
+
import psutil
|
|
151
|
+
|
|
152
|
+
cleaned_count = 0
|
|
153
|
+
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
|
154
|
+
try:
|
|
155
|
+
if (
|
|
156
|
+
proc.info['name']
|
|
157
|
+
and 'node' in proc.info['name'].lower()
|
|
158
|
+
and proc.info['cmdline']
|
|
159
|
+
and any(
|
|
160
|
+
'websocket-server.js' in arg
|
|
161
|
+
for arg in proc.info['cmdline']
|
|
162
|
+
)
|
|
163
|
+
):
|
|
164
|
+
if any(self.ts_dir in arg for arg in proc.info['cmdline']):
|
|
165
|
+
logger.warning(
|
|
166
|
+
f"Found existing WebSocket server process "
|
|
167
|
+
f"(PID: {proc.info['pid']}). "
|
|
168
|
+
f"Terminating it to prevent conflicts."
|
|
169
|
+
)
|
|
170
|
+
proc.terminate()
|
|
171
|
+
try:
|
|
172
|
+
proc.wait(timeout=3)
|
|
173
|
+
except psutil.TimeoutExpired:
|
|
174
|
+
proc.kill()
|
|
175
|
+
cleaned_count += 1
|
|
176
|
+
except (
|
|
177
|
+
psutil.NoSuchProcess,
|
|
178
|
+
psutil.AccessDenied,
|
|
179
|
+
psutil.ZombieProcess,
|
|
180
|
+
):
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
if cleaned_count > 0:
|
|
184
|
+
logger.warning(
|
|
185
|
+
f"Cleaned up {cleaned_count} existing WebSocket server "
|
|
186
|
+
f"process(es). This may have been caused by improper "
|
|
187
|
+
f"shutdown in previous sessions."
|
|
188
|
+
)
|
|
189
|
+
await asyncio.sleep(0.5)
|
|
190
|
+
|
|
157
191
|
async def start(self):
|
|
158
192
|
"""Start the WebSocket server and connect to it."""
|
|
159
|
-
|
|
193
|
+
await self._cleanup_existing_processes()
|
|
194
|
+
|
|
195
|
+
import platform
|
|
196
|
+
|
|
197
|
+
use_shell = platform.system() == 'Windows'
|
|
160
198
|
npm_check = subprocess.run(
|
|
161
199
|
['npm', '--version'],
|
|
162
200
|
capture_output=True,
|
|
163
201
|
text=True,
|
|
202
|
+
shell=use_shell,
|
|
164
203
|
)
|
|
165
204
|
if npm_check.returncode != 0:
|
|
166
205
|
raise RuntimeError(
|
|
@@ -169,11 +208,11 @@ class WebSocketBrowserWrapper:
|
|
|
169
208
|
"to use the hybrid browser toolkit."
|
|
170
209
|
)
|
|
171
210
|
|
|
172
|
-
# Check if node is installed
|
|
173
211
|
node_check = subprocess.run(
|
|
174
212
|
['node', '--version'],
|
|
175
213
|
capture_output=True,
|
|
176
214
|
text=True,
|
|
215
|
+
shell=use_shell,
|
|
177
216
|
)
|
|
178
217
|
if node_check.returncode != 0:
|
|
179
218
|
raise RuntimeError(
|
|
@@ -182,7 +221,6 @@ class WebSocketBrowserWrapper:
|
|
|
182
221
|
"to use the hybrid browser toolkit."
|
|
183
222
|
)
|
|
184
223
|
|
|
185
|
-
# Check if node_modules exists (dependencies installed)
|
|
186
224
|
node_modules_path = os.path.join(self.ts_dir, 'node_modules')
|
|
187
225
|
if not os.path.exists(node_modules_path):
|
|
188
226
|
logger.warning("Node modules not found. Running npm install...")
|
|
@@ -191,6 +229,7 @@ class WebSocketBrowserWrapper:
|
|
|
191
229
|
cwd=self.ts_dir,
|
|
192
230
|
capture_output=True,
|
|
193
231
|
text=True,
|
|
232
|
+
shell=use_shell,
|
|
194
233
|
)
|
|
195
234
|
if install_result.returncode != 0:
|
|
196
235
|
logger.error(f"npm install failed: {install_result.stderr}")
|
|
@@ -200,12 +239,12 @@ class WebSocketBrowserWrapper:
|
|
|
200
239
|
)
|
|
201
240
|
logger.info("npm dependencies installed successfully")
|
|
202
241
|
|
|
203
|
-
# Ensure the TypeScript code is built
|
|
204
242
|
build_result = subprocess.run(
|
|
205
243
|
['npm', 'run', 'build'],
|
|
206
244
|
cwd=self.ts_dir,
|
|
207
245
|
capture_output=True,
|
|
208
246
|
text=True,
|
|
247
|
+
shell=use_shell,
|
|
209
248
|
)
|
|
210
249
|
if build_result.returncode != 0:
|
|
211
250
|
logger.error(f"TypeScript build failed: {build_result.stderr}")
|
|
@@ -213,20 +252,20 @@ class WebSocketBrowserWrapper:
|
|
|
213
252
|
f"TypeScript build failed: {build_result.stderr}"
|
|
214
253
|
)
|
|
215
254
|
|
|
216
|
-
#
|
|
255
|
+
# use_shell already defined above
|
|
217
256
|
self.process = subprocess.Popen(
|
|
218
257
|
['node', 'websocket-server.js'],
|
|
219
258
|
cwd=self.ts_dir,
|
|
220
259
|
stdout=subprocess.PIPE,
|
|
221
|
-
stderr=subprocess.STDOUT,
|
|
260
|
+
stderr=subprocess.STDOUT,
|
|
222
261
|
text=True,
|
|
223
|
-
|
|
262
|
+
encoding='utf-8',
|
|
263
|
+
bufsize=1,
|
|
264
|
+
shell=use_shell,
|
|
224
265
|
)
|
|
225
266
|
|
|
226
|
-
# Create a future to wait for server ready (before starting log reader)
|
|
227
267
|
self._server_ready_future = asyncio.get_running_loop().create_future()
|
|
228
268
|
|
|
229
|
-
# Start log reader task immediately after process starts
|
|
230
269
|
self._log_reader_task = asyncio.create_task(
|
|
231
270
|
self._read_and_log_output()
|
|
232
271
|
)
|
|
@@ -237,11 +276,9 @@ class WebSocketBrowserWrapper:
|
|
|
237
276
|
f"{self.ts_log_file_path}"
|
|
238
277
|
)
|
|
239
278
|
|
|
240
|
-
# Wait for server to output the port
|
|
241
279
|
server_ready = False
|
|
242
|
-
timeout = 10
|
|
280
|
+
timeout = 10
|
|
243
281
|
|
|
244
|
-
# Wait for the server to be ready
|
|
245
282
|
try:
|
|
246
283
|
await asyncio.wait_for(self._server_ready_future, timeout=timeout)
|
|
247
284
|
server_ready = True
|
|
@@ -252,14 +289,11 @@ class WebSocketBrowserWrapper:
|
|
|
252
289
|
with contextlib.suppress(ProcessLookupError, Exception):
|
|
253
290
|
self.process.kill()
|
|
254
291
|
with contextlib.suppress(Exception):
|
|
255
|
-
# Ensure the process fully exits
|
|
256
292
|
self.process.wait(timeout=2)
|
|
257
|
-
# Cancel and await the log reader task
|
|
258
293
|
if self._log_reader_task and not self._log_reader_task.done():
|
|
259
294
|
self._log_reader_task.cancel()
|
|
260
295
|
with contextlib.suppress(asyncio.CancelledError):
|
|
261
296
|
await self._log_reader_task
|
|
262
|
-
# Close TS log file if open
|
|
263
297
|
if getattr(self, 'ts_log_file', None):
|
|
264
298
|
with contextlib.suppress(Exception):
|
|
265
299
|
self.ts_log_file.close()
|
|
@@ -270,7 +304,7 @@ class WebSocketBrowserWrapper:
|
|
|
270
304
|
import psutil
|
|
271
305
|
|
|
272
306
|
mem = psutil.virtual_memory()
|
|
273
|
-
if mem.available < 1024**3:
|
|
307
|
+
if mem.available < 1024**3:
|
|
274
308
|
error_msg = (
|
|
275
309
|
f"WebSocket server failed to start"
|
|
276
310
|
f"(likely due to insufficient memory). "
|
|
@@ -280,16 +314,59 @@ class WebSocketBrowserWrapper:
|
|
|
280
314
|
|
|
281
315
|
raise RuntimeError(error_msg)
|
|
282
316
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
317
|
+
max_retries = 3
|
|
318
|
+
retry_delays = [1, 2, 4]
|
|
319
|
+
|
|
320
|
+
for attempt in range(max_retries):
|
|
321
|
+
try:
|
|
322
|
+
connect_timeout = 10.0 + (attempt * 5.0)
|
|
323
|
+
|
|
324
|
+
logger.info(
|
|
325
|
+
f"Attempting to connect to WebSocket server "
|
|
326
|
+
f"(attempt {attempt + 1}/{max_retries}, "
|
|
327
|
+
f"timeout: {connect_timeout}s)"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
self.websocket = await asyncio.wait_for(
|
|
331
|
+
websockets.connect(
|
|
332
|
+
f"ws://localhost:{self.server_port}",
|
|
333
|
+
ping_interval=30,
|
|
334
|
+
ping_timeout=10,
|
|
335
|
+
max_size=50 * 1024 * 1024,
|
|
336
|
+
),
|
|
337
|
+
timeout=connect_timeout,
|
|
338
|
+
)
|
|
339
|
+
logger.info("Connected to WebSocket server")
|
|
340
|
+
break
|
|
341
|
+
|
|
342
|
+
except asyncio.TimeoutError:
|
|
343
|
+
if attempt < max_retries - 1:
|
|
344
|
+
delay = retry_delays[attempt]
|
|
345
|
+
logger.warning(
|
|
346
|
+
f"WebSocket handshake timeout "
|
|
347
|
+
f"(attempt {attempt + 1}/{max_retries}). "
|
|
348
|
+
f"Retrying in {delay} seconds..."
|
|
349
|
+
)
|
|
350
|
+
await asyncio.sleep(delay)
|
|
351
|
+
else:
|
|
352
|
+
raise RuntimeError(
|
|
353
|
+
f"Failed to connect to WebSocket server after "
|
|
354
|
+
f"{max_retries} attempts: Handshake timeout"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
if attempt < max_retries - 1 and "timed out" in str(e).lower():
|
|
359
|
+
delay = retry_delays[attempt]
|
|
360
|
+
logger.warning(
|
|
361
|
+
f"WebSocket connection failed "
|
|
362
|
+
f"(attempt {attempt + 1}/{max_retries}): {e}. "
|
|
363
|
+
f"Retrying in {delay} seconds..."
|
|
364
|
+
)
|
|
365
|
+
await asyncio.sleep(delay)
|
|
366
|
+
else:
|
|
367
|
+
break
|
|
368
|
+
|
|
369
|
+
if not self.websocket:
|
|
293
370
|
with contextlib.suppress(ProcessLookupError, Exception):
|
|
294
371
|
self.process.kill()
|
|
295
372
|
with contextlib.suppress(Exception):
|
|
@@ -304,45 +381,45 @@ class WebSocketBrowserWrapper:
|
|
|
304
381
|
self.ts_log_file = None
|
|
305
382
|
self.process = None
|
|
306
383
|
|
|
307
|
-
error_msg = f"Failed to connect to WebSocket server: {e}"
|
|
308
384
|
import psutil
|
|
309
385
|
|
|
310
386
|
mem = psutil.virtual_memory()
|
|
311
|
-
|
|
387
|
+
|
|
388
|
+
error_msg = (
|
|
389
|
+
"Failed to connect to WebSocket server after multiple attempts"
|
|
390
|
+
)
|
|
391
|
+
if mem.available < 1024**3:
|
|
312
392
|
error_msg = (
|
|
313
|
-
f"Failed to connect to WebSocket server"
|
|
393
|
+
f"Failed to connect to WebSocket server "
|
|
314
394
|
f"(likely due to insufficient memory). "
|
|
315
|
-
f"Available memory: {mem.available / 1024**3:.2f}GB"
|
|
316
|
-
f"({mem.percent}% used)
|
|
317
|
-
f"Original error: {e}"
|
|
395
|
+
f"Available memory: {mem.available / 1024**3:.2f}GB "
|
|
396
|
+
f"({mem.percent}% used)"
|
|
318
397
|
)
|
|
319
398
|
|
|
320
|
-
raise RuntimeError(error_msg)
|
|
399
|
+
raise RuntimeError(error_msg)
|
|
321
400
|
|
|
322
|
-
# Start the background receiver task
|
|
323
401
|
self._receive_task = asyncio.create_task(self._receive_loop())
|
|
324
402
|
|
|
325
|
-
# Initialize the browser toolkit
|
|
326
403
|
await self._send_command('init', self.config)
|
|
327
404
|
|
|
405
|
+
if self.config.get('cdpUrl'):
|
|
406
|
+
self._browser_opened = True
|
|
407
|
+
|
|
328
408
|
async def stop(self):
|
|
329
409
|
"""Stop the WebSocket connection and server."""
|
|
330
|
-
# First, send shutdown command while receive task is still running
|
|
331
410
|
if self.websocket:
|
|
332
411
|
with contextlib.suppress(asyncio.TimeoutError, Exception):
|
|
333
|
-
# Send shutdown command with a short timeout
|
|
334
412
|
await asyncio.wait_for(
|
|
335
413
|
self._send_command('shutdown', {}),
|
|
336
|
-
timeout=2.0,
|
|
414
|
+
timeout=2.0,
|
|
337
415
|
)
|
|
338
|
-
# Note: TimeoutError is expected as server may close
|
|
339
|
-
# before responding
|
|
340
416
|
|
|
341
|
-
# Close websocket connection
|
|
342
417
|
with contextlib.suppress(Exception):
|
|
343
418
|
await self.websocket.close()
|
|
344
419
|
self.websocket = None
|
|
345
420
|
|
|
421
|
+
self._browser_opened = False
|
|
422
|
+
|
|
346
423
|
# Gracefully stop the Node process before cancelling the log reader
|
|
347
424
|
if self.process:
|
|
348
425
|
try:
|
|
@@ -381,6 +458,54 @@ class WebSocketBrowserWrapper:
|
|
|
381
458
|
# Ensure process handle cleared
|
|
382
459
|
self.process = None
|
|
383
460
|
|
|
461
|
+
async def disconnect_only(self):
|
|
462
|
+
"""Disconnect WebSocket and stop server without closing the browser.
|
|
463
|
+
|
|
464
|
+
This is useful for CDP mode where the browser should remain open.
|
|
465
|
+
"""
|
|
466
|
+
if self.websocket:
|
|
467
|
+
with contextlib.suppress(Exception):
|
|
468
|
+
await self.websocket.close()
|
|
469
|
+
self.websocket = None
|
|
470
|
+
|
|
471
|
+
self._browser_opened = False
|
|
472
|
+
|
|
473
|
+
# Stop the Node process
|
|
474
|
+
if self.process:
|
|
475
|
+
try:
|
|
476
|
+
# Send SIGTERM to gracefully shutdown
|
|
477
|
+
self.process.terminate()
|
|
478
|
+
self.process.wait(timeout=3)
|
|
479
|
+
except subprocess.TimeoutExpired:
|
|
480
|
+
# Force kill if needed
|
|
481
|
+
with contextlib.suppress(ProcessLookupError, Exception):
|
|
482
|
+
self.process.kill()
|
|
483
|
+
self.process.wait()
|
|
484
|
+
except Exception as e:
|
|
485
|
+
logger.warning(f"Error terminating process: {e}")
|
|
486
|
+
|
|
487
|
+
# Cancel background tasks
|
|
488
|
+
tasks_to_cancel = [
|
|
489
|
+
('_receive_task', self._receive_task),
|
|
490
|
+
('_log_reader_task', self._log_reader_task),
|
|
491
|
+
]
|
|
492
|
+
for _, task in tasks_to_cancel:
|
|
493
|
+
if task and not task.done():
|
|
494
|
+
task.cancel()
|
|
495
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
496
|
+
await task
|
|
497
|
+
|
|
498
|
+
# Close TS log file if open
|
|
499
|
+
if getattr(self, 'ts_log_file', None):
|
|
500
|
+
with contextlib.suppress(Exception):
|
|
501
|
+
self.ts_log_file.close()
|
|
502
|
+
self.ts_log_file = None
|
|
503
|
+
|
|
504
|
+
# Ensure process handle cleared
|
|
505
|
+
self.process = None
|
|
506
|
+
|
|
507
|
+
logger.info("WebSocket disconnected without closing browser")
|
|
508
|
+
|
|
384
509
|
async def _log_action(
|
|
385
510
|
self,
|
|
386
511
|
action_name: str,
|
|
@@ -454,7 +579,16 @@ class WebSocketBrowserWrapper:
|
|
|
454
579
|
except asyncio.CancelledError:
|
|
455
580
|
break
|
|
456
581
|
except Exception as e:
|
|
457
|
-
|
|
582
|
+
# Check if it's a normal WebSocket close
|
|
583
|
+
if isinstance(e, websockets.exceptions.ConnectionClosed):
|
|
584
|
+
if e.code == 1000: # Normal closure
|
|
585
|
+
logger.debug(f"WebSocket closed normally: {e}")
|
|
586
|
+
else:
|
|
587
|
+
logger.warning(
|
|
588
|
+
f"WebSocket closed with code {e.code}: {e}"
|
|
589
|
+
)
|
|
590
|
+
else:
|
|
591
|
+
logger.error(f"Error in receive loop: {e}")
|
|
458
592
|
# Notify all pending futures of the error
|
|
459
593
|
for future in self._pending_responses.values():
|
|
460
594
|
if not future.done():
|
|
@@ -471,7 +605,7 @@ class WebSocketBrowserWrapper:
|
|
|
471
605
|
import psutil
|
|
472
606
|
|
|
473
607
|
mem = psutil.virtual_memory()
|
|
474
|
-
if mem.available < 1024**3:
|
|
608
|
+
if mem.available < 1024**3:
|
|
475
609
|
error_msg = (
|
|
476
610
|
f"WebSocket not connected "
|
|
477
611
|
f"(likely due to insufficient memory). "
|
|
@@ -494,7 +628,7 @@ class WebSocketBrowserWrapper:
|
|
|
494
628
|
import psutil
|
|
495
629
|
|
|
496
630
|
mem = psutil.virtual_memory()
|
|
497
|
-
if mem.available < 1024**3:
|
|
631
|
+
if mem.available < 1024**3:
|
|
498
632
|
error_msg = (
|
|
499
633
|
f"WebSocket connection lost "
|
|
500
634
|
f"(likely due to insufficient memory). "
|
|
@@ -542,6 +676,16 @@ class WebSocketBrowserWrapper:
|
|
|
542
676
|
except asyncio.TimeoutError:
|
|
543
677
|
# Remove from pending if timeout
|
|
544
678
|
self._pending_responses.pop(message_id, None)
|
|
679
|
+
# Special handling for shutdown command
|
|
680
|
+
if command == 'shutdown':
|
|
681
|
+
logger.debug(
|
|
682
|
+
"Shutdown command timeout is expected - "
|
|
683
|
+
"server may have closed before responding"
|
|
684
|
+
)
|
|
685
|
+
# Return a success response for shutdown
|
|
686
|
+
return {
|
|
687
|
+
'message': 'Browser shutdown (no response received)'
|
|
688
|
+
}
|
|
545
689
|
raise RuntimeError(
|
|
546
690
|
f"Timeout waiting for response to command: {command}"
|
|
547
691
|
)
|
|
@@ -555,6 +699,12 @@ class WebSocketBrowserWrapper:
|
|
|
555
699
|
"close frame" in str(e)
|
|
556
700
|
or "connection closed" in str(e).lower()
|
|
557
701
|
):
|
|
702
|
+
# Special handling for shutdown command
|
|
703
|
+
if command == 'shutdown':
|
|
704
|
+
logger.debug(
|
|
705
|
+
f"Connection closed during shutdown (expected): {e}"
|
|
706
|
+
)
|
|
707
|
+
return {'message': 'Browser shutdown (connection closed)'}
|
|
558
708
|
logger.error(f"WebSocket connection closed unexpectedly: {e}")
|
|
559
709
|
# Mark connection as closed
|
|
560
710
|
self.websocket = None
|
|
@@ -575,17 +725,31 @@ class WebSocketBrowserWrapper:
|
|
|
575
725
|
response = await self._send_command(
|
|
576
726
|
'open_browser', {'startUrl': start_url}
|
|
577
727
|
)
|
|
728
|
+
self._browser_opened = True
|
|
578
729
|
return response
|
|
579
730
|
|
|
580
731
|
@action_logger
|
|
581
732
|
async def close_browser(self) -> str:
|
|
582
733
|
"""Close browser."""
|
|
583
734
|
response = await self._send_command('close_browser', {})
|
|
735
|
+
self._browser_opened = False
|
|
584
736
|
return response['message']
|
|
585
737
|
|
|
586
738
|
@action_logger
|
|
587
739
|
async def visit_page(self, url: str) -> Dict[str, Any]:
|
|
588
|
-
"""Visit a page.
|
|
740
|
+
"""Visit a page.
|
|
741
|
+
|
|
742
|
+
In non-CDP mode, automatically opens browser if not already open.
|
|
743
|
+
"""
|
|
744
|
+
if not self._browser_opened:
|
|
745
|
+
is_cdp_mode = bool(self.config.get('cdpUrl'))
|
|
746
|
+
|
|
747
|
+
if not is_cdp_mode:
|
|
748
|
+
logger.info(
|
|
749
|
+
"Browser not open, automatically opening browser..."
|
|
750
|
+
)
|
|
751
|
+
await self.open_browser()
|
|
752
|
+
|
|
589
753
|
response = await self._send_command('visit_page', {'url': url})
|
|
590
754
|
return response
|
|
591
755
|
|
|
@@ -680,6 +844,8 @@ class WebSocketBrowserWrapper:
|
|
|
680
844
|
async def type(self, ref: str, text: str) -> Dict[str, Any]:
|
|
681
845
|
"""Type text into an element."""
|
|
682
846
|
response = await self._send_command('type', {'ref': ref, 'text': text})
|
|
847
|
+
# Log the response for debugging
|
|
848
|
+
logger.debug(f"Type response for ref {ref}: {response}")
|
|
683
849
|
return response
|
|
684
850
|
|
|
685
851
|
@action_logger
|
|
@@ -92,12 +92,12 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
|
|
|
92
92
|
user_data_dir: Optional[str] = None,
|
|
93
93
|
stealth: bool = False,
|
|
94
94
|
web_agent_model: Optional[BaseModelBackend] = None,
|
|
95
|
-
cache_dir: str =
|
|
95
|
+
cache_dir: Optional[str] = None,
|
|
96
96
|
enabled_tools: Optional[List[str]] = None,
|
|
97
97
|
browser_log_to_file: bool = False,
|
|
98
98
|
log_dir: Optional[str] = None,
|
|
99
99
|
session_id: Optional[str] = None,
|
|
100
|
-
default_start_url: str =
|
|
100
|
+
default_start_url: Optional[str] = None,
|
|
101
101
|
default_timeout: Optional[int] = None,
|
|
102
102
|
short_timeout: Optional[int] = None,
|
|
103
103
|
navigation_timeout: Optional[int] = None,
|
|
@@ -202,10 +202,10 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
|
|
|
202
202
|
self._user_data_dir = user_data_dir
|
|
203
203
|
self._stealth = stealth
|
|
204
204
|
self._web_agent_model = web_agent_model
|
|
205
|
-
self._cache_dir = cache_dir
|
|
205
|
+
self._cache_dir = cache_dir or "tmp/"
|
|
206
206
|
self._browser_log_to_file = browser_log_to_file
|
|
207
207
|
self._log_dir = log_dir
|
|
208
|
-
self._default_start_url = default_start_url
|
|
208
|
+
self._default_start_url = default_start_url or "https://google.com/"
|
|
209
209
|
self._session_id = session_id or "default"
|
|
210
210
|
self._viewport_limit = viewport_limit
|
|
211
211
|
|
|
@@ -25,12 +25,38 @@ logger = get_logger(__name__)
|
|
|
25
25
|
|
|
26
26
|
@MCPServer()
|
|
27
27
|
class MarkItDownToolkit(BaseToolkit):
|
|
28
|
-
r"""A class representing a toolkit for MarkItDown.
|
|
28
|
+
r"""A class representing a toolkit for MarkItDown.
|
|
29
|
+
|
|
30
|
+
.. deprecated::
|
|
31
|
+
MarkItDownToolkit is deprecated. Use FileToolkit instead, which now
|
|
32
|
+
includes the same functionality through its read_file method that
|
|
33
|
+
supports both single files and multiple files.
|
|
34
|
+
|
|
35
|
+
Example migration:
|
|
36
|
+
# Old way
|
|
37
|
+
from camel.toolkits import MarkItDownToolkit
|
|
38
|
+
toolkit = MarkItDownToolkit()
|
|
39
|
+
content = toolkit.read_files(['file1.pdf', 'file2.docx'])
|
|
40
|
+
|
|
41
|
+
# New way
|
|
42
|
+
from camel.toolkits import FileToolkit
|
|
43
|
+
toolkit = FileToolkit()
|
|
44
|
+
content = toolkit.read_file(['file1.pdf', 'file2.docx'])
|
|
45
|
+
"""
|
|
29
46
|
|
|
30
47
|
def __init__(
|
|
31
48
|
self,
|
|
32
49
|
timeout: Optional[float] = None,
|
|
33
50
|
):
|
|
51
|
+
import warnings
|
|
52
|
+
|
|
53
|
+
warnings.warn(
|
|
54
|
+
"MarkItDownToolkit is deprecated and will be removed in a future "
|
|
55
|
+
"version. Please use FileToolkit instead, which now includes "
|
|
56
|
+
"read_file method that supports both single and multiple files.",
|
|
57
|
+
DeprecationWarning,
|
|
58
|
+
stacklevel=2,
|
|
59
|
+
)
|
|
34
60
|
super().__init__(timeout=timeout)
|
|
35
61
|
|
|
36
62
|
def read_files(self, file_paths: List[str]) -> Dict[str, str]:
|
|
@@ -138,33 +138,43 @@ class NoteTakingToolkit(BaseToolkit):
|
|
|
138
138
|
self.registry.append(note_name)
|
|
139
139
|
self._save_registry()
|
|
140
140
|
|
|
141
|
-
def create_note(
|
|
141
|
+
def create_note(
|
|
142
|
+
self, note_name: str, content: str, overwrite: bool = False
|
|
143
|
+
) -> str:
|
|
142
144
|
r"""Creates a new note with a unique name.
|
|
143
145
|
|
|
144
146
|
This function will create a new file for your note.
|
|
145
|
-
|
|
146
|
-
to add content to an existing note, use the `append_note`
|
|
147
|
-
instead.
|
|
147
|
+
By default, you must provide a `note_name` that does not already exist.
|
|
148
|
+
If you want to add content to an existing note, use the `append_note`
|
|
149
|
+
function instead. If you want to overwrite an existing note, set
|
|
150
|
+
`overwrite=True`.
|
|
148
151
|
|
|
149
152
|
Args:
|
|
150
153
|
note_name (str): The name for your new note (without the .md
|
|
151
|
-
extension). This name must be unique.
|
|
154
|
+
extension). This name must be unique unless overwrite is True.
|
|
152
155
|
content (str): The initial content to write in the note.
|
|
156
|
+
overwrite (bool): Whether to overwrite an existing note.
|
|
157
|
+
Defaults to False.
|
|
153
158
|
|
|
154
159
|
Returns:
|
|
155
160
|
str: A message confirming the creation of the note or an error if
|
|
156
|
-
the note name is not valid or already exists
|
|
161
|
+
the note name is not valid or already exists
|
|
162
|
+
(when overwrite=False).
|
|
157
163
|
"""
|
|
158
164
|
try:
|
|
159
165
|
note_path = self.working_directory / f"{note_name}.md"
|
|
166
|
+
existed_before = note_path.exists()
|
|
160
167
|
|
|
161
|
-
if
|
|
168
|
+
if existed_before and not overwrite:
|
|
162
169
|
return f"Error: Note '{note_name}.md' already exists."
|
|
163
170
|
|
|
164
171
|
note_path.write_text(content, encoding="utf-8")
|
|
165
172
|
self._register_note(note_name)
|
|
166
173
|
|
|
167
|
-
|
|
174
|
+
if existed_before and overwrite:
|
|
175
|
+
return f"Note '{note_name}.md' successfully overwritten."
|
|
176
|
+
else:
|
|
177
|
+
return f"Note '{note_name}.md' successfully created."
|
|
168
178
|
except Exception as e:
|
|
169
179
|
return f"Error creating note: {e}"
|
|
170
180
|
|