camel-ai 0.2.70__py3-none-any.whl → 0.2.71a2__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 +61 -3
- camel/messages/func_message.py +32 -5
- camel/societies/workforce/role_playing_worker.py +4 -4
- camel/societies/workforce/single_agent_worker.py +5 -9
- camel/societies/workforce/workforce.py +304 -49
- camel/societies/workforce/workforce_logger.py +0 -1
- camel/tasks/task.py +83 -7
- camel/toolkits/craw4ai_toolkit.py +27 -7
- camel/toolkits/file_write_toolkit.py +110 -31
- camel/toolkits/human_toolkit.py +29 -9
- camel/toolkits/jina_reranker_toolkit.py +3 -4
- camel/toolkits/non_visual_browser_toolkit/browser_non_visual_toolkit.py +23 -2
- camel/toolkits/non_visual_browser_toolkit/nv_browser_session.py +53 -11
- camel/toolkits/non_visual_browser_toolkit/snapshot.js +211 -131
- camel/toolkits/non_visual_browser_toolkit/snapshot.py +9 -8
- camel/toolkits/terminal_toolkit.py +206 -64
- camel/toolkits/video_download_toolkit.py +6 -3
- camel/utils/message_summarizer.py +148 -0
- {camel_ai-0.2.70.dist-info → camel_ai-0.2.71a2.dist-info}/METADATA +4 -4
- {camel_ai-0.2.70.dist-info → camel_ai-0.2.71a2.dist-info}/RECORD +23 -22
- {camel_ai-0.2.70.dist-info → camel_ai-0.2.71a2.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.70.dist-info → camel_ai-0.2.71a2.dist-info}/licenses/LICENSE +0 -0
|
@@ -31,6 +31,16 @@ class Crawl4AIToolkit(BaseToolkit):
|
|
|
31
31
|
timeout: Optional[float] = None,
|
|
32
32
|
):
|
|
33
33
|
super().__init__(timeout=timeout)
|
|
34
|
+
self._client = None
|
|
35
|
+
|
|
36
|
+
async def _get_client(self):
|
|
37
|
+
r"""Get or create the AsyncWebCrawler client."""
|
|
38
|
+
if self._client is None:
|
|
39
|
+
from crawl4ai import AsyncWebCrawler
|
|
40
|
+
|
|
41
|
+
self._client = AsyncWebCrawler()
|
|
42
|
+
await self._client.__aenter__()
|
|
43
|
+
return self._client
|
|
34
44
|
|
|
35
45
|
async def scrape(self, url: str) -> str:
|
|
36
46
|
r"""Scrapes a webpage and returns its content.
|
|
@@ -47,19 +57,29 @@ class Crawl4AIToolkit(BaseToolkit):
|
|
|
47
57
|
str: The scraped content of the webpage as a string. If the
|
|
48
58
|
scraping fails, it will return an error message.
|
|
49
59
|
"""
|
|
50
|
-
from crawl4ai import
|
|
60
|
+
from crawl4ai import CrawlerRunConfig
|
|
51
61
|
|
|
52
62
|
try:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
client = await self._get_client()
|
|
64
|
+
config = CrawlerRunConfig(
|
|
65
|
+
only_text=True,
|
|
66
|
+
)
|
|
67
|
+
content = await client.arun(url, crawler_config=config)
|
|
68
|
+
return str(content.markdown) if content.markdown else ""
|
|
59
69
|
except Exception as e:
|
|
60
70
|
logger.error(f"Error scraping {url}: {e}")
|
|
61
71
|
return f"Error scraping {url}: {e}"
|
|
62
72
|
|
|
73
|
+
async def __aenter__(self):
|
|
74
|
+
"""Async context manager entry."""
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
78
|
+
"""Async context manager exit - cleanup the client."""
|
|
79
|
+
if self._client is not None:
|
|
80
|
+
await self._client.__aexit__(exc_type, exc_val, exc_tb)
|
|
81
|
+
self._client = None
|
|
82
|
+
|
|
63
83
|
def get_tools(self) -> List[FunctionTool]:
|
|
64
84
|
r"""Returns a list of FunctionTool objects representing the
|
|
65
85
|
functions in the toolkit.
|
|
@@ -146,23 +146,25 @@ class FileWriteToolkit(BaseToolkit):
|
|
|
146
146
|
document.save(str(file_path))
|
|
147
147
|
logger.debug(f"Wrote DOCX to {file_path} with default formatting")
|
|
148
148
|
|
|
149
|
-
@dependencies_required('pylatex', '
|
|
149
|
+
@dependencies_required('pylatex', 'pymupdf')
|
|
150
150
|
def _write_pdf_file(
|
|
151
|
-
self,
|
|
151
|
+
self,
|
|
152
|
+
file_path: Path,
|
|
153
|
+
title: str,
|
|
154
|
+
content: str,
|
|
155
|
+
use_latex: bool = False,
|
|
152
156
|
) -> None:
|
|
153
157
|
r"""Write text content to a PDF file with default formatting.
|
|
154
158
|
|
|
155
159
|
Args:
|
|
156
160
|
file_path (Path): The target file path.
|
|
161
|
+
title (str): The title of the document.
|
|
157
162
|
content (str): The text content to write.
|
|
158
163
|
use_latex (bool): Whether to use LaTeX for rendering. (requires
|
|
159
|
-
LaTeX toolchain). If False, uses
|
|
164
|
+
LaTeX toolchain). If False, uses PyMuPDF for simpler PDF
|
|
160
165
|
generation. (default: :obj:`False`)
|
|
161
|
-
|
|
162
|
-
Raises:
|
|
163
|
-
RuntimeError: If the 'pylatex' or 'fpdf' library is not installed
|
|
164
|
-
when use_latex=True.
|
|
165
166
|
"""
|
|
167
|
+
# TODO: table generation need to be improved
|
|
166
168
|
if use_latex:
|
|
167
169
|
from pylatex import (
|
|
168
170
|
Command,
|
|
@@ -213,30 +215,105 @@ class FileWriteToolkit(BaseToolkit):
|
|
|
213
215
|
|
|
214
216
|
logger.info(f"Wrote PDF (with LaTeX) to {file_path}")
|
|
215
217
|
else:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
#
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
#
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
218
|
+
import pymupdf
|
|
219
|
+
|
|
220
|
+
# Create a new PDF document
|
|
221
|
+
doc = pymupdf.open()
|
|
222
|
+
|
|
223
|
+
# Add a page
|
|
224
|
+
page = doc.new_page()
|
|
225
|
+
|
|
226
|
+
# Process the content
|
|
227
|
+
lines = content.strip().split('\n')
|
|
228
|
+
document_title = title
|
|
229
|
+
|
|
230
|
+
# Create a TextWriter for writing text to the page
|
|
231
|
+
text_writer = pymupdf.TextWriter(page.rect)
|
|
232
|
+
|
|
233
|
+
# Define fonts
|
|
234
|
+
normal_font = pymupdf.Font(
|
|
235
|
+
"helv"
|
|
236
|
+
) # Standard font with multilingual support
|
|
237
|
+
bold_font = pymupdf.Font("helv")
|
|
238
|
+
|
|
239
|
+
# Start position for text
|
|
240
|
+
y_pos = 50
|
|
241
|
+
x_pos = 50
|
|
242
|
+
|
|
243
|
+
# Add title
|
|
244
|
+
text_writer.fill_textbox(
|
|
245
|
+
pymupdf.Rect(
|
|
246
|
+
x_pos, y_pos, page.rect.width - x_pos, y_pos + 30
|
|
247
|
+
),
|
|
248
|
+
document_title,
|
|
249
|
+
fontsize=16,
|
|
250
|
+
)
|
|
251
|
+
y_pos += 40
|
|
252
|
+
|
|
253
|
+
# Process content
|
|
254
|
+
for line in lines:
|
|
255
|
+
stripped_line = line.strip()
|
|
256
|
+
|
|
257
|
+
# Skip empty lines but add some space
|
|
258
|
+
if not stripped_line:
|
|
259
|
+
y_pos += 10
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
# Handle headers
|
|
263
|
+
if stripped_line.startswith('## '):
|
|
264
|
+
text_writer.fill_textbox(
|
|
265
|
+
pymupdf.Rect(
|
|
266
|
+
x_pos, y_pos, page.rect.width - x_pos, y_pos + 20
|
|
267
|
+
),
|
|
268
|
+
stripped_line[3:].strip(),
|
|
269
|
+
font=bold_font,
|
|
270
|
+
fontsize=14,
|
|
271
|
+
)
|
|
272
|
+
y_pos += 25
|
|
273
|
+
elif stripped_line.startswith('# '):
|
|
274
|
+
text_writer.fill_textbox(
|
|
275
|
+
pymupdf.Rect(
|
|
276
|
+
x_pos, y_pos, page.rect.width - x_pos, y_pos + 25
|
|
277
|
+
),
|
|
278
|
+
stripped_line[2:].strip(),
|
|
279
|
+
font=bold_font,
|
|
280
|
+
fontsize=16,
|
|
281
|
+
)
|
|
282
|
+
y_pos += 30
|
|
283
|
+
# Handle horizontal rule
|
|
284
|
+
elif stripped_line == '---':
|
|
285
|
+
page.draw_line(
|
|
286
|
+
pymupdf.Point(x_pos, y_pos + 5),
|
|
287
|
+
pymupdf.Point(page.rect.width - x_pos, y_pos + 5),
|
|
288
|
+
)
|
|
289
|
+
y_pos += 15
|
|
290
|
+
# Regular text
|
|
235
291
|
else:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
292
|
+
# Check if we need a new page
|
|
293
|
+
if y_pos > page.rect.height - 50:
|
|
294
|
+
text_writer.write_text(page)
|
|
295
|
+
page = doc.new_page()
|
|
296
|
+
text_writer = pymupdf.TextWriter(page.rect)
|
|
297
|
+
y_pos = 50
|
|
298
|
+
|
|
299
|
+
# Add text to the current page
|
|
300
|
+
text_writer.fill_textbox(
|
|
301
|
+
pymupdf.Rect(
|
|
302
|
+
x_pos, y_pos, page.rect.width - x_pos, y_pos + 15
|
|
303
|
+
),
|
|
304
|
+
stripped_line,
|
|
305
|
+
font=normal_font,
|
|
306
|
+
)
|
|
307
|
+
y_pos += 15
|
|
308
|
+
|
|
309
|
+
# Write the accumulated text to the last page
|
|
310
|
+
text_writer.write_text(page)
|
|
311
|
+
|
|
312
|
+
# Save the PDF
|
|
313
|
+
doc.save(str(file_path))
|
|
314
|
+
doc.close()
|
|
315
|
+
|
|
316
|
+
logger.debug(f"Wrote PDF to {file_path} with PyMuPDF formatting")
|
|
240
317
|
|
|
241
318
|
def _write_csv_file(
|
|
242
319
|
self,
|
|
@@ -338,6 +415,7 @@ class FileWriteToolkit(BaseToolkit):
|
|
|
338
415
|
|
|
339
416
|
def write_to_file(
|
|
340
417
|
self,
|
|
418
|
+
title: str,
|
|
341
419
|
content: Union[str, List[List[str]]],
|
|
342
420
|
filename: str,
|
|
343
421
|
encoding: Optional[str] = None,
|
|
@@ -351,6 +429,7 @@ class FileWriteToolkit(BaseToolkit):
|
|
|
351
429
|
and HTML (.html, .htm).
|
|
352
430
|
|
|
353
431
|
Args:
|
|
432
|
+
title (str): The title of the document.
|
|
354
433
|
content (Union[str, List[List[str]]]): The content to write to the
|
|
355
434
|
file. Content format varies by file type:
|
|
356
435
|
- Text formats (txt, md, html, yaml): string
|
|
@@ -388,7 +467,7 @@ class FileWriteToolkit(BaseToolkit):
|
|
|
388
467
|
self._write_docx_file(file_path, str(content))
|
|
389
468
|
elif extension == ".pdf":
|
|
390
469
|
self._write_pdf_file(
|
|
391
|
-
file_path, str(content), use_latex=use_latex
|
|
470
|
+
file_path, title, str(content), use_latex=use_latex
|
|
392
471
|
)
|
|
393
472
|
elif extension == ".csv":
|
|
394
473
|
self._write_csv_file(
|
camel/toolkits/human_toolkit.py
CHANGED
|
@@ -22,16 +22,29 @@ logger = logging.getLogger(__name__)
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class HumanToolkit(BaseToolkit):
|
|
25
|
-
r"""A class representing a toolkit for human interaction.
|
|
25
|
+
r"""A class representing a toolkit for human interaction.
|
|
26
|
+
|
|
27
|
+
Note:
|
|
28
|
+
This toolkit should be called to send a tidy message to the user to
|
|
29
|
+
keep them informed.
|
|
30
|
+
"""
|
|
26
31
|
|
|
27
32
|
def ask_human_via_console(self, question: str) -> str:
|
|
28
|
-
r"""
|
|
33
|
+
r"""Use this tool to ask a question to the user when you are stuck,
|
|
34
|
+
need clarification, or require a decision to be made. This is a
|
|
35
|
+
two-way communication channel that will wait for the user's response.
|
|
36
|
+
You should use it to:
|
|
37
|
+
- Clarify ambiguous instructions or requirements.
|
|
38
|
+
- Request missing information that you cannot find (e.g., login
|
|
39
|
+
credentials, file paths).
|
|
40
|
+
- Ask for a decision when there are multiple viable options.
|
|
41
|
+
- Seek help when you encounter an error you cannot resolve on your own.
|
|
29
42
|
|
|
30
43
|
Args:
|
|
31
|
-
question (str): The question to ask the
|
|
44
|
+
question (str): The question to ask the user.
|
|
32
45
|
|
|
33
46
|
Returns:
|
|
34
|
-
str: The
|
|
47
|
+
str: The user's response to the question.
|
|
35
48
|
"""
|
|
36
49
|
print(f"Question: {question}")
|
|
37
50
|
logger.info(f"Question: {question}")
|
|
@@ -40,14 +53,21 @@ class HumanToolkit(BaseToolkit):
|
|
|
40
53
|
return reply
|
|
41
54
|
|
|
42
55
|
def send_message_to_user(self, message: str) -> None:
|
|
43
|
-
r"""
|
|
44
|
-
|
|
56
|
+
r"""Use this tool to send a tidy message to the user in one short
|
|
57
|
+
sentence.
|
|
45
58
|
|
|
46
|
-
This
|
|
47
|
-
|
|
59
|
+
This one-way tool keeps the user informed about your progress,
|
|
60
|
+
decisions, or actions. It does not require a response.
|
|
61
|
+
You should use it to:
|
|
62
|
+
- Announce what you are about to do (e.g., "I will now search for
|
|
63
|
+
papers on GUI Agents.").
|
|
64
|
+
- Report the result of an action (e.g., "I have found 15 relevant
|
|
65
|
+
papers.").
|
|
66
|
+
- State a decision (e.g., "I will now analyze the top 10 papers.").
|
|
67
|
+
- Give a status update during a long-running task.
|
|
48
68
|
|
|
49
69
|
Args:
|
|
50
|
-
message (str): The
|
|
70
|
+
message (str): The tidy and informative message for the user.
|
|
51
71
|
"""
|
|
52
72
|
print(f"\nAgent Message:\n{message}")
|
|
53
73
|
logger.info(f"\nAgent Message:\n{message}")
|
|
@@ -34,7 +34,7 @@ class JinaRerankerToolkit(BaseToolkit):
|
|
|
34
34
|
def __init__(
|
|
35
35
|
self,
|
|
36
36
|
timeout: Optional[float] = None,
|
|
37
|
-
model_name:
|
|
37
|
+
model_name: str = "jinaai/jina-reranker-m0",
|
|
38
38
|
device: Optional[str] = None,
|
|
39
39
|
use_api: bool = True,
|
|
40
40
|
) -> None:
|
|
@@ -44,9 +44,8 @@ class JinaRerankerToolkit(BaseToolkit):
|
|
|
44
44
|
timeout (Optional[float]): The timeout value for API requests
|
|
45
45
|
in seconds. If None, no timeout is applied.
|
|
46
46
|
(default: :obj:`None`)
|
|
47
|
-
model_name (
|
|
48
|
-
|
|
49
|
-
(default: :obj:`None`)
|
|
47
|
+
model_name (str): The reranker model name.
|
|
48
|
+
(default: :obj:`"jinaai/jina-reranker-m0"`)
|
|
50
49
|
device (Optional[str]): Device to load the model on. If None,
|
|
51
50
|
will use CUDA if available, otherwise CPU.
|
|
52
51
|
Only effective when use_api=False.
|
|
@@ -80,7 +80,28 @@ class BrowserNonVisualToolkit(BaseToolkit):
|
|
|
80
80
|
return
|
|
81
81
|
|
|
82
82
|
if loop.is_closed():
|
|
83
|
-
#
|
|
83
|
+
# The default loop is closed, create a *temporary* loop just
|
|
84
|
+
# for cleanup so that Playwright / asyncio transports are
|
|
85
|
+
# gracefully shut down. This avoids noisy warnings such as
|
|
86
|
+
# "RuntimeError: Event loop is closed" when the program
|
|
87
|
+
# exits.
|
|
88
|
+
try:
|
|
89
|
+
tmp_loop = asyncio.new_event_loop()
|
|
90
|
+
try:
|
|
91
|
+
asyncio.set_event_loop(tmp_loop)
|
|
92
|
+
tmp_loop.run_until_complete(self.close_browser())
|
|
93
|
+
finally:
|
|
94
|
+
# Best-effort shutdown of async generators and loop
|
|
95
|
+
# itself (Python ≥3.6).
|
|
96
|
+
if hasattr(tmp_loop, "shutdown_asyncgens"):
|
|
97
|
+
tmp_loop.run_until_complete(
|
|
98
|
+
tmp_loop.shutdown_asyncgens()
|
|
99
|
+
)
|
|
100
|
+
tmp_loop.close()
|
|
101
|
+
finally:
|
|
102
|
+
# Ensure no subsequent get_event_loop() picks up a now
|
|
103
|
+
# closed temporary loop.
|
|
104
|
+
asyncio.set_event_loop(None)
|
|
84
105
|
return
|
|
85
106
|
|
|
86
107
|
if loop.is_running():
|
|
@@ -155,7 +176,7 @@ class BrowserNonVisualToolkit(BaseToolkit):
|
|
|
155
176
|
self._agent = None
|
|
156
177
|
|
|
157
178
|
# Close session
|
|
158
|
-
await
|
|
179
|
+
await NVBrowserSession.close_all_sessions()
|
|
159
180
|
return "Browser session closed."
|
|
160
181
|
|
|
161
182
|
async def visit_page(self, url: str) -> Dict[str, str]:
|
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import asyncio
|
|
16
17
|
from pathlib import Path
|
|
17
|
-
from typing import TYPE_CHECKING, Any, Optional
|
|
18
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional
|
|
19
|
+
|
|
20
|
+
from camel.logger import get_logger
|
|
18
21
|
|
|
19
22
|
from .actions import ActionExecutor
|
|
20
23
|
from .snapshot import PageSnapshot
|
|
@@ -28,6 +31,9 @@ if TYPE_CHECKING:
|
|
|
28
31
|
)
|
|
29
32
|
|
|
30
33
|
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
31
37
|
class NVBrowserSession:
|
|
32
38
|
"""Lightweight wrapper around Playwright for non-visual (headless)
|
|
33
39
|
browsing.
|
|
@@ -35,15 +41,37 @@ class NVBrowserSession:
|
|
|
35
41
|
It provides a single *Page* instance plus helper utilities (snapshot &
|
|
36
42
|
executor). Multiple toolkits or agents can reuse this class without
|
|
37
43
|
duplicating Playwright setup code.
|
|
44
|
+
|
|
45
|
+
This class is a singleton per event-loop.
|
|
38
46
|
"""
|
|
39
47
|
|
|
40
48
|
# Configuration constants
|
|
41
49
|
DEFAULT_NAVIGATION_TIMEOUT = 10000 # 10 seconds
|
|
42
50
|
NETWORK_IDLE_TIMEOUT = 5000 # 5 seconds
|
|
43
51
|
|
|
52
|
+
_sessions: ClassVar[
|
|
53
|
+
Dict[asyncio.AbstractEventLoop, "NVBrowserSession"]
|
|
54
|
+
] = {}
|
|
55
|
+
|
|
56
|
+
_initialized: bool
|
|
57
|
+
|
|
58
|
+
def __new__(
|
|
59
|
+
cls, *, headless: bool = True, user_data_dir: Optional[str] = None
|
|
60
|
+
):
|
|
61
|
+
loop = asyncio.get_running_loop()
|
|
62
|
+
if loop not in cls._sessions:
|
|
63
|
+
instance = super().__new__(cls)
|
|
64
|
+
instance._initialized = False
|
|
65
|
+
cls._sessions[loop] = instance
|
|
66
|
+
return cls._sessions[loop]
|
|
67
|
+
|
|
44
68
|
def __init__(
|
|
45
69
|
self, *, headless: bool = True, user_data_dir: Optional[str] = None
|
|
46
70
|
):
|
|
71
|
+
if self._initialized:
|
|
72
|
+
return
|
|
73
|
+
self._initialized = True
|
|
74
|
+
|
|
47
75
|
self._headless = headless
|
|
48
76
|
self._user_data_dir = user_data_dir
|
|
49
77
|
|
|
@@ -56,8 +84,6 @@ class NVBrowserSession:
|
|
|
56
84
|
self.executor: Optional[ActionExecutor] = None
|
|
57
85
|
|
|
58
86
|
# Protect browser initialisation against concurrent calls
|
|
59
|
-
import asyncio
|
|
60
|
-
|
|
61
87
|
self._ensure_lock: "asyncio.Lock" = asyncio.Lock()
|
|
62
88
|
|
|
63
89
|
# ------------------------------------------------------------------
|
|
@@ -93,10 +119,6 @@ class NVBrowserSession:
|
|
|
93
119
|
self._browser = await pl.chromium.launch(headless=self._headless)
|
|
94
120
|
self._context = await self._browser.new_context()
|
|
95
121
|
|
|
96
|
-
from camel.logger import get_logger
|
|
97
|
-
|
|
98
|
-
_dbg_logger = get_logger(__name__)
|
|
99
|
-
|
|
100
122
|
# Reuse an already open page (persistent context may restore last
|
|
101
123
|
# session)
|
|
102
124
|
if self._context.pages:
|
|
@@ -105,7 +127,7 @@ class NVBrowserSession:
|
|
|
105
127
|
self._page = await self._context.new_page()
|
|
106
128
|
|
|
107
129
|
# Debug information to help trace concurrency issues
|
|
108
|
-
|
|
130
|
+
logger.debug(
|
|
109
131
|
"Session %s created browser=%s context=%s page=%s (url=%s)",
|
|
110
132
|
hex(id(self)),
|
|
111
133
|
hex(id(self._browser)) if self._browser else None,
|
|
@@ -122,6 +144,11 @@ class NVBrowserSession:
|
|
|
122
144
|
r"""Close all browser resources, ensuring cleanup even if some
|
|
123
145
|
operations fail.
|
|
124
146
|
"""
|
|
147
|
+
# The close method will now only close the *current* event-loop's
|
|
148
|
+
# browser instance. Use `close_all_sessions` for a full cleanup.
|
|
149
|
+
await self._close_session()
|
|
150
|
+
|
|
151
|
+
async def _close_session(self) -> None:
|
|
125
152
|
errors: list[str] = []
|
|
126
153
|
|
|
127
154
|
# Close context first (which closes pages)
|
|
@@ -151,13 +178,28 @@ class NVBrowserSession:
|
|
|
151
178
|
|
|
152
179
|
# Log errors if any occurred during cleanup
|
|
153
180
|
if errors:
|
|
154
|
-
from camel.logger import get_logger
|
|
155
|
-
|
|
156
|
-
logger = get_logger(__name__)
|
|
157
181
|
logger.warning(
|
|
158
182
|
"Errors during browser session cleanup: %s", "; ".join(errors)
|
|
159
183
|
)
|
|
160
184
|
|
|
185
|
+
@classmethod
|
|
186
|
+
async def close_all_sessions(cls) -> None:
|
|
187
|
+
r"""Iterate over all stored sessions and close them."""
|
|
188
|
+
for loop, session in cls._sessions.items():
|
|
189
|
+
if loop.is_running():
|
|
190
|
+
await session._close_session()
|
|
191
|
+
else:
|
|
192
|
+
try:
|
|
193
|
+
if not loop.is_closed():
|
|
194
|
+
loop.run_until_complete(session._close_session())
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.warning(
|
|
197
|
+
"Failed to close session for loop %s: %s",
|
|
198
|
+
hex(id(loop)),
|
|
199
|
+
e,
|
|
200
|
+
)
|
|
201
|
+
cls._sessions.clear()
|
|
202
|
+
|
|
161
203
|
# ------------------------------------------------------------------
|
|
162
204
|
# Convenience wrappers around common actions
|
|
163
205
|
# ------------------------------------------------------------------
|