camel-ai 0.2.72a2__py3-none-any.whl → 0.2.72a5__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 +92 -18
- camel/models/openai_model.py +29 -15
- camel/toolkits/__init__.py +2 -0
- camel/toolkits/hybrid_browser_toolkit/browser_session.py +124 -123
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +19 -19
- camel/toolkits/origene_mcp_toolkit.py +95 -0
- camel/toolkits/terminal_toolkit.py +135 -158
- 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.72a2.dist-info → camel_ai-0.2.72a5.dist-info}/METADATA +3 -3
- {camel_ai-0.2.72a2.dist-info → camel_ai-0.2.72a5.dist-info}/RECORD +15 -14
- {camel_ai-0.2.72a2.dist-info → camel_ai-0.2.72a5.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.72a2.dist-info → camel_ai-0.2.72a5.dist-info}/licenses/LICENSE +0 -0
|
@@ -33,6 +33,20 @@ if TYPE_CHECKING:
|
|
|
33
33
|
logger = get_logger(__name__)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
class TabIdGenerator:
|
|
37
|
+
"""Monotonically increasing tab ID generator."""
|
|
38
|
+
|
|
39
|
+
_counter: int = 0
|
|
40
|
+
_lock: ClassVar[asyncio.Lock] = asyncio.Lock()
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
async def generate_tab_id(cls) -> str:
|
|
44
|
+
"""Generate a monotonically increasing tab ID."""
|
|
45
|
+
async with cls._lock:
|
|
46
|
+
cls._counter += 1
|
|
47
|
+
return f"tab-{cls._counter:03d}"
|
|
48
|
+
|
|
49
|
+
|
|
36
50
|
class HybridBrowserSession:
|
|
37
51
|
"""Lightweight wrapper around Playwright for
|
|
38
52
|
browsing with multi-tab support.
|
|
@@ -172,9 +186,9 @@ class HybridBrowserSession:
|
|
|
172
186
|
self._context: Optional[BrowserContext] = None
|
|
173
187
|
self._page: Optional[Page] = None
|
|
174
188
|
|
|
175
|
-
#
|
|
176
|
-
self._pages:
|
|
177
|
-
self.
|
|
189
|
+
# Dictionary-based tab management with monotonic IDs
|
|
190
|
+
self._pages: Dict[str, Page] = {} # tab_id -> Page object
|
|
191
|
+
self._current_tab_id: Optional[str] = None # Current active tab ID
|
|
178
192
|
|
|
179
193
|
self.snapshot: Optional[PageSnapshot] = None
|
|
180
194
|
self.executor: Optional[ActionExecutor] = None
|
|
@@ -221,20 +235,23 @@ class HybridBrowserSession:
|
|
|
221
235
|
# ------------------------------------------------------------------
|
|
222
236
|
# Multi-tab management methods
|
|
223
237
|
# ------------------------------------------------------------------
|
|
224
|
-
async def create_new_tab(self, url: Optional[str] = None) ->
|
|
238
|
+
async def create_new_tab(self, url: Optional[str] = None) -> str:
|
|
225
239
|
r"""Create a new tab and optionally navigate to a URL.
|
|
226
240
|
|
|
227
241
|
Args:
|
|
228
242
|
url: Optional URL to navigate to in the new tab
|
|
229
243
|
|
|
230
244
|
Returns:
|
|
231
|
-
|
|
245
|
+
str: ID of the newly created tab
|
|
232
246
|
"""
|
|
233
247
|
await self.ensure_browser()
|
|
234
248
|
|
|
235
249
|
if self._context is None:
|
|
236
250
|
raise RuntimeError("Browser context is not available")
|
|
237
251
|
|
|
252
|
+
# Generate unique tab ID
|
|
253
|
+
tab_id = await TabIdGenerator.generate_tab_id()
|
|
254
|
+
|
|
238
255
|
# Create new page
|
|
239
256
|
new_page = await self._context.new_page()
|
|
240
257
|
|
|
@@ -248,9 +265,8 @@ class HybridBrowserSession:
|
|
|
248
265
|
f"Failed to apply stealth script to new tab: {e}"
|
|
249
266
|
)
|
|
250
267
|
|
|
251
|
-
#
|
|
252
|
-
self._pages
|
|
253
|
-
new_tab_index = len(self._pages) - 1
|
|
268
|
+
# Store in pages dictionary
|
|
269
|
+
self._pages[tab_id] = new_page
|
|
254
270
|
|
|
255
271
|
# Navigate if URL provided
|
|
256
272
|
if url:
|
|
@@ -261,196 +277,168 @@ class HybridBrowserSession:
|
|
|
261
277
|
logger.warning(f"Failed to navigate new tab to {url}: {e}")
|
|
262
278
|
|
|
263
279
|
logger.info(
|
|
264
|
-
f"Created new tab {
|
|
280
|
+
f"Created new tab {tab_id}, total tabs: {len(self._pages)}"
|
|
265
281
|
)
|
|
266
|
-
return
|
|
282
|
+
return tab_id
|
|
267
283
|
|
|
268
|
-
async def register_page(self, new_page: "Page") ->
|
|
284
|
+
async def register_page(self, new_page: "Page") -> str:
|
|
269
285
|
r"""Register a page that was created externally (e.g., by a click).
|
|
270
286
|
|
|
271
287
|
Args:
|
|
272
288
|
new_page (Page): The new page object to register.
|
|
273
289
|
|
|
274
290
|
Returns:
|
|
275
|
-
|
|
291
|
+
str: The ID of the (newly) registered tab.
|
|
276
292
|
"""
|
|
277
|
-
if
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
self._pages.append(new_page)
|
|
287
|
-
new_tab_index = len(self._pages) - 1
|
|
293
|
+
# Check if page is already registered
|
|
294
|
+
for tab_id, page in self._pages.items():
|
|
295
|
+
if page is new_page:
|
|
296
|
+
return tab_id
|
|
297
|
+
|
|
298
|
+
# Create new ID for the page
|
|
299
|
+
tab_id = await TabIdGenerator.generate_tab_id()
|
|
300
|
+
self._pages[tab_id] = new_page
|
|
301
|
+
|
|
288
302
|
logger.info(
|
|
289
|
-
f"Registered new tab {
|
|
303
|
+
f"Registered new tab {tab_id} (opened by user action). "
|
|
290
304
|
f"Total tabs: {len(self._pages)}"
|
|
291
305
|
)
|
|
292
|
-
return
|
|
306
|
+
return tab_id
|
|
293
307
|
|
|
294
|
-
async def switch_to_tab(self,
|
|
295
|
-
r"""Switch to a specific tab by
|
|
308
|
+
async def switch_to_tab(self, tab_id: str) -> bool:
|
|
309
|
+
r"""Switch to a specific tab by ID.
|
|
296
310
|
|
|
297
311
|
Args:
|
|
298
|
-
|
|
312
|
+
tab_id: ID of the tab to switch to
|
|
299
313
|
|
|
300
314
|
Returns:
|
|
301
|
-
bool: True if successful, False if tab
|
|
315
|
+
bool: True if successful, False if tab ID is invalid
|
|
302
316
|
"""
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
logger.warning("No tabs available")
|
|
307
|
-
return False
|
|
317
|
+
if tab_id not in self._pages:
|
|
318
|
+
logger.warning(f"Invalid tab ID: {tab_id}")
|
|
319
|
+
return False
|
|
308
320
|
|
|
309
|
-
|
|
310
|
-
current_pages = self._pages.copy()
|
|
311
|
-
pages_count = len(current_pages)
|
|
321
|
+
page = self._pages[tab_id]
|
|
312
322
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
323
|
+
# Check if page is still valid
|
|
324
|
+
if page.is_closed():
|
|
325
|
+
logger.warning(f"Tab {tab_id} is closed, removing from registry")
|
|
326
|
+
# Clean up closed tab
|
|
327
|
+
del self._pages[tab_id]
|
|
328
|
+
return False
|
|
319
329
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
logger.warning(
|
|
324
|
-
f"Tab {tab_index} is closed, removing from list"
|
|
325
|
-
)
|
|
326
|
-
# Remove closed page from original list
|
|
327
|
-
if (
|
|
328
|
-
tab_index < len(self._pages)
|
|
329
|
-
and self._pages[tab_index] is page
|
|
330
|
-
):
|
|
331
|
-
self._pages.pop(tab_index)
|
|
332
|
-
# Adjust current tab index if necessary
|
|
333
|
-
if self._current_tab_index >= len(self._pages):
|
|
334
|
-
self._current_tab_index = max(0, len(self._pages) - 1)
|
|
335
|
-
return False
|
|
336
|
-
|
|
337
|
-
self._current_tab_index = tab_index
|
|
330
|
+
try:
|
|
331
|
+
# Switch to the tab
|
|
332
|
+
self._current_tab_id = tab_id
|
|
338
333
|
self._page = page
|
|
339
334
|
|
|
340
335
|
# Bring the tab to the front in the browser window
|
|
341
|
-
await
|
|
336
|
+
await page.bring_to_front()
|
|
342
337
|
|
|
343
|
-
# Update
|
|
338
|
+
# Update utilities for new tab
|
|
344
339
|
self.executor = ActionExecutor(
|
|
345
|
-
|
|
340
|
+
page,
|
|
346
341
|
self,
|
|
347
342
|
default_timeout=self._default_timeout,
|
|
348
343
|
short_timeout=self._short_timeout,
|
|
349
344
|
)
|
|
350
|
-
self.snapshot = PageSnapshot(
|
|
345
|
+
self.snapshot = PageSnapshot(page)
|
|
351
346
|
|
|
352
|
-
logger.info(f"Switched to tab {
|
|
347
|
+
logger.info(f"Switched to tab {tab_id}")
|
|
353
348
|
return True
|
|
354
349
|
|
|
355
350
|
except Exception as e:
|
|
356
|
-
logger.warning(f"Error switching to tab {
|
|
351
|
+
logger.warning(f"Error switching to tab {tab_id}: {e}")
|
|
357
352
|
return False
|
|
358
353
|
|
|
359
|
-
async def close_tab(self,
|
|
360
|
-
r"""Close a specific tab.
|
|
354
|
+
async def close_tab(self, tab_id: str) -> bool:
|
|
355
|
+
r"""Close a specific tab by ID.
|
|
361
356
|
|
|
362
357
|
Args:
|
|
363
|
-
|
|
358
|
+
tab_id: ID of the tab to close
|
|
364
359
|
|
|
365
360
|
Returns:
|
|
366
|
-
bool: True if successful, False if tab
|
|
361
|
+
bool: True if successful, False if tab ID is invalid
|
|
367
362
|
"""
|
|
368
|
-
if not
|
|
363
|
+
if tab_id not in self._pages:
|
|
364
|
+
logger.warning(f"Invalid tab ID: {tab_id}")
|
|
369
365
|
return False
|
|
370
366
|
|
|
367
|
+
page = self._pages[tab_id]
|
|
368
|
+
|
|
371
369
|
try:
|
|
372
|
-
page
|
|
370
|
+
# Close the page if not already closed
|
|
373
371
|
if not page.is_closed():
|
|
374
372
|
await page.close()
|
|
375
373
|
|
|
376
|
-
# Remove from our
|
|
377
|
-
self._pages
|
|
374
|
+
# Remove from our dictionary
|
|
375
|
+
del self._pages[tab_id]
|
|
378
376
|
|
|
379
377
|
# If we closed the current tab, switch to another one
|
|
380
|
-
if
|
|
378
|
+
if tab_id == self._current_tab_id:
|
|
381
379
|
if self._pages:
|
|
382
|
-
# Switch to
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
0, min(tab_index - 1, len(self._pages) - 1)
|
|
386
|
-
)
|
|
387
|
-
await self.switch_to_tab(new_index)
|
|
380
|
+
# Switch to any available tab (first one we find)
|
|
381
|
+
next_tab_id = next(iter(self._pages.keys()))
|
|
382
|
+
await self.switch_to_tab(next_tab_id)
|
|
388
383
|
else:
|
|
389
384
|
# No tabs left
|
|
390
|
-
self.
|
|
385
|
+
self._current_tab_id = None
|
|
391
386
|
self._page = None
|
|
392
387
|
self.executor = None
|
|
393
388
|
self.snapshot = None
|
|
394
|
-
elif tab_index < self._current_tab_index:
|
|
395
|
-
# Adjust current tab index since we removed a tab before it
|
|
396
|
-
self._current_tab_index -= 1
|
|
397
389
|
|
|
398
390
|
logger.info(
|
|
399
|
-
f"Closed tab {
|
|
391
|
+
f"Closed tab {tab_id}, remaining tabs: {len(self._pages)}"
|
|
400
392
|
)
|
|
401
393
|
return True
|
|
402
394
|
|
|
403
395
|
except Exception as e:
|
|
404
|
-
logger.warning(f"Error closing tab {
|
|
396
|
+
logger.warning(f"Error closing tab {tab_id}: {e}")
|
|
405
397
|
return False
|
|
406
398
|
|
|
407
399
|
async def get_tab_info(self) -> List[Dict[str, Any]]:
|
|
408
|
-
r"""Get information about all open tabs.
|
|
400
|
+
r"""Get information about all open tabs including IDs.
|
|
409
401
|
|
|
410
402
|
Returns:
|
|
411
403
|
List of dictionaries containing tab information
|
|
412
404
|
"""
|
|
413
405
|
tab_info = []
|
|
414
|
-
|
|
406
|
+
tabs_to_cleanup = []
|
|
407
|
+
|
|
408
|
+
# Process all tabs in dictionary
|
|
409
|
+
for tab_id, page in list(self._pages.items()):
|
|
415
410
|
try:
|
|
416
411
|
if not page.is_closed():
|
|
417
412
|
title = await page.title()
|
|
418
413
|
url = page.url
|
|
419
|
-
is_current =
|
|
414
|
+
is_current = tab_id == self._current_tab_id
|
|
420
415
|
tab_info.append(
|
|
421
416
|
{
|
|
422
|
-
"
|
|
417
|
+
"tab_id": tab_id,
|
|
423
418
|
"title": title,
|
|
424
419
|
"url": url,
|
|
425
420
|
"is_current": is_current,
|
|
426
421
|
}
|
|
427
422
|
)
|
|
428
423
|
else:
|
|
429
|
-
# Mark
|
|
430
|
-
|
|
431
|
-
{
|
|
432
|
-
"index": i,
|
|
433
|
-
"title": "[CLOSED]",
|
|
434
|
-
"url": "",
|
|
435
|
-
"is_current": False,
|
|
436
|
-
}
|
|
437
|
-
)
|
|
424
|
+
# Mark for cleanup
|
|
425
|
+
tabs_to_cleanup.append(tab_id)
|
|
438
426
|
except Exception as e:
|
|
439
|
-
logger.warning(f"Error getting info for tab {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
)
|
|
427
|
+
logger.warning(f"Error getting info for tab {tab_id}: {e}")
|
|
428
|
+
tabs_to_cleanup.append(tab_id)
|
|
429
|
+
|
|
430
|
+
# Clean up closed/invalid tabs
|
|
431
|
+
for tab_id in tabs_to_cleanup:
|
|
432
|
+
if tab_id in self._pages:
|
|
433
|
+
del self._pages[tab_id]
|
|
448
434
|
|
|
449
435
|
return tab_info
|
|
450
436
|
|
|
451
|
-
async def
|
|
452
|
-
r"""Get the
|
|
453
|
-
|
|
437
|
+
async def get_current_tab_id(self) -> Optional[str]:
|
|
438
|
+
r"""Get the id for the current active tab."""
|
|
439
|
+
if not self._current_tab_id or not self._pages:
|
|
440
|
+
return None
|
|
441
|
+
return self._current_tab_id
|
|
454
442
|
|
|
455
443
|
# ------------------------------------------------------------------
|
|
456
444
|
# Browser lifecycle helpers
|
|
@@ -470,7 +458,7 @@ class HybridBrowserSession:
|
|
|
470
458
|
self._context = singleton_instance._context
|
|
471
459
|
self._page = singleton_instance._page
|
|
472
460
|
self._pages = singleton_instance._pages
|
|
473
|
-
self.
|
|
461
|
+
self._current_tab_id = singleton_instance._current_tab_id
|
|
474
462
|
self.snapshot = singleton_instance.snapshot
|
|
475
463
|
self.executor = singleton_instance.executor
|
|
476
464
|
return
|
|
@@ -512,17 +500,30 @@ class HybridBrowserSession:
|
|
|
512
500
|
pages = context.pages
|
|
513
501
|
if pages:
|
|
514
502
|
self._page = pages[0]
|
|
515
|
-
|
|
503
|
+
# Create ID for initial page
|
|
504
|
+
initial_tab_id = await TabIdGenerator.generate_tab_id()
|
|
505
|
+
self._pages[initial_tab_id] = pages[0]
|
|
506
|
+
self._current_tab_id = initial_tab_id
|
|
507
|
+
# Handle additional pages if any
|
|
508
|
+
for page in pages[1:]:
|
|
509
|
+
tab_id = await TabIdGenerator.generate_tab_id()
|
|
510
|
+
self._pages[tab_id] = page
|
|
516
511
|
else:
|
|
517
512
|
self._page = await context.new_page()
|
|
518
|
-
|
|
513
|
+
initial_tab_id = await TabIdGenerator.generate_tab_id()
|
|
514
|
+
self._pages[initial_tab_id] = self._page
|
|
515
|
+
self._current_tab_id = initial_tab_id
|
|
519
516
|
else:
|
|
520
517
|
self._browser = await self._playwright.chromium.launch(
|
|
521
518
|
**launch_options
|
|
522
519
|
)
|
|
523
520
|
self._context = await self._browser.new_context(**context_options)
|
|
524
521
|
self._page = await self._context.new_page()
|
|
525
|
-
|
|
522
|
+
|
|
523
|
+
# Create ID for initial page
|
|
524
|
+
initial_tab_id = await TabIdGenerator.generate_tab_id()
|
|
525
|
+
self._pages[initial_tab_id] = self._page
|
|
526
|
+
self._current_tab_id = initial_tab_id
|
|
526
527
|
|
|
527
528
|
# Apply stealth modifications if enabled
|
|
528
529
|
if self._stealth and self._stealth_script:
|
|
@@ -544,7 +545,6 @@ class HybridBrowserSession:
|
|
|
544
545
|
default_timeout=self._default_timeout,
|
|
545
546
|
short_timeout=self._short_timeout,
|
|
546
547
|
)
|
|
547
|
-
self._current_tab_index = 0
|
|
548
548
|
|
|
549
549
|
logger.info("Browser session initialized successfully")
|
|
550
550
|
|
|
@@ -593,8 +593,8 @@ class HybridBrowserSession:
|
|
|
593
593
|
logger.error(f"Error during browser session close: {e}")
|
|
594
594
|
finally:
|
|
595
595
|
self._page = None
|
|
596
|
-
self._pages =
|
|
597
|
-
self.
|
|
596
|
+
self._pages = {}
|
|
597
|
+
self._current_tab_id = None
|
|
598
598
|
self.snapshot = None
|
|
599
599
|
self.executor = None
|
|
600
600
|
|
|
@@ -602,7 +602,7 @@ class HybridBrowserSession:
|
|
|
602
602
|
r"""Internal session close logic with thorough cleanup."""
|
|
603
603
|
try:
|
|
604
604
|
# Close all pages first
|
|
605
|
-
pages_to_close = self._pages.
|
|
605
|
+
pages_to_close = list(self._pages.values())
|
|
606
606
|
for page in pages_to_close:
|
|
607
607
|
try:
|
|
608
608
|
if not page.is_closed():
|
|
@@ -614,7 +614,7 @@ class HybridBrowserSession:
|
|
|
614
614
|
except Exception as e:
|
|
615
615
|
logger.warning(f"Error closing page: {e}")
|
|
616
616
|
|
|
617
|
-
# Clear the pages
|
|
617
|
+
# Clear the pages dictionary
|
|
618
618
|
self._pages.clear()
|
|
619
619
|
|
|
620
620
|
# Close context with explicit wait
|
|
@@ -658,7 +658,8 @@ class HybridBrowserSession:
|
|
|
658
658
|
finally:
|
|
659
659
|
# Ensure all attributes are cleared regardless of errors
|
|
660
660
|
self._page = None
|
|
661
|
-
self._pages =
|
|
661
|
+
self._pages = {}
|
|
662
|
+
self._current_tab_id = None
|
|
662
663
|
self._context = None
|
|
663
664
|
self._browser = None
|
|
664
665
|
self._playwright = None
|
|
@@ -766,7 +766,7 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
766
766
|
# Add debug info for tab info retrieval
|
|
767
767
|
logger.debug("Attempting to get tab info from session...")
|
|
768
768
|
tab_info = await session.get_tab_info()
|
|
769
|
-
current_tab_index = await session.
|
|
769
|
+
current_tab_index = await session.get_current_tab_id()
|
|
770
770
|
|
|
771
771
|
# Debug log the successful retrieval
|
|
772
772
|
logger.debug(
|
|
@@ -814,7 +814,7 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
814
814
|
try:
|
|
815
815
|
open_pages = [
|
|
816
816
|
p
|
|
817
|
-
for p in fallback_session._pages
|
|
817
|
+
for p in fallback_session._pages.values()
|
|
818
818
|
if not p.is_closed()
|
|
819
819
|
]
|
|
820
820
|
actual_tab_count = len(open_pages)
|
|
@@ -1228,9 +1228,9 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1228
1228
|
if should_create_new_tab:
|
|
1229
1229
|
logger.info(f"Creating new tab and navigating to URL: {url}")
|
|
1230
1230
|
try:
|
|
1231
|
-
|
|
1232
|
-
await session.switch_to_tab(
|
|
1233
|
-
nav_result = f"Visited {url} in new tab {
|
|
1231
|
+
new_tab_id = await session.create_new_tab(url)
|
|
1232
|
+
await session.switch_to_tab(new_tab_id)
|
|
1233
|
+
nav_result = f"Visited {url} in new tab {new_tab_id}"
|
|
1234
1234
|
except Exception as e:
|
|
1235
1235
|
logger.error(f"Failed to create new tab and navigate: {e}")
|
|
1236
1236
|
nav_result = f"Error creating new tab: {e}"
|
|
@@ -1897,14 +1897,14 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1897
1897
|
)
|
|
1898
1898
|
|
|
1899
1899
|
@action_logger
|
|
1900
|
-
async def switch_tab(self, *,
|
|
1901
|
-
r"""Switches to a different browser tab using its
|
|
1900
|
+
async def switch_tab(self, *, tab_id: str) -> Dict[str, Any]:
|
|
1901
|
+
r"""Switches to a different browser tab using its ID.
|
|
1902
1902
|
|
|
1903
1903
|
After switching, all actions will apply to the new tab. Use
|
|
1904
|
-
`get_tab_info` to find the
|
|
1904
|
+
`get_tab_info` to find the ID of the tab you want to switch to.
|
|
1905
1905
|
|
|
1906
1906
|
Args:
|
|
1907
|
-
|
|
1907
|
+
tab_id (str): The ID of the tab to activate.
|
|
1908
1908
|
|
|
1909
1909
|
Returns:
|
|
1910
1910
|
Dict[str, Any]: A dictionary with the result of the action:
|
|
@@ -1917,7 +1917,7 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1917
1917
|
await self._ensure_browser()
|
|
1918
1918
|
session = await self._get_session()
|
|
1919
1919
|
|
|
1920
|
-
success = await session.switch_to_tab(
|
|
1920
|
+
success = await session.switch_to_tab(tab_id)
|
|
1921
1921
|
|
|
1922
1922
|
if success:
|
|
1923
1923
|
snapshot = await session.get_snapshot(
|
|
@@ -1926,14 +1926,14 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1926
1926
|
tab_info = await self._get_tab_info_for_output()
|
|
1927
1927
|
|
|
1928
1928
|
result = {
|
|
1929
|
-
"result": f"Successfully switched to tab {
|
|
1929
|
+
"result": f"Successfully switched to tab {tab_id}",
|
|
1930
1930
|
"snapshot": snapshot,
|
|
1931
1931
|
**tab_info,
|
|
1932
1932
|
}
|
|
1933
1933
|
else:
|
|
1934
1934
|
tab_info = await self._get_tab_info_for_output()
|
|
1935
1935
|
result = {
|
|
1936
|
-
"result": f"Failed to switch to tab {
|
|
1936
|
+
"result": f"Failed to switch to tab {tab_id}. Tab may not "
|
|
1937
1937
|
f"exist.",
|
|
1938
1938
|
"snapshot": "",
|
|
1939
1939
|
**tab_info,
|
|
@@ -1942,14 +1942,14 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1942
1942
|
return result
|
|
1943
1943
|
|
|
1944
1944
|
@action_logger
|
|
1945
|
-
async def close_tab(self, *,
|
|
1946
|
-
r"""Closes a browser tab using its
|
|
1945
|
+
async def close_tab(self, *, tab_id: str) -> Dict[str, Any]:
|
|
1946
|
+
r"""Closes a browser tab using its ID.
|
|
1947
1947
|
|
|
1948
|
-
Use `get_tab_info` to find the
|
|
1948
|
+
Use `get_tab_info` to find the ID of the tab to close. After
|
|
1949
1949
|
closing, the browser will switch to another tab if available.
|
|
1950
1950
|
|
|
1951
1951
|
Args:
|
|
1952
|
-
|
|
1952
|
+
tab_id (str): The ID of the tab to close.
|
|
1953
1953
|
|
|
1954
1954
|
Returns:
|
|
1955
1955
|
Dict[str, Any]: A dictionary with the result of the action:
|
|
@@ -1962,7 +1962,7 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1962
1962
|
await self._ensure_browser()
|
|
1963
1963
|
session = await self._get_session()
|
|
1964
1964
|
|
|
1965
|
-
success = await session.close_tab(
|
|
1965
|
+
success = await session.close_tab(tab_id)
|
|
1966
1966
|
|
|
1967
1967
|
if success:
|
|
1968
1968
|
# Get current state after closing the tab
|
|
@@ -1976,14 +1976,14 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1976
1976
|
tab_info = await self._get_tab_info_for_output()
|
|
1977
1977
|
|
|
1978
1978
|
result = {
|
|
1979
|
-
"result": f"Successfully closed tab {
|
|
1979
|
+
"result": f"Successfully closed tab {tab_id}",
|
|
1980
1980
|
"snapshot": snapshot,
|
|
1981
1981
|
**tab_info,
|
|
1982
1982
|
}
|
|
1983
1983
|
else:
|
|
1984
1984
|
tab_info = await self._get_tab_info_for_output()
|
|
1985
1985
|
result = {
|
|
1986
|
-
"result": f"Failed to close tab {
|
|
1986
|
+
"result": f"Failed to close tab {tab_id}. Tab may not "
|
|
1987
1987
|
f"exist.",
|
|
1988
1988
|
"snapshot": "",
|
|
1989
1989
|
**tab_info,
|
|
@@ -0,0 +1,95 @@
|
|
|
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 typing import List, Optional
|
|
16
|
+
|
|
17
|
+
from camel.toolkits import BaseToolkit, FunctionTool, MCPToolkit
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OrigeneToolkit(BaseToolkit):
|
|
21
|
+
r"""OrigeneToolkit provides an interface for interacting with
|
|
22
|
+
Origene MCP server.
|
|
23
|
+
|
|
24
|
+
This toolkit can be used as an async context manager for automatic
|
|
25
|
+
connection management:
|
|
26
|
+
|
|
27
|
+
async with OrigeneToolkit() as toolkit:
|
|
28
|
+
tools = toolkit.get_tools()
|
|
29
|
+
# Toolkit is automatically disconnected when exiting
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
timeout (Optional[float]): Connection timeout in seconds.
|
|
33
|
+
(default: :obj:`None`)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
timeout: Optional[float] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
r"""Initializes the OrigeneToolkit.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
timeout (Optional[float]): Connection timeout in seconds.
|
|
44
|
+
(default: :obj:`None`)
|
|
45
|
+
"""
|
|
46
|
+
super().__init__(timeout=timeout)
|
|
47
|
+
|
|
48
|
+
self._mcp_toolkit = MCPToolkit(
|
|
49
|
+
config_dict={
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"pubchem_mcp": {
|
|
52
|
+
"url": "http://127.0.0.1:8791/mcp/",
|
|
53
|
+
"mode": "streamable-http",
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
timeout=timeout,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
async def connect(self):
|
|
61
|
+
r"""Explicitly connect to the Origene MCP server."""
|
|
62
|
+
await self._mcp_toolkit.connect()
|
|
63
|
+
|
|
64
|
+
async def disconnect(self):
|
|
65
|
+
r"""Explicitly disconnect from the Origene MCP server."""
|
|
66
|
+
await self._mcp_toolkit.disconnect()
|
|
67
|
+
|
|
68
|
+
async def __aenter__(self) -> "OrigeneToolkit":
|
|
69
|
+
r"""Async context manager entry point.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
OrigeneToolkit: The connected toolkit instance.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
async with OrigeneToolkit() as toolkit:
|
|
76
|
+
tools = toolkit.get_tools()
|
|
77
|
+
"""
|
|
78
|
+
await self.connect()
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
82
|
+
r"""Async context manager exit point.
|
|
83
|
+
|
|
84
|
+
Automatically disconnects from the Origene MCP server.
|
|
85
|
+
"""
|
|
86
|
+
await self.disconnect()
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def get_tools(self) -> List[FunctionTool]:
|
|
90
|
+
r"""Returns a list of tools provided by the Origene MCP server.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
List[FunctionTool]: List of available tools.
|
|
94
|
+
"""
|
|
95
|
+
return self._mcp_toolkit.get_tools()
|