camel-ai 0.2.71a12__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.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +260 -488
- camel/memories/agent_memories.py +39 -0
- camel/memories/base.py +8 -0
- camel/models/gemini_model.py +30 -2
- camel/models/moonshot_model.py +36 -4
- camel/models/openai_model.py +29 -15
- camel/societies/workforce/prompts.py +24 -14
- camel/societies/workforce/single_agent_worker.py +9 -7
- camel/societies/workforce/workforce.py +44 -16
- camel/storages/vectordb_storages/__init__.py +1 -0
- camel/storages/vectordb_storages/surreal.py +415 -0
- camel/toolkits/__init__.py +10 -1
- camel/toolkits/base.py +57 -1
- camel/toolkits/human_toolkit.py +5 -1
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +127 -414
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +783 -1626
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +489 -0
- camel/toolkits/markitdown_toolkit.py +2 -2
- camel/toolkits/message_integration.py +592 -0
- camel/toolkits/note_taking_toolkit.py +195 -26
- camel/toolkits/openai_image_toolkit.py +5 -5
- camel/toolkits/origene_mcp_toolkit.py +97 -0
- camel/toolkits/screenshot_toolkit.py +213 -0
- camel/toolkits/search_toolkit.py +115 -36
- camel/toolkits/terminal_toolkit.py +379 -165
- camel/toolkits/video_analysis_toolkit.py +13 -13
- camel/toolkits/video_download_toolkit.py +11 -11
- camel/toolkits/web_deploy_toolkit.py +1024 -0
- camel/types/enums.py +6 -3
- camel/types/unified_model_type.py +16 -4
- camel/utils/mcp_client.py +8 -0
- {camel_ai-0.2.71a12.dist-info → camel_ai-0.2.72.dist-info}/METADATA +6 -3
- {camel_ai-0.2.71a12.dist-info → camel_ai-0.2.72.dist-info}/RECORD +36 -36
- camel/toolkits/hybrid_browser_toolkit/actions.py +0 -417
- camel/toolkits/hybrid_browser_toolkit/agent.py +0 -311
- camel/toolkits/hybrid_browser_toolkit/browser_session.py +0 -739
- camel/toolkits/hybrid_browser_toolkit/snapshot.py +0 -227
- camel/toolkits/hybrid_browser_toolkit/stealth_script.js +0 -0
- camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +0 -1002
- {camel_ai-0.2.71a12.dist-info → camel_ai-0.2.72.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.71a12.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
|
|
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.
|
|
77
|
+
FunctionTool(self.read_files),
|
|
78
78
|
]
|