camel-ai 0.2.71a6__py3-none-any.whl → 0.2.71a8__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.

@@ -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.web_agent_model = web_agent_model
148
- self.cache_dir = cache_dir
149
- self.default_start_url = default_start_url
150
- os.makedirs(self.cache_dir, exist_ok=True)
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.PAGE_STABILITY_TIMEOUT
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 (important for AJAX/SPA)
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.NETWORK_IDLE_TIMEOUT
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
- # Additional small delay for JavaScript execution
462
- await asyncio.sleep(0.5)
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(self) -> Dict[str, Any]:
471
- r"""Get unified analysis data from the page."""
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
- result = await page.evaluate(self._unified_script)
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
- return result
485
- except Exception as e:
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
- snapshot_start = time.time()
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
- snapshot_time = time.time() - snapshot_start
880
+ before_snapshot_time = time.time() - snapshot_start_before
736
881
  logger.info(
737
- f"Pre-action snapshot captured in {snapshot_time:.2f}s"
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
- snapshot_start = time.time()
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
- snapshot_time = time.time() - snapshot_start
932
+ after_snapshot_time = time.time() - snapshot_start_after
788
933
  logger.info(
789
- f"Post-action snapshot " f"captured in {snapshot_time:.2f}s"
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.web_agent_model is None:
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.web_agent_model,
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
- navigates to the default start URL that was configured during toolkit
955
- initialization. Agents cannot specify a custom URL - they must use the
956
- visit_page tool for navigation to other URLs.
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.default_start_url
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 default "
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 the current browser page to a specified URL.
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. Must be a
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
- - "result": A message indicating the outcome of the navigation,
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
- logger.info(f"Navigating to URL: {url}")
1211
+ await self._ensure_browser()
1212
+ session = await self._get_session()
1213
+ nav_result = ""
1067
1214
 
1068
- # Navigate to page
1069
- nav_start = time.time()
1070
- nav_result = await self._session.visit(url)
1071
- nav_time = time.time() - nav_start
1072
- logger.info(f"Page navigation completed in {nav_time:.2f}s")
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
- logger.info("Capturing page snapshot after navigation...")
1076
- snapshot_start = time.time()
1077
- snapshot = await self._session.get_snapshot(
1078
- force_refresh=True, diff_only=False
1079
- )
1080
- snapshot_time = time.time() - snapshot_start
1081
- logger.info(f"Navigation snapshot captured in {snapshot_time:.2f}s")
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(wait_until="domcontentloaded", timeout=30000)
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
- # Wait for page stability
1118
- await self._wait_for_page_stability()
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(wait_until="domcontentloaded", timeout=30000)
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
- # Wait for page stability
1183
- await self._wait_for_page_stability()
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.DEFAULT_SCREENSHOT_TIMEOUT}ms"
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.cache_dir, f"{url_name}_{timestamp}_som.png"
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.web_agent_model is None:
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._session._stealth if self._session else False,
1750
- web_agent_model=self.web_agent_model,
1751
- cache_dir=f"{self.cache_dir.rstrip('/')}_clone_{new_session_id}/",
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.log_to_file,
1933
+ browser_log_to_file=self._browser_log_to_file,
1754
1934
  session_id=new_session_id,
1755
- default_start_url=self.default_start_url,
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.MAX_TIMEOUT_MS
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.MAX_TIMEOUT_MS
157
+ "domcontentloaded", timeout=self.dom_timeout
157
158
  )
158
159
  except Exception:
159
160
  # Even if waiting fails, attempt retry to give it