camel-ai 0.2.71a7__py3-none-any.whl → 0.2.71a9__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/societies/workforce/single_agent_worker.py +53 -9
- camel/societies/workforce/task_channel.py +4 -1
- camel/societies/workforce/workforce.py +146 -14
- camel/tasks/task.py +104 -4
- camel/toolkits/file_write_toolkit.py +19 -8
- camel/toolkits/hybrid_browser_toolkit/actions.py +28 -18
- camel/toolkits/hybrid_browser_toolkit/agent.py +7 -1
- camel/toolkits/hybrid_browser_toolkit/browser_session.py +48 -18
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +447 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +272 -85
- camel/toolkits/hybrid_browser_toolkit/snapshot.py +5 -4
- camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +572 -17
- camel/toolkits/note_taking_toolkit.py +24 -9
- camel/toolkits/pptx_toolkit.py +21 -8
- camel/toolkits/search_toolkit.py +15 -5
- {camel_ai-0.2.71a7.dist-info → camel_ai-0.2.71a9.dist-info}/METADATA +1 -1
- {camel_ai-0.2.71a7.dist-info → camel_ai-0.2.71a9.dist-info}/RECORD +20 -20
- camel/toolkits/hybrid_browser_toolkit/stealth_config.py +0 -116
- {camel_ai-0.2.71a7.dist-info → camel_ai-0.2.71a9.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.71a7.dist-info → camel_ai-0.2.71a9.dist-info}/licenses/LICENSE +0 -0
|
@@ -32,6 +32,7 @@ from camel.utils.tool_result import ToolResult
|
|
|
32
32
|
|
|
33
33
|
from .agent import PlaywrightLLMAgent
|
|
34
34
|
from .browser_session import HybridBrowserSession
|
|
35
|
+
from .config_loader import ConfigLoader
|
|
35
36
|
|
|
36
37
|
logger = get_logger(__name__)
|
|
37
38
|
|
|
@@ -47,11 +48,6 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
47
48
|
interactive elements.
|
|
48
49
|
"""
|
|
49
50
|
|
|
50
|
-
# Configuration constants
|
|
51
|
-
DEFAULT_SCREENSHOT_TIMEOUT = 60000 # 60 seconds for screenshots
|
|
52
|
-
PAGE_STABILITY_TIMEOUT = 3000 # 3 seconds for DOM stability
|
|
53
|
-
NETWORK_IDLE_TIMEOUT = 2000 # 2 seconds for network idle
|
|
54
|
-
|
|
55
51
|
# Default tool list - core browser functionality
|
|
56
52
|
DEFAULT_TOOLS: ClassVar[List[str]] = [
|
|
57
53
|
"open_browser",
|
|
@@ -98,6 +94,13 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
98
94
|
browser_log_to_file: bool = False,
|
|
99
95
|
session_id: Optional[str] = None,
|
|
100
96
|
default_start_url: str = "https://google.com/",
|
|
97
|
+
default_timeout: Optional[int] = None,
|
|
98
|
+
short_timeout: Optional[int] = None,
|
|
99
|
+
navigation_timeout: Optional[int] = None,
|
|
100
|
+
network_idle_timeout: Optional[int] = None,
|
|
101
|
+
screenshot_timeout: Optional[int] = None,
|
|
102
|
+
page_stability_timeout: Optional[int] = None,
|
|
103
|
+
dom_content_loaded_timeout: Optional[int] = None,
|
|
101
104
|
) -> None:
|
|
102
105
|
r"""Initialize the HybridBrowserToolkit.
|
|
103
106
|
|
|
@@ -140,14 +143,71 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
140
143
|
default_start_url (str): The default URL to navigate to when
|
|
141
144
|
open_browser() is called without a start_url parameter or with
|
|
142
145
|
None. Defaults to `"https://google.com/"`.
|
|
146
|
+
default_timeout (Optional[int]): Default timeout in milliseconds
|
|
147
|
+
for browser actions. If None, uses environment variable
|
|
148
|
+
HYBRID_BROWSER_DEFAULT_TIMEOUT or defaults to 3000ms.
|
|
149
|
+
Defaults to `None`.
|
|
150
|
+
short_timeout (Optional[int]): Short timeout in milliseconds
|
|
151
|
+
for quick browser actions. If None, uses environment variable
|
|
152
|
+
HYBRID_BROWSER_SHORT_TIMEOUT or defaults to 1000ms.
|
|
153
|
+
Defaults to `None`.
|
|
154
|
+
navigation_timeout (Optional[int]): Custom navigation timeout in
|
|
155
|
+
milliseconds.
|
|
156
|
+
If None, uses environment variable
|
|
157
|
+
HYBRID_BROWSER_NAVIGATION_TIMEOUT or defaults to 10000ms.
|
|
158
|
+
Defaults to `None`.
|
|
159
|
+
network_idle_timeout (Optional[int]): Custom network idle
|
|
160
|
+
timeout in milliseconds.
|
|
161
|
+
If None, uses environment variable
|
|
162
|
+
HYBRID_BROWSER_NETWORK_IDLE_TIMEOUT or defaults to 5000ms.
|
|
163
|
+
Defaults to `None`.
|
|
164
|
+
screenshot_timeout (Optional[int]): Custom screenshot timeout in
|
|
165
|
+
milliseconds.
|
|
166
|
+
If None, uses environment variable
|
|
167
|
+
HYBRID_BROWSER_SCREENSHOT_TIMEOUT or defaults to 15000ms.
|
|
168
|
+
Defaults to `None`.
|
|
169
|
+
page_stability_timeout (Optional[int]): Custom page stability
|
|
170
|
+
timeout in milliseconds.
|
|
171
|
+
If None, uses environment variable
|
|
172
|
+
HYBRID_BROWSER_PAGE_STABILITY_TIMEOUT or defaults to 1500ms.
|
|
173
|
+
Defaults to `None`.
|
|
174
|
+
dom_content_loaded_timeout (Optional[int]): Custom DOM content
|
|
175
|
+
loaded timeout in milliseconds.
|
|
176
|
+
If None, uses environment variable
|
|
177
|
+
HYBRID_BROWSER_DOM_CONTENT_LOADED_TIMEOUT or defaults to
|
|
178
|
+
5000ms.
|
|
179
|
+
Defaults to `None`.
|
|
143
180
|
"""
|
|
144
181
|
super().__init__()
|
|
145
182
|
self._headless = headless
|
|
146
183
|
self._user_data_dir = user_data_dir
|
|
147
|
-
self.
|
|
148
|
-
self.
|
|
149
|
-
self.
|
|
150
|
-
|
|
184
|
+
self._stealth = stealth
|
|
185
|
+
self._web_agent_model = web_agent_model
|
|
186
|
+
self._cache_dir = cache_dir
|
|
187
|
+
self._browser_log_to_file = browser_log_to_file
|
|
188
|
+
self._default_start_url = default_start_url
|
|
189
|
+
self._session_id = session_id or "default"
|
|
190
|
+
|
|
191
|
+
# Store timeout configuration
|
|
192
|
+
self._default_timeout = default_timeout
|
|
193
|
+
self._short_timeout = short_timeout
|
|
194
|
+
self._navigation_timeout = ConfigLoader.get_navigation_timeout(
|
|
195
|
+
navigation_timeout
|
|
196
|
+
)
|
|
197
|
+
self._network_idle_timeout = ConfigLoader.get_network_idle_timeout(
|
|
198
|
+
network_idle_timeout
|
|
199
|
+
)
|
|
200
|
+
self._screenshot_timeout = ConfigLoader.get_screenshot_timeout(
|
|
201
|
+
screenshot_timeout
|
|
202
|
+
)
|
|
203
|
+
self._page_stability_timeout = ConfigLoader.get_page_stability_timeout(
|
|
204
|
+
page_stability_timeout
|
|
205
|
+
)
|
|
206
|
+
self._dom_content_loaded_timeout = (
|
|
207
|
+
ConfigLoader.get_dom_content_loaded_timeout(
|
|
208
|
+
dom_content_loaded_timeout
|
|
209
|
+
)
|
|
210
|
+
)
|
|
151
211
|
|
|
152
212
|
# Logging configuration - fixed values for simplicity
|
|
153
213
|
self.enable_action_logging = True
|
|
@@ -203,6 +263,8 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
203
263
|
user_data_dir=user_data_dir,
|
|
204
264
|
stealth=stealth,
|
|
205
265
|
session_id=session_id,
|
|
266
|
+
default_timeout=default_timeout,
|
|
267
|
+
short_timeout=short_timeout,
|
|
206
268
|
)
|
|
207
269
|
# Use the session directly - singleton logic is handled in
|
|
208
270
|
# ensure_browser
|
|
@@ -210,6 +272,21 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
210
272
|
self._agent: Optional[PlaywrightLLMAgent] = None
|
|
211
273
|
self._unified_script = self._load_unified_analyzer()
|
|
212
274
|
|
|
275
|
+
@property
|
|
276
|
+
def web_agent_model(self) -> Optional[BaseModelBackend]:
|
|
277
|
+
"""Get the web agent model."""
|
|
278
|
+
return self._web_agent_model
|
|
279
|
+
|
|
280
|
+
@web_agent_model.setter
|
|
281
|
+
def web_agent_model(self, value: Optional[BaseModelBackend]) -> None:
|
|
282
|
+
"""Set the web agent model."""
|
|
283
|
+
self._web_agent_model = value
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def cache_dir(self) -> str:
|
|
287
|
+
"""Get the cache directory."""
|
|
288
|
+
return self._cache_dir
|
|
289
|
+
|
|
213
290
|
def __del__(self):
|
|
214
291
|
r"""Cleanup browser resources on garbage collection."""
|
|
215
292
|
try:
|
|
@@ -437,29 +514,29 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
437
514
|
|
|
438
515
|
async def _wait_for_page_stability(self):
|
|
439
516
|
r"""Wait for page to become stable after actions that might trigger
|
|
440
|
-
updates.
|
|
517
|
+
updates. Optimized with shorter timeouts.
|
|
441
518
|
"""
|
|
442
519
|
page = await self._require_page()
|
|
443
520
|
import asyncio
|
|
444
521
|
|
|
445
522
|
try:
|
|
446
|
-
# Wait for DOM content to be loaded
|
|
523
|
+
# Wait for DOM content to be loaded (reduced timeout)
|
|
447
524
|
await page.wait_for_load_state(
|
|
448
|
-
'domcontentloaded', timeout=self.
|
|
525
|
+
'domcontentloaded', timeout=self._page_stability_timeout
|
|
449
526
|
)
|
|
450
527
|
logger.debug("DOM content loaded")
|
|
451
528
|
|
|
452
|
-
# Try to wait for network idle
|
|
529
|
+
# Try to wait for network idle with shorter timeout
|
|
453
530
|
try:
|
|
454
531
|
await page.wait_for_load_state(
|
|
455
|
-
'networkidle', timeout=self.
|
|
532
|
+
'networkidle', timeout=self._network_idle_timeout
|
|
456
533
|
)
|
|
457
534
|
logger.debug("Network idle achieved")
|
|
458
535
|
except Exception:
|
|
459
536
|
logger.debug("Network idle timeout - continuing anyway")
|
|
460
537
|
|
|
461
|
-
#
|
|
462
|
-
await asyncio.sleep(0.
|
|
538
|
+
# Reduced delay for JavaScript execution
|
|
539
|
+
await asyncio.sleep(0.2) # Reduced from 0.5s
|
|
463
540
|
logger.debug("Page stability wait completed")
|
|
464
541
|
|
|
465
542
|
except Exception as e:
|
|
@@ -467,24 +544,92 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
467
544
|
f"Page stability wait failed: {e} - continuing anyway"
|
|
468
545
|
)
|
|
469
546
|
|
|
470
|
-
async def _get_unified_analysis(
|
|
471
|
-
|
|
547
|
+
async def _get_unified_analysis(
|
|
548
|
+
self, max_retries: int = 3
|
|
549
|
+
) -> Dict[str, Any]:
|
|
550
|
+
r"""Get unified analysis data from the page with retry mechanism for
|
|
551
|
+
navigation issues."""
|
|
472
552
|
page = await self._require_page()
|
|
473
|
-
try:
|
|
474
|
-
if not self._unified_script:
|
|
475
|
-
logger.error("Unified analyzer script not loaded")
|
|
476
|
-
return {"elements": {}, "metadata": {"elementCount": 0}}
|
|
477
553
|
|
|
478
|
-
|
|
554
|
+
for attempt in range(max_retries):
|
|
555
|
+
try:
|
|
556
|
+
if not self._unified_script:
|
|
557
|
+
logger.error("Unified analyzer script not loaded")
|
|
558
|
+
return {"elements": {}, "metadata": {"elementCount": 0}}
|
|
559
|
+
|
|
560
|
+
# Wait for DOM stability before each attempt (with optimized
|
|
561
|
+
# timeout)
|
|
562
|
+
try:
|
|
563
|
+
await page.wait_for_load_state(
|
|
564
|
+
'domcontentloaded',
|
|
565
|
+
timeout=self._dom_content_loaded_timeout,
|
|
566
|
+
)
|
|
567
|
+
except Exception:
|
|
568
|
+
# Don't fail if DOM wait times out
|
|
569
|
+
pass
|
|
570
|
+
|
|
571
|
+
result = await page.evaluate(self._unified_script)
|
|
572
|
+
|
|
573
|
+
if not isinstance(result, dict):
|
|
574
|
+
logger.warning(f"Invalid result type: {type(result)}")
|
|
575
|
+
return {"elements": {}, "metadata": {"elementCount": 0}}
|
|
576
|
+
|
|
577
|
+
# Success - return result
|
|
578
|
+
if attempt > 0:
|
|
579
|
+
logger.debug(
|
|
580
|
+
f"Unified analysis succeeded on attempt {attempt + 1}"
|
|
581
|
+
)
|
|
582
|
+
return result
|
|
583
|
+
|
|
584
|
+
except Exception as e:
|
|
585
|
+
error_msg = str(e)
|
|
586
|
+
|
|
587
|
+
# Check if this is a navigation-related error
|
|
588
|
+
is_navigation_error = (
|
|
589
|
+
"Execution context was destroyed" in error_msg
|
|
590
|
+
or "Most likely because of a navigation" in error_msg
|
|
591
|
+
or "Target page, context or browser has been closed"
|
|
592
|
+
in error_msg
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
if is_navigation_error and attempt < max_retries - 1:
|
|
596
|
+
logger.debug(
|
|
597
|
+
f"Navigation error in unified analysis (attempt "
|
|
598
|
+
f"{attempt + 1}/{max_retries}): {e}. Retrying..."
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
# Wait a bit for page stability before retrying (optimized)
|
|
602
|
+
try:
|
|
603
|
+
await page.wait_for_load_state(
|
|
604
|
+
'domcontentloaded',
|
|
605
|
+
timeout=self._page_stability_timeout,
|
|
606
|
+
)
|
|
607
|
+
# Reduced delay for JS context to stabilize
|
|
608
|
+
import asyncio
|
|
609
|
+
|
|
610
|
+
await asyncio.sleep(0.1) # Reduced from 0.2s
|
|
611
|
+
except Exception:
|
|
612
|
+
# Continue even if wait fails
|
|
613
|
+
pass
|
|
614
|
+
|
|
615
|
+
continue
|
|
616
|
+
|
|
617
|
+
# Non-navigation error or final attempt - log and return
|
|
618
|
+
# empty result
|
|
619
|
+
if attempt == max_retries - 1:
|
|
620
|
+
logger.warning(
|
|
621
|
+
f"Error in unified analysis after {max_retries} "
|
|
622
|
+
f"attempts: {e}"
|
|
623
|
+
)
|
|
624
|
+
else:
|
|
625
|
+
logger.warning(
|
|
626
|
+
f"Non-retryable error in unified analysis: {e}"
|
|
627
|
+
)
|
|
479
628
|
|
|
480
|
-
if not isinstance(result, dict):
|
|
481
|
-
logger.warning(f"Invalid result type: {type(result)}")
|
|
482
629
|
return {"elements": {}, "metadata": {"elementCount": 0}}
|
|
483
630
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
logger.warning(f"Error in unified analysis: {e}")
|
|
487
|
-
return {"elements": {}, "metadata": {"elementCount": 0}}
|
|
631
|
+
# Should not reach here, but just in case
|
|
632
|
+
return {"elements": {}, "metadata": {"elementCount": 0}}
|
|
488
633
|
|
|
489
634
|
def _convert_analysis_to_rects(
|
|
490
635
|
self, analysis_data: Dict[str, Any]
|
|
@@ -728,13 +873,13 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
728
873
|
try:
|
|
729
874
|
# Get before snapshot
|
|
730
875
|
logger.info("Capturing pre-action snapshot...")
|
|
731
|
-
|
|
876
|
+
snapshot_start_before = time.time()
|
|
732
877
|
before_snapshot = await self._session.get_snapshot(
|
|
733
878
|
force_refresh=True, diff_only=False
|
|
734
879
|
)
|
|
735
|
-
|
|
880
|
+
before_snapshot_time = time.time() - snapshot_start_before
|
|
736
881
|
logger.info(
|
|
737
|
-
f"Pre-action snapshot captured in {
|
|
882
|
+
f"Pre-action snapshot captured in {before_snapshot_time:.2f}s"
|
|
738
883
|
)
|
|
739
884
|
|
|
740
885
|
# Execute action
|
|
@@ -780,13 +925,14 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
780
925
|
|
|
781
926
|
# Get after snapshot
|
|
782
927
|
logger.info("Capturing post-action snapshot...")
|
|
783
|
-
|
|
928
|
+
snapshot_start_after = time.time()
|
|
784
929
|
after_snapshot = await self._session.get_snapshot(
|
|
785
930
|
force_refresh=True, diff_only=False
|
|
786
931
|
)
|
|
787
|
-
|
|
932
|
+
after_snapshot_time = time.time() - snapshot_start_after
|
|
788
933
|
logger.info(
|
|
789
|
-
f"Post-action snapshot "
|
|
934
|
+
f"Post-action snapshot "
|
|
935
|
+
f"captured in {after_snapshot_time:.2f}s"
|
|
790
936
|
)
|
|
791
937
|
|
|
792
938
|
# Check for snapshot quality and log warnings
|
|
@@ -820,6 +966,7 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
820
966
|
|
|
821
967
|
# Create comprehensive output for logging
|
|
822
968
|
execution_time = time.time() - action_start_time
|
|
969
|
+
total_snapshot_time = before_snapshot_time + after_snapshot_time
|
|
823
970
|
outputs = {
|
|
824
971
|
"result": result_message,
|
|
825
972
|
"snapshot": snapshot,
|
|
@@ -830,6 +977,7 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
830
977
|
"stability_time_ms": round(stability_time * 1000, 2)
|
|
831
978
|
if stability_time > 0
|
|
832
979
|
else None,
|
|
980
|
+
"snapshot_time_ms": round(total_snapshot_time * 1000, 2),
|
|
833
981
|
"total_time_ms": round(execution_time * 1000, 2),
|
|
834
982
|
},
|
|
835
983
|
**tab_info, # Include tab information
|
|
@@ -930,7 +1078,7 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
930
1078
|
|
|
931
1079
|
def _ensure_agent(self) -> PlaywrightLLMAgent:
|
|
932
1080
|
r"""Create PlaywrightLLMAgent on first use."""
|
|
933
|
-
if self.
|
|
1081
|
+
if self._web_agent_model is None:
|
|
934
1082
|
raise RuntimeError(
|
|
935
1083
|
"web_agent_model required for high-level task planning"
|
|
936
1084
|
)
|
|
@@ -939,7 +1087,7 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
939
1087
|
self._agent = PlaywrightLLMAgent(
|
|
940
1088
|
headless=self._headless,
|
|
941
1089
|
user_data_dir=self._user_data_dir,
|
|
942
|
-
model_backend=self.
|
|
1090
|
+
model_backend=self._web_agent_model,
|
|
943
1091
|
)
|
|
944
1092
|
return self._agent
|
|
945
1093
|
|
|
@@ -950,10 +1098,10 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
950
1098
|
default page.
|
|
951
1099
|
|
|
952
1100
|
This method initializes the underlying browser instance and
|
|
953
|
-
automatically
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1101
|
+
automatically navigates to the default start URL that was configured
|
|
1102
|
+
during toolkit initialization in the first tab. Agents cannot specify
|
|
1103
|
+
a custom URL - they must use the visit_page tool to open new tabs
|
|
1104
|
+
with other URLs.
|
|
957
1105
|
|
|
958
1106
|
Returns:
|
|
959
1107
|
Dict[str, Any]: A dictionary containing:
|
|
@@ -979,9 +1127,10 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
979
1127
|
|
|
980
1128
|
try:
|
|
981
1129
|
# Always use the configured default start URL
|
|
982
|
-
start_url = self.
|
|
1130
|
+
start_url = self._default_start_url
|
|
983
1131
|
logger.info(f"Navigating to configured default page: {start_url}")
|
|
984
1132
|
|
|
1133
|
+
# Use visit_page without creating a new tab
|
|
985
1134
|
result = await self.visit_page(start_url)
|
|
986
1135
|
|
|
987
1136
|
# Log success
|
|
@@ -991,8 +1140,8 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
991
1140
|
action_name="open_browser",
|
|
992
1141
|
inputs=inputs,
|
|
993
1142
|
outputs={
|
|
994
|
-
"result": "Browser opened and navigated to
|
|
995
|
-
"page."
|
|
1143
|
+
"result": "Browser opened and navigated to "
|
|
1144
|
+
"default page."
|
|
996
1145
|
},
|
|
997
1146
|
execution_time=execution_time,
|
|
998
1147
|
)
|
|
@@ -1035,21 +1184,17 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1035
1184
|
|
|
1036
1185
|
@action_logger
|
|
1037
1186
|
async def visit_page(self, url: str) -> Dict[str, Any]:
|
|
1038
|
-
r"""Navigates
|
|
1187
|
+
r"""Navigates to a URL.
|
|
1188
|
+
|
|
1189
|
+
This method creates a new tab for the URL instead of navigating
|
|
1190
|
+
in the current tab, allowing better multi-tab management.
|
|
1039
1191
|
|
|
1040
1192
|
Args:
|
|
1041
|
-
url (str): The web address to load in the browser.
|
|
1042
|
-
valid URL.
|
|
1193
|
+
url (str): The web address to load in the browser.
|
|
1043
1194
|
|
|
1044
1195
|
Returns:
|
|
1045
|
-
Dict[str, Any]: A dictionary containing
|
|
1046
|
-
|
|
1047
|
-
e.g., "Navigation successful.".
|
|
1048
|
-
- "snapshot": A new textual snapshot of the page's interactive
|
|
1049
|
-
elements after the new page has loaded.
|
|
1050
|
-
- "tabs": List of all open tabs with their information.
|
|
1051
|
-
- "current_tab": Index of the currently active tab.
|
|
1052
|
-
- "total_tabs": Total number of open tabs.
|
|
1196
|
+
Dict[str, Any]: A dictionary containing the result, snapshot, and
|
|
1197
|
+
tab information.
|
|
1053
1198
|
"""
|
|
1054
1199
|
if not url or not isinstance(url, str):
|
|
1055
1200
|
return {
|
|
@@ -1063,22 +1208,50 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1063
1208
|
if '://' not in url:
|
|
1064
1209
|
url = f'https://{url}'
|
|
1065
1210
|
|
|
1066
|
-
|
|
1211
|
+
await self._ensure_browser()
|
|
1212
|
+
session = await self._get_session()
|
|
1213
|
+
nav_result = ""
|
|
1067
1214
|
|
|
1068
|
-
#
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1215
|
+
# By default, we want to create a new tab.
|
|
1216
|
+
should_create_new_tab = True
|
|
1217
|
+
try:
|
|
1218
|
+
# If the browser has just started with a single "about:blank" tab,
|
|
1219
|
+
# use that tab instead of creating a new one.
|
|
1220
|
+
tab_info_data = await self._get_tab_info_for_output()
|
|
1221
|
+
tabs = tab_info_data.get("tabs", [])
|
|
1222
|
+
if len(tabs) == 1 and tabs[0].get("url") == "about:blank":
|
|
1223
|
+
logger.info(
|
|
1224
|
+
"Found single blank tab, navigating in current tab "
|
|
1225
|
+
"instead of creating a new one."
|
|
1226
|
+
)
|
|
1227
|
+
should_create_new_tab = False
|
|
1228
|
+
except Exception as e:
|
|
1229
|
+
logger.warning(
|
|
1230
|
+
"Could not get tab info to check for blank tab, "
|
|
1231
|
+
f"proceeding with default behavior (new tab). Error: {e}"
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
if should_create_new_tab:
|
|
1235
|
+
logger.info(f"Creating new tab and navigating to URL: {url}")
|
|
1236
|
+
try:
|
|
1237
|
+
new_tab_index = await session.create_new_tab(url)
|
|
1238
|
+
await session.switch_to_tab(new_tab_index)
|
|
1239
|
+
nav_result = f"Visited {url} in new tab {new_tab_index}"
|
|
1240
|
+
except Exception as e:
|
|
1241
|
+
logger.error(f"Failed to create new tab and navigate: {e}")
|
|
1242
|
+
nav_result = f"Error creating new tab: {e}"
|
|
1243
|
+
else:
|
|
1244
|
+
logger.info(f"Navigating to URL in current tab: {url}")
|
|
1245
|
+
nav_result = await session.visit(url)
|
|
1073
1246
|
|
|
1074
1247
|
# Get snapshot
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1248
|
+
snapshot = ""
|
|
1249
|
+
try:
|
|
1250
|
+
snapshot = await session.get_snapshot(
|
|
1251
|
+
force_refresh=True, diff_only=False
|
|
1252
|
+
)
|
|
1253
|
+
except Exception as e:
|
|
1254
|
+
logger.warning(f"Failed to capture snapshot: {e}")
|
|
1082
1255
|
|
|
1083
1256
|
# Get tab information
|
|
1084
1257
|
tab_info = await self._get_tab_info_for_output()
|
|
@@ -1110,12 +1283,16 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1110
1283
|
try:
|
|
1111
1284
|
logger.info("Navigating back in browser history...")
|
|
1112
1285
|
nav_start = time.time()
|
|
1113
|
-
await page.go_back(
|
|
1286
|
+
await page.go_back(
|
|
1287
|
+
wait_until="domcontentloaded", timeout=self._navigation_timeout
|
|
1288
|
+
)
|
|
1114
1289
|
nav_time = time.time() - nav_start
|
|
1115
1290
|
logger.info(f"Back navigation completed in {nav_time:.2f}s")
|
|
1116
1291
|
|
|
1117
|
-
#
|
|
1118
|
-
|
|
1292
|
+
# Minimal wait for page stability (back navigation is usually fast)
|
|
1293
|
+
import asyncio
|
|
1294
|
+
|
|
1295
|
+
await asyncio.sleep(0.2)
|
|
1119
1296
|
|
|
1120
1297
|
# Get snapshot
|
|
1121
1298
|
logger.info("Capturing page snapshot after back navigation...")
|
|
@@ -1175,12 +1352,17 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1175
1352
|
try:
|
|
1176
1353
|
logger.info("Navigating forward in browser history...")
|
|
1177
1354
|
nav_start = time.time()
|
|
1178
|
-
await page.go_forward(
|
|
1355
|
+
await page.go_forward(
|
|
1356
|
+
wait_until="domcontentloaded", timeout=self._navigation_timeout
|
|
1357
|
+
)
|
|
1179
1358
|
nav_time = time.time() - nav_start
|
|
1180
1359
|
logger.info(f"Forward navigation completed in {nav_time:.2f}s")
|
|
1181
1360
|
|
|
1182
|
-
#
|
|
1183
|
-
|
|
1361
|
+
# Minimal wait for page stability (forward navigation is usually
|
|
1362
|
+
# fast)
|
|
1363
|
+
import asyncio
|
|
1364
|
+
|
|
1365
|
+
await asyncio.sleep(0.2)
|
|
1184
1366
|
|
|
1185
1367
|
# Get snapshot
|
|
1186
1368
|
logger.info("Capturing page snapshot after forward navigation...")
|
|
@@ -1285,13 +1467,11 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1285
1467
|
# Log screenshot timeout start
|
|
1286
1468
|
logger.info(
|
|
1287
1469
|
f"Starting screenshot capture"
|
|
1288
|
-
f"with timeout: {self.
|
|
1470
|
+
f"with timeout: {self._screenshot_timeout}ms"
|
|
1289
1471
|
)
|
|
1290
1472
|
|
|
1291
1473
|
start_time = time.time()
|
|
1292
|
-
image_data = await page.screenshot(
|
|
1293
|
-
timeout=self.DEFAULT_SCREENSHOT_TIMEOUT
|
|
1294
|
-
)
|
|
1474
|
+
image_data = await page.screenshot(timeout=self._screenshot_timeout)
|
|
1295
1475
|
screenshot_time = time.time() - start_time
|
|
1296
1476
|
|
|
1297
1477
|
logger.info(f"Screenshot capture completed in {screenshot_time:.2f}s")
|
|
@@ -1317,7 +1497,7 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1317
1497
|
url_name = sanitize_filename(str(parsed_url.path), max_length=241)
|
|
1318
1498
|
timestamp = datetime.datetime.now().strftime("%m%d%H%M%S")
|
|
1319
1499
|
file_path = os.path.join(
|
|
1320
|
-
self.
|
|
1500
|
+
self._cache_dir, f"{url_name}_{timestamp}_som.png"
|
|
1321
1501
|
)
|
|
1322
1502
|
marked_image.save(file_path, "PNG")
|
|
1323
1503
|
|
|
@@ -1706,7 +1886,7 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1706
1886
|
enabled_tools = []
|
|
1707
1887
|
|
|
1708
1888
|
for tool_name in self.enabled_tools:
|
|
1709
|
-
if tool_name == "solve_task" and self.
|
|
1889
|
+
if tool_name == "solve_task" and self._web_agent_model is None:
|
|
1710
1890
|
logger.warning(
|
|
1711
1891
|
f"Tool '{tool_name}' is enabled but web_agent_model "
|
|
1712
1892
|
f"is not provided. Skipping this tool."
|
|
@@ -1746,13 +1926,20 @@ class HybridBrowserToolkit(BaseToolkit):
|
|
|
1746
1926
|
return HybridBrowserToolkit(
|
|
1747
1927
|
headless=self._headless,
|
|
1748
1928
|
user_data_dir=self._user_data_dir,
|
|
1749
|
-
stealth=self.
|
|
1750
|
-
web_agent_model=self.
|
|
1751
|
-
cache_dir=f"{self.
|
|
1929
|
+
stealth=self._stealth,
|
|
1930
|
+
web_agent_model=self._web_agent_model,
|
|
1931
|
+
cache_dir=f"{self._cache_dir.rstrip('/')}_clone_{new_session_id}/",
|
|
1752
1932
|
enabled_tools=self.enabled_tools.copy(),
|
|
1753
|
-
browser_log_to_file=self.
|
|
1933
|
+
browser_log_to_file=self._browser_log_to_file,
|
|
1754
1934
|
session_id=new_session_id,
|
|
1755
|
-
default_start_url=self.
|
|
1935
|
+
default_start_url=self._default_start_url,
|
|
1936
|
+
default_timeout=self._default_timeout,
|
|
1937
|
+
short_timeout=self._short_timeout,
|
|
1938
|
+
navigation_timeout=self._navigation_timeout,
|
|
1939
|
+
network_idle_timeout=self._network_idle_timeout,
|
|
1940
|
+
screenshot_timeout=self._screenshot_timeout,
|
|
1941
|
+
page_stability_timeout=self._page_stability_timeout,
|
|
1942
|
+
dom_content_loaded_timeout=self._dom_content_loaded_timeout,
|
|
1756
1943
|
)
|
|
1757
1944
|
|
|
1758
1945
|
@action_logger
|
|
@@ -20,6 +20,8 @@ if TYPE_CHECKING:
|
|
|
20
20
|
# Logging support
|
|
21
21
|
from camel.logger import get_logger
|
|
22
22
|
|
|
23
|
+
from .config_loader import ConfigLoader
|
|
24
|
+
|
|
23
25
|
logger = get_logger(__name__)
|
|
24
26
|
|
|
25
27
|
|
|
@@ -27,8 +29,6 @@ class PageSnapshot:
|
|
|
27
29
|
"""Utility for capturing YAML-like page snapshots and diff-only
|
|
28
30
|
variants."""
|
|
29
31
|
|
|
30
|
-
MAX_TIMEOUT_MS = 5000 # wait_for_load_state timeout
|
|
31
|
-
|
|
32
32
|
def __init__(self, page: "Page"):
|
|
33
33
|
self.page = page
|
|
34
34
|
self.snapshot_data: Optional[str] = None # last full snapshot
|
|
@@ -37,6 +37,7 @@ class PageSnapshot:
|
|
|
37
37
|
"is_diff": False,
|
|
38
38
|
"priorities": [1, 2, 3],
|
|
39
39
|
}
|
|
40
|
+
self.dom_timeout = ConfigLoader.get_dom_content_loaded_timeout()
|
|
40
41
|
|
|
41
42
|
# ---------------------------------------------------------------------
|
|
42
43
|
# Public API
|
|
@@ -60,7 +61,7 @@ class PageSnapshot:
|
|
|
60
61
|
|
|
61
62
|
# ensure DOM stability
|
|
62
63
|
await self.page.wait_for_load_state(
|
|
63
|
-
'domcontentloaded', timeout=self.
|
|
64
|
+
'domcontentloaded', timeout=self.dom_timeout
|
|
64
65
|
)
|
|
65
66
|
|
|
66
67
|
logger.debug("Capturing page snapshot …")
|
|
@@ -153,7 +154,7 @@ class PageSnapshot:
|
|
|
153
154
|
# Wait for next DOM stability before retrying
|
|
154
155
|
try:
|
|
155
156
|
await self.page.wait_for_load_state(
|
|
156
|
-
"domcontentloaded", timeout=self.
|
|
157
|
+
"domcontentloaded", timeout=self.dom_timeout
|
|
157
158
|
)
|
|
158
159
|
except Exception:
|
|
159
160
|
# Even if waiting fails, attempt retry to give it
|