optexity-browser-use 0.9.5__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.
Files changed (147) hide show
  1. browser_use/__init__.py +157 -0
  2. browser_use/actor/__init__.py +11 -0
  3. browser_use/actor/element.py +1175 -0
  4. browser_use/actor/mouse.py +134 -0
  5. browser_use/actor/page.py +561 -0
  6. browser_use/actor/playground/flights.py +41 -0
  7. browser_use/actor/playground/mixed_automation.py +54 -0
  8. browser_use/actor/playground/playground.py +236 -0
  9. browser_use/actor/utils.py +176 -0
  10. browser_use/agent/cloud_events.py +282 -0
  11. browser_use/agent/gif.py +424 -0
  12. browser_use/agent/judge.py +170 -0
  13. browser_use/agent/message_manager/service.py +473 -0
  14. browser_use/agent/message_manager/utils.py +52 -0
  15. browser_use/agent/message_manager/views.py +98 -0
  16. browser_use/agent/prompts.py +413 -0
  17. browser_use/agent/service.py +2316 -0
  18. browser_use/agent/system_prompt.md +185 -0
  19. browser_use/agent/system_prompt_flash.md +10 -0
  20. browser_use/agent/system_prompt_no_thinking.md +183 -0
  21. browser_use/agent/views.py +743 -0
  22. browser_use/browser/__init__.py +41 -0
  23. browser_use/browser/cloud/cloud.py +203 -0
  24. browser_use/browser/cloud/views.py +89 -0
  25. browser_use/browser/events.py +578 -0
  26. browser_use/browser/profile.py +1158 -0
  27. browser_use/browser/python_highlights.py +548 -0
  28. browser_use/browser/session.py +3225 -0
  29. browser_use/browser/session_manager.py +399 -0
  30. browser_use/browser/video_recorder.py +162 -0
  31. browser_use/browser/views.py +200 -0
  32. browser_use/browser/watchdog_base.py +260 -0
  33. browser_use/browser/watchdogs/__init__.py +0 -0
  34. browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
  35. browser_use/browser/watchdogs/crash_watchdog.py +335 -0
  36. browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
  37. browser_use/browser/watchdogs/dom_watchdog.py +817 -0
  38. browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
  39. browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
  40. browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
  41. browser_use/browser/watchdogs/popups_watchdog.py +143 -0
  42. browser_use/browser/watchdogs/recording_watchdog.py +126 -0
  43. browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
  44. browser_use/browser/watchdogs/security_watchdog.py +280 -0
  45. browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
  46. browser_use/cli.py +2359 -0
  47. browser_use/code_use/__init__.py +16 -0
  48. browser_use/code_use/formatting.py +192 -0
  49. browser_use/code_use/namespace.py +665 -0
  50. browser_use/code_use/notebook_export.py +276 -0
  51. browser_use/code_use/service.py +1340 -0
  52. browser_use/code_use/system_prompt.md +574 -0
  53. browser_use/code_use/utils.py +150 -0
  54. browser_use/code_use/views.py +171 -0
  55. browser_use/config.py +505 -0
  56. browser_use/controller/__init__.py +3 -0
  57. browser_use/dom/enhanced_snapshot.py +161 -0
  58. browser_use/dom/markdown_extractor.py +169 -0
  59. browser_use/dom/playground/extraction.py +312 -0
  60. browser_use/dom/playground/multi_act.py +32 -0
  61. browser_use/dom/serializer/clickable_elements.py +200 -0
  62. browser_use/dom/serializer/code_use_serializer.py +287 -0
  63. browser_use/dom/serializer/eval_serializer.py +478 -0
  64. browser_use/dom/serializer/html_serializer.py +212 -0
  65. browser_use/dom/serializer/paint_order.py +197 -0
  66. browser_use/dom/serializer/serializer.py +1170 -0
  67. browser_use/dom/service.py +825 -0
  68. browser_use/dom/utils.py +129 -0
  69. browser_use/dom/views.py +906 -0
  70. browser_use/exceptions.py +5 -0
  71. browser_use/filesystem/__init__.py +0 -0
  72. browser_use/filesystem/file_system.py +619 -0
  73. browser_use/init_cmd.py +376 -0
  74. browser_use/integrations/gmail/__init__.py +24 -0
  75. browser_use/integrations/gmail/actions.py +115 -0
  76. browser_use/integrations/gmail/service.py +225 -0
  77. browser_use/llm/__init__.py +155 -0
  78. browser_use/llm/anthropic/chat.py +242 -0
  79. browser_use/llm/anthropic/serializer.py +312 -0
  80. browser_use/llm/aws/__init__.py +36 -0
  81. browser_use/llm/aws/chat_anthropic.py +242 -0
  82. browser_use/llm/aws/chat_bedrock.py +289 -0
  83. browser_use/llm/aws/serializer.py +257 -0
  84. browser_use/llm/azure/chat.py +91 -0
  85. browser_use/llm/base.py +57 -0
  86. browser_use/llm/browser_use/__init__.py +3 -0
  87. browser_use/llm/browser_use/chat.py +201 -0
  88. browser_use/llm/cerebras/chat.py +193 -0
  89. browser_use/llm/cerebras/serializer.py +109 -0
  90. browser_use/llm/deepseek/chat.py +212 -0
  91. browser_use/llm/deepseek/serializer.py +109 -0
  92. browser_use/llm/exceptions.py +29 -0
  93. browser_use/llm/google/__init__.py +3 -0
  94. browser_use/llm/google/chat.py +542 -0
  95. browser_use/llm/google/serializer.py +120 -0
  96. browser_use/llm/groq/chat.py +229 -0
  97. browser_use/llm/groq/parser.py +158 -0
  98. browser_use/llm/groq/serializer.py +159 -0
  99. browser_use/llm/messages.py +238 -0
  100. browser_use/llm/models.py +271 -0
  101. browser_use/llm/oci_raw/__init__.py +10 -0
  102. browser_use/llm/oci_raw/chat.py +443 -0
  103. browser_use/llm/oci_raw/serializer.py +229 -0
  104. browser_use/llm/ollama/chat.py +97 -0
  105. browser_use/llm/ollama/serializer.py +143 -0
  106. browser_use/llm/openai/chat.py +264 -0
  107. browser_use/llm/openai/like.py +15 -0
  108. browser_use/llm/openai/serializer.py +165 -0
  109. browser_use/llm/openrouter/chat.py +211 -0
  110. browser_use/llm/openrouter/serializer.py +26 -0
  111. browser_use/llm/schema.py +176 -0
  112. browser_use/llm/views.py +48 -0
  113. browser_use/logging_config.py +330 -0
  114. browser_use/mcp/__init__.py +18 -0
  115. browser_use/mcp/__main__.py +12 -0
  116. browser_use/mcp/client.py +544 -0
  117. browser_use/mcp/controller.py +264 -0
  118. browser_use/mcp/server.py +1114 -0
  119. browser_use/observability.py +204 -0
  120. browser_use/py.typed +0 -0
  121. browser_use/sandbox/__init__.py +41 -0
  122. browser_use/sandbox/sandbox.py +637 -0
  123. browser_use/sandbox/views.py +132 -0
  124. browser_use/screenshots/__init__.py +1 -0
  125. browser_use/screenshots/service.py +52 -0
  126. browser_use/sync/__init__.py +6 -0
  127. browser_use/sync/auth.py +357 -0
  128. browser_use/sync/service.py +161 -0
  129. browser_use/telemetry/__init__.py +51 -0
  130. browser_use/telemetry/service.py +112 -0
  131. browser_use/telemetry/views.py +101 -0
  132. browser_use/tokens/__init__.py +0 -0
  133. browser_use/tokens/custom_pricing.py +24 -0
  134. browser_use/tokens/mappings.py +4 -0
  135. browser_use/tokens/service.py +580 -0
  136. browser_use/tokens/views.py +108 -0
  137. browser_use/tools/registry/service.py +572 -0
  138. browser_use/tools/registry/views.py +174 -0
  139. browser_use/tools/service.py +1675 -0
  140. browser_use/tools/utils.py +82 -0
  141. browser_use/tools/views.py +100 -0
  142. browser_use/utils.py +670 -0
  143. optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
  144. optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
  145. optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
  146. optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
  147. optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,200 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any
3
+
4
+ from bubus import BaseEvent
5
+ from cdp_use.cdp.target import TargetID
6
+ from pydantic import AliasChoices, BaseModel, ConfigDict, Field, field_serializer
7
+
8
+ from browser_use.dom.views import DOMInteractedElement, SerializedDOMState
9
+
10
+ # Known placeholder image data for about:blank pages - a 4x4 white PNG
11
+ PLACEHOLDER_4PX_SCREENSHOT = (
12
+ 'iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAAFElEQVR4nGP8//8/AwwwMSAB3BwAlm4DBfIlvvkAAAAASUVORK5CYII='
13
+ )
14
+
15
+
16
+ # Pydantic
17
+ class TabInfo(BaseModel):
18
+ """Represents information about a browser tab"""
19
+
20
+ model_config = ConfigDict(
21
+ extra='forbid',
22
+ validate_by_name=True,
23
+ validate_by_alias=True,
24
+ populate_by_name=True,
25
+ )
26
+
27
+ # Original fields
28
+ url: str
29
+ title: str
30
+ target_id: TargetID = Field(serialization_alias='tab_id', validation_alias=AliasChoices('tab_id', 'target_id'))
31
+ parent_target_id: TargetID | None = Field(
32
+ default=None, serialization_alias='parent_tab_id', validation_alias=AliasChoices('parent_tab_id', 'parent_target_id')
33
+ ) # parent page that contains this popup or cross-origin iframe
34
+
35
+ @field_serializer('target_id')
36
+ def serialize_target_id(self, target_id: TargetID, _info: Any) -> str:
37
+ return target_id[-4:]
38
+
39
+ @field_serializer('parent_target_id')
40
+ def serialize_parent_target_id(self, parent_target_id: TargetID | None, _info: Any) -> str | None:
41
+ return parent_target_id[-4:] if parent_target_id else None
42
+
43
+
44
+ class PageInfo(BaseModel):
45
+ """Comprehensive page size and scroll information"""
46
+
47
+ # Current viewport dimensions
48
+ viewport_width: int
49
+ viewport_height: int
50
+
51
+ # Total page dimensions
52
+ page_width: int
53
+ page_height: int
54
+
55
+ # Current scroll position
56
+ scroll_x: int
57
+ scroll_y: int
58
+
59
+ # Calculated scroll information
60
+ pixels_above: int
61
+ pixels_below: int
62
+ pixels_left: int
63
+ pixels_right: int
64
+
65
+ # Page statistics are now computed dynamically instead of stored
66
+
67
+
68
+ @dataclass
69
+ class NetworkRequest:
70
+ """Information about a pending network request"""
71
+
72
+ url: str
73
+ method: str = 'GET'
74
+ loading_duration_ms: float = 0.0 # How long this request has been loading (ms since request started, max 10s)
75
+ resource_type: str | None = None # e.g., 'Document', 'Stylesheet', 'Image', 'Script', 'XHR', 'Fetch'
76
+
77
+
78
+ @dataclass
79
+ class PaginationButton:
80
+ """Information about a pagination button detected on the page"""
81
+
82
+ button_type: str # 'next', 'prev', 'first', 'last', 'page_number'
83
+ backend_node_id: int # Backend node ID for clicking
84
+ text: str # Button text/label
85
+ selector: str # XPath or other selector to locate the element
86
+ is_disabled: bool = False # Whether the button appears disabled
87
+
88
+
89
+ @dataclass
90
+ class BrowserStateSummary:
91
+ """The summary of the browser's current state designed for an LLM to process"""
92
+
93
+ # provided by SerializedDOMState:
94
+ dom_state: SerializedDOMState
95
+
96
+ url: str
97
+ title: str
98
+ tabs: list[TabInfo]
99
+ screenshot: str | None = field(default=None, repr=False)
100
+ page_info: PageInfo | None = None # Enhanced page information
101
+
102
+ # Keep legacy fields for backward compatibility
103
+ pixels_above: int = 0
104
+ pixels_below: int = 0
105
+ browser_errors: list[str] = field(default_factory=list)
106
+ is_pdf_viewer: bool = False # Whether the current page is a PDF viewer
107
+ recent_events: str | None = None # Text summary of recent browser events
108
+ pending_network_requests: list[NetworkRequest] = field(default_factory=list) # Currently loading network requests
109
+ pagination_buttons: list[PaginationButton] = field(default_factory=list) # Detected pagination buttons
110
+ closed_popup_messages: list[str] = field(default_factory=list) # Messages from auto-closed JavaScript dialogs
111
+
112
+
113
+ @dataclass
114
+ class BrowserStateHistory:
115
+ """The summary of the browser's state at a past point in time to usse in LLM message history"""
116
+
117
+ url: str
118
+ title: str
119
+ tabs: list[TabInfo]
120
+ interacted_element: list[DOMInteractedElement | None] | list[None]
121
+ screenshot_path: str | None = None
122
+
123
+ def get_screenshot(self) -> str | None:
124
+ """Load screenshot from disk and return as base64 string"""
125
+ if not self.screenshot_path:
126
+ return None
127
+
128
+ import base64
129
+ from pathlib import Path
130
+
131
+ path_obj = Path(self.screenshot_path)
132
+ if not path_obj.exists():
133
+ return None
134
+
135
+ try:
136
+ with open(path_obj, 'rb') as f:
137
+ screenshot_data = f.read()
138
+ return base64.b64encode(screenshot_data).decode('utf-8')
139
+ except Exception:
140
+ return None
141
+
142
+ def to_dict(self) -> dict[str, Any]:
143
+ data = {}
144
+ data['tabs'] = [tab.model_dump() for tab in self.tabs]
145
+ data['screenshot_path'] = self.screenshot_path
146
+ data['interacted_element'] = [el.to_dict() if el else None for el in self.interacted_element]
147
+ data['url'] = self.url
148
+ data['title'] = self.title
149
+ return data
150
+
151
+
152
+ class BrowserError(Exception):
153
+ """Browser error with structured memory for LLM context management.
154
+
155
+ This exception class provides separate memory contexts for browser actions:
156
+ - short_term_memory: Immediate context shown once to the LLM for the next action
157
+ - long_term_memory: Persistent error information stored across steps
158
+ """
159
+
160
+ message: str
161
+ short_term_memory: str | None = None
162
+ long_term_memory: str | None = None
163
+ details: dict[str, Any] | None = None
164
+ while_handling_event: BaseEvent[Any] | None = None
165
+
166
+ def __init__(
167
+ self,
168
+ message: str,
169
+ short_term_memory: str | None = None,
170
+ long_term_memory: str | None = None,
171
+ details: dict[str, Any] | None = None,
172
+ event: BaseEvent[Any] | None = None,
173
+ ):
174
+ """Initialize a BrowserError with structured memory contexts.
175
+
176
+ Args:
177
+ message: Technical error message for logging and debugging
178
+ short_term_memory: Context shown once to LLM (e.g., available actions, options)
179
+ long_term_memory: Persistent error info stored in agent memory
180
+ details: Additional metadata for debugging
181
+ event: The browser event that triggered this error
182
+ """
183
+ self.message = message
184
+ self.short_term_memory = short_term_memory
185
+ self.long_term_memory = long_term_memory
186
+ self.details = details
187
+ self.while_handling_event = event
188
+ super().__init__(message)
189
+
190
+ def __str__(self) -> str:
191
+ if self.details:
192
+ return f'{self.message} ({self.details}) during: {self.while_handling_event}'
193
+ elif self.while_handling_event:
194
+ return f'{self.message} (while handling: {self.while_handling_event})'
195
+ else:
196
+ return self.message
197
+
198
+
199
+ class URLNotAllowedError(BrowserError):
200
+ """Error raised when a URL is not allowed"""
@@ -0,0 +1,260 @@
1
+ """Base watchdog class for browser monitoring components."""
2
+
3
+ import inspect
4
+ import time
5
+ from collections.abc import Iterable
6
+ from typing import Any, ClassVar
7
+
8
+ from bubus import BaseEvent, EventBus
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+ from browser_use.browser.session import BrowserSession
12
+
13
+
14
+ class BaseWatchdog(BaseModel):
15
+ """Base class for all browser watchdogs.
16
+
17
+ Watchdogs monitor browser state and emit events based on changes.
18
+ They automatically register event handlers based on method names.
19
+
20
+ Handler methods should be named: on_EventTypeName(self, event: EventTypeName)
21
+ """
22
+
23
+ model_config = ConfigDict(
24
+ arbitrary_types_allowed=True, # allow non-serializable objects like EventBus/BrowserSession in fields
25
+ extra='forbid', # dont allow implicit class/instance state, everything must be a properly typed Field or PrivateAttr
26
+ validate_assignment=False, # avoid re-triggering __init__ / validators on values on every assignment
27
+ revalidate_instances='never', # avoid re-triggering __init__ / validators and erasing private attrs
28
+ )
29
+
30
+ # Class variables to statically define the list of events relevant to each watchdog
31
+ # (not enforced, just to make it easier to understand the code and debug watchdogs at runtime)
32
+ LISTENS_TO: ClassVar[list[type[BaseEvent[Any]]]] = [] # Events this watchdog listens to
33
+ EMITS: ClassVar[list[type[BaseEvent[Any]]]] = [] # Events this watchdog emits
34
+
35
+ # Core dependencies
36
+ event_bus: EventBus = Field()
37
+ browser_session: BrowserSession = Field()
38
+
39
+ # Shared state that other watchdogs might need to access should not be defined on BrowserSession, not here!
40
+ # Shared helper methods needed by other watchdogs should be defined on BrowserSession, not here!
41
+ # Alternatively, expose some events on the watchdog to allow access to state/helpers via event_bus system.
42
+
43
+ # Private state internal to the watchdog can be defined like this on BaseWatchdog subclasses:
44
+ # _screenshot_cache: dict[str, bytes] = PrivateAttr(default_factory=dict)
45
+ # _browser_crash_watcher_task: asyncio.Task | None = PrivateAttr(default=None)
46
+ # _cdp_download_tasks: WeakSet[asyncio.Task] = PrivateAttr(default_factory=WeakSet)
47
+ # ...
48
+
49
+ @property
50
+ def logger(self):
51
+ """Get the logger from the browser session."""
52
+ return self.browser_session.logger
53
+
54
+ @staticmethod
55
+ def attach_handler_to_session(browser_session: 'BrowserSession', event_class: type[BaseEvent[Any]], handler) -> None:
56
+ """Attach a single event handler to a browser session.
57
+
58
+ Args:
59
+ browser_session: The browser session to attach to
60
+ event_class: The event class to listen for
61
+ handler: The handler method (must start with 'on_' and end with event type)
62
+ """
63
+ event_bus = browser_session.event_bus
64
+
65
+ # Validate handler naming convention
66
+ assert hasattr(handler, '__name__'), 'Handler must have a __name__ attribute'
67
+ assert handler.__name__.startswith('on_'), f'Handler {handler.__name__} must start with "on_"'
68
+ assert handler.__name__.endswith(event_class.__name__), (
69
+ f'Handler {handler.__name__} must end with event type {event_class.__name__}'
70
+ )
71
+
72
+ # Get the watchdog instance if this is a bound method
73
+ watchdog_instance = getattr(handler, '__self__', None)
74
+ watchdog_class_name = watchdog_instance.__class__.__name__ if watchdog_instance else 'Unknown'
75
+
76
+ # Create a wrapper function with unique name to avoid duplicate handler warnings
77
+ # Capture handler by value to avoid closure issues
78
+ def make_unique_handler(actual_handler):
79
+ async def unique_handler(event):
80
+ # just for debug logging, not used for anything else
81
+ parent_event = event_bus.event_history.get(event.event_parent_id) if event.event_parent_id else None
82
+ grandparent_event = (
83
+ event_bus.event_history.get(parent_event.event_parent_id)
84
+ if parent_event and parent_event.event_parent_id
85
+ else None
86
+ )
87
+ parent = (
88
+ f'โ†ฒ triggered by on_{parent_event.event_type}#{parent_event.event_id[-4:]}'
89
+ if parent_event
90
+ else '๐Ÿ‘ˆ by Agent'
91
+ )
92
+ grandparent = (
93
+ (
94
+ f'โ†ฒ under {grandparent_event.event_type}#{grandparent_event.event_id[-4:]}'
95
+ if grandparent_event
96
+ else '๐Ÿ‘ˆ by Agent'
97
+ )
98
+ if parent_event
99
+ else ''
100
+ )
101
+ event_str = f'#{event.event_id[-4:]}'
102
+ time_start = time.time()
103
+ watchdog_and_handler_str = f'[{watchdog_class_name}.{actual_handler.__name__}({event_str})]'.ljust(54)
104
+ browser_session.logger.debug(f'๐ŸšŒ {watchdog_and_handler_str} โณ Starting... {parent} {grandparent}')
105
+
106
+ try:
107
+ # **EXECUTE THE EVENT HANDLER FUNCTION**
108
+ result = await actual_handler(event)
109
+
110
+ if isinstance(result, Exception):
111
+ raise result
112
+
113
+ # just for debug logging, not used for anything else
114
+ time_end = time.time()
115
+ time_elapsed = time_end - time_start
116
+ result_summary = '' if result is None else f' โžก๏ธ <{type(result).__name__}>'
117
+ parents_summary = f' {parent}'.replace('โ†ฒ triggered by ', 'โคด returned to ').replace(
118
+ '๐Ÿ‘ˆ by Agent', '๐Ÿ‘‰ returned to Agent'
119
+ )
120
+ browser_session.logger.debug(
121
+ f'๐ŸšŒ {watchdog_and_handler_str} Succeeded ({time_elapsed:.2f}s){result_summary}{parents_summary}'
122
+ )
123
+ return result
124
+ except Exception as e:
125
+ time_end = time.time()
126
+ time_elapsed = time_end - time_start
127
+ original_error = e
128
+ browser_session.logger.error(
129
+ f'๐ŸšŒ {watchdog_and_handler_str} โŒ Failed ({time_elapsed:.2f}s): {type(e).__name__}: {e}'
130
+ )
131
+
132
+ # attempt to repair potentially crashed CDP session
133
+ try:
134
+ if browser_session.agent_focus and browser_session.agent_focus.target_id:
135
+ # With event-driven sessions, Chrome will send detach/attach events
136
+ # SessionManager handles pool cleanup automatically
137
+ target_id_to_restore = browser_session.agent_focus.target_id
138
+ browser_session.logger.debug(
139
+ f'๐ŸšŒ {watchdog_and_handler_str} โš ๏ธ Session error detected, waiting for CDP events to sync\n\t{browser_session.agent_focus}'
140
+ )
141
+
142
+ # Wait for new attach event to restore the session
143
+ # This will raise ValueError if target doesn't re-attach
144
+ browser_session.agent_focus = await browser_session.get_or_create_cdp_session(
145
+ target_id=target_id_to_restore, focus=True
146
+ )
147
+ else:
148
+ # Try to get any available session
149
+ await browser_session.get_or_create_cdp_session(target_id=None, focus=True)
150
+ except Exception as sub_error:
151
+ if 'ConnectionClosedError' in str(type(sub_error)) or 'ConnectionError' in str(type(sub_error)):
152
+ browser_session.logger.error(
153
+ f'๐ŸšŒ {watchdog_and_handler_str} โŒ Browser closed or CDP Connection disconnected by remote. {type(sub_error).__name__}: {sub_error}\n'
154
+ )
155
+ raise
156
+ else:
157
+ browser_session.logger.error(
158
+ f'๐ŸšŒ {watchdog_and_handler_str} โŒ CDP connected but failed to re-create CDP session after error "{type(original_error).__name__}: {original_error}" in {actual_handler.__name__}({event.event_type}#{event.event_id[-4:]}): due to {type(sub_error).__name__}: {sub_error}\n'
159
+ )
160
+
161
+ # Always re-raise the original error with its traceback preserved
162
+ raise
163
+
164
+ return unique_handler
165
+
166
+ unique_handler = make_unique_handler(handler)
167
+ unique_handler.__name__ = f'{watchdog_class_name}.{handler.__name__}'
168
+
169
+ # Check if this handler is already registered - throw error if duplicate
170
+ existing_handlers = event_bus.handlers.get(event_class.__name__, [])
171
+ handler_names = [getattr(h, '__name__', str(h)) for h in existing_handlers]
172
+
173
+ if unique_handler.__name__ in handler_names:
174
+ raise RuntimeError(
175
+ f'[{watchdog_class_name}] Duplicate handler registration attempted! '
176
+ f'Handler {unique_handler.__name__} is already registered for {event_class.__name__}. '
177
+ f'This likely means attach_to_session() was called multiple times.'
178
+ )
179
+
180
+ event_bus.on(event_class, unique_handler)
181
+
182
+ def attach_to_session(self) -> None:
183
+ """Attach watchdog to its browser session and start monitoring.
184
+
185
+ This method handles event listener registration. The watchdog is already
186
+ bound to a browser session via self.browser_session from initialization.
187
+ """
188
+ # Register event handlers automatically based on method names
189
+ assert self.browser_session is not None, 'Root CDP client not initialized - browser may not be connected yet'
190
+
191
+ from browser_use.browser import events
192
+
193
+ event_classes = {}
194
+ for name in dir(events):
195
+ obj = getattr(events, name)
196
+ if inspect.isclass(obj) and issubclass(obj, BaseEvent) and obj is not BaseEvent:
197
+ event_classes[name] = obj
198
+
199
+ # Find all handler methods (on_EventName)
200
+ registered_events = set()
201
+ for method_name in dir(self):
202
+ if method_name.startswith('on_') and callable(getattr(self, method_name)):
203
+ # Extract event name from method name (on_EventName -> EventName)
204
+ event_name = method_name[3:] # Remove 'on_' prefix
205
+
206
+ if event_name in event_classes:
207
+ event_class = event_classes[event_name]
208
+
209
+ # ASSERTION: If LISTENS_TO is defined, enforce it
210
+ if self.LISTENS_TO:
211
+ assert event_class in self.LISTENS_TO, (
212
+ f'[{self.__class__.__name__}] Handler {method_name} listens to {event_name} '
213
+ f'but {event_name} is not declared in LISTENS_TO: {[e.__name__ for e in self.LISTENS_TO]}'
214
+ )
215
+
216
+ handler = getattr(self, method_name)
217
+
218
+ # Use the static helper to attach the handler
219
+ self.attach_handler_to_session(self.browser_session, event_class, handler)
220
+ registered_events.add(event_class)
221
+
222
+ # ASSERTION: If LISTENS_TO is defined, ensure all declared events have handlers
223
+ if self.LISTENS_TO:
224
+ missing_handlers = set(self.LISTENS_TO) - registered_events
225
+ if missing_handlers:
226
+ missing_names = [e.__name__ for e in missing_handlers]
227
+ self.logger.warning(
228
+ f'[{self.__class__.__name__}] LISTENS_TO declares {missing_names} '
229
+ f'but no handlers found (missing on_{"_, on_".join(missing_names)} methods)'
230
+ )
231
+
232
+ def __del__(self) -> None:
233
+ """Clean up any running tasks during garbage collection."""
234
+
235
+ # A BIT OF MAGIC: Cancel any private attributes that look like asyncio tasks
236
+ try:
237
+ for attr_name in dir(self):
238
+ # e.g. _browser_crash_watcher_task = asyncio.Task
239
+ if attr_name.startswith('_') and attr_name.endswith('_task'):
240
+ try:
241
+ task = getattr(self, attr_name)
242
+ if hasattr(task, 'cancel') and callable(task.cancel) and not task.done():
243
+ task.cancel()
244
+ # self.logger.debug(f'[{self.__class__.__name__}] Cancelled {attr_name} during cleanup')
245
+ except Exception:
246
+ pass # Ignore errors during cleanup
247
+
248
+ # e.g. _cdp_download_tasks = WeakSet[asyncio.Task] or list[asyncio.Task]
249
+ if attr_name.startswith('_') and attr_name.endswith('_tasks') and isinstance(getattr(self, attr_name), Iterable):
250
+ for task in getattr(self, attr_name):
251
+ try:
252
+ if hasattr(task, 'cancel') and callable(task.cancel) and not task.done():
253
+ task.cancel()
254
+ # self.logger.debug(f'[{self.__class__.__name__}] Cancelled {attr_name} during cleanup')
255
+ except Exception:
256
+ pass # Ignore errors during cleanup
257
+ except Exception as e:
258
+ from browser_use.utils import logger
259
+
260
+ logger.error(f'โš ๏ธ Error during BrowserSession {self.__class__.__name__} garbage collection __del__(): {type(e)}: {e}')
File without changes