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.
- browser_use/__init__.py +157 -0
- browser_use/actor/__init__.py +11 -0
- browser_use/actor/element.py +1175 -0
- browser_use/actor/mouse.py +134 -0
- browser_use/actor/page.py +561 -0
- browser_use/actor/playground/flights.py +41 -0
- browser_use/actor/playground/mixed_automation.py +54 -0
- browser_use/actor/playground/playground.py +236 -0
- browser_use/actor/utils.py +176 -0
- browser_use/agent/cloud_events.py +282 -0
- browser_use/agent/gif.py +424 -0
- browser_use/agent/judge.py +170 -0
- browser_use/agent/message_manager/service.py +473 -0
- browser_use/agent/message_manager/utils.py +52 -0
- browser_use/agent/message_manager/views.py +98 -0
- browser_use/agent/prompts.py +413 -0
- browser_use/agent/service.py +2316 -0
- browser_use/agent/system_prompt.md +185 -0
- browser_use/agent/system_prompt_flash.md +10 -0
- browser_use/agent/system_prompt_no_thinking.md +183 -0
- browser_use/agent/views.py +743 -0
- browser_use/browser/__init__.py +41 -0
- browser_use/browser/cloud/cloud.py +203 -0
- browser_use/browser/cloud/views.py +89 -0
- browser_use/browser/events.py +578 -0
- browser_use/browser/profile.py +1158 -0
- browser_use/browser/python_highlights.py +548 -0
- browser_use/browser/session.py +3225 -0
- browser_use/browser/session_manager.py +399 -0
- browser_use/browser/video_recorder.py +162 -0
- browser_use/browser/views.py +200 -0
- browser_use/browser/watchdog_base.py +260 -0
- browser_use/browser/watchdogs/__init__.py +0 -0
- browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
- browser_use/browser/watchdogs/crash_watchdog.py +335 -0
- browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
- browser_use/browser/watchdogs/dom_watchdog.py +817 -0
- browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
- browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
- browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
- browser_use/browser/watchdogs/popups_watchdog.py +143 -0
- browser_use/browser/watchdogs/recording_watchdog.py +126 -0
- browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
- browser_use/browser/watchdogs/security_watchdog.py +280 -0
- browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
- browser_use/cli.py +2359 -0
- browser_use/code_use/__init__.py +16 -0
- browser_use/code_use/formatting.py +192 -0
- browser_use/code_use/namespace.py +665 -0
- browser_use/code_use/notebook_export.py +276 -0
- browser_use/code_use/service.py +1340 -0
- browser_use/code_use/system_prompt.md +574 -0
- browser_use/code_use/utils.py +150 -0
- browser_use/code_use/views.py +171 -0
- browser_use/config.py +505 -0
- browser_use/controller/__init__.py +3 -0
- browser_use/dom/enhanced_snapshot.py +161 -0
- browser_use/dom/markdown_extractor.py +169 -0
- browser_use/dom/playground/extraction.py +312 -0
- browser_use/dom/playground/multi_act.py +32 -0
- browser_use/dom/serializer/clickable_elements.py +200 -0
- browser_use/dom/serializer/code_use_serializer.py +287 -0
- browser_use/dom/serializer/eval_serializer.py +478 -0
- browser_use/dom/serializer/html_serializer.py +212 -0
- browser_use/dom/serializer/paint_order.py +197 -0
- browser_use/dom/serializer/serializer.py +1170 -0
- browser_use/dom/service.py +825 -0
- browser_use/dom/utils.py +129 -0
- browser_use/dom/views.py +906 -0
- browser_use/exceptions.py +5 -0
- browser_use/filesystem/__init__.py +0 -0
- browser_use/filesystem/file_system.py +619 -0
- browser_use/init_cmd.py +376 -0
- browser_use/integrations/gmail/__init__.py +24 -0
- browser_use/integrations/gmail/actions.py +115 -0
- browser_use/integrations/gmail/service.py +225 -0
- browser_use/llm/__init__.py +155 -0
- browser_use/llm/anthropic/chat.py +242 -0
- browser_use/llm/anthropic/serializer.py +312 -0
- browser_use/llm/aws/__init__.py +36 -0
- browser_use/llm/aws/chat_anthropic.py +242 -0
- browser_use/llm/aws/chat_bedrock.py +289 -0
- browser_use/llm/aws/serializer.py +257 -0
- browser_use/llm/azure/chat.py +91 -0
- browser_use/llm/base.py +57 -0
- browser_use/llm/browser_use/__init__.py +3 -0
- browser_use/llm/browser_use/chat.py +201 -0
- browser_use/llm/cerebras/chat.py +193 -0
- browser_use/llm/cerebras/serializer.py +109 -0
- browser_use/llm/deepseek/chat.py +212 -0
- browser_use/llm/deepseek/serializer.py +109 -0
- browser_use/llm/exceptions.py +29 -0
- browser_use/llm/google/__init__.py +3 -0
- browser_use/llm/google/chat.py +542 -0
- browser_use/llm/google/serializer.py +120 -0
- browser_use/llm/groq/chat.py +229 -0
- browser_use/llm/groq/parser.py +158 -0
- browser_use/llm/groq/serializer.py +159 -0
- browser_use/llm/messages.py +238 -0
- browser_use/llm/models.py +271 -0
- browser_use/llm/oci_raw/__init__.py +10 -0
- browser_use/llm/oci_raw/chat.py +443 -0
- browser_use/llm/oci_raw/serializer.py +229 -0
- browser_use/llm/ollama/chat.py +97 -0
- browser_use/llm/ollama/serializer.py +143 -0
- browser_use/llm/openai/chat.py +264 -0
- browser_use/llm/openai/like.py +15 -0
- browser_use/llm/openai/serializer.py +165 -0
- browser_use/llm/openrouter/chat.py +211 -0
- browser_use/llm/openrouter/serializer.py +26 -0
- browser_use/llm/schema.py +176 -0
- browser_use/llm/views.py +48 -0
- browser_use/logging_config.py +330 -0
- browser_use/mcp/__init__.py +18 -0
- browser_use/mcp/__main__.py +12 -0
- browser_use/mcp/client.py +544 -0
- browser_use/mcp/controller.py +264 -0
- browser_use/mcp/server.py +1114 -0
- browser_use/observability.py +204 -0
- browser_use/py.typed +0 -0
- browser_use/sandbox/__init__.py +41 -0
- browser_use/sandbox/sandbox.py +637 -0
- browser_use/sandbox/views.py +132 -0
- browser_use/screenshots/__init__.py +1 -0
- browser_use/screenshots/service.py +52 -0
- browser_use/sync/__init__.py +6 -0
- browser_use/sync/auth.py +357 -0
- browser_use/sync/service.py +161 -0
- browser_use/telemetry/__init__.py +51 -0
- browser_use/telemetry/service.py +112 -0
- browser_use/telemetry/views.py +101 -0
- browser_use/tokens/__init__.py +0 -0
- browser_use/tokens/custom_pricing.py +24 -0
- browser_use/tokens/mappings.py +4 -0
- browser_use/tokens/service.py +580 -0
- browser_use/tokens/views.py +108 -0
- browser_use/tools/registry/service.py +572 -0
- browser_use/tools/registry/views.py +174 -0
- browser_use/tools/service.py +1675 -0
- browser_use/tools/utils.py +82 -0
- browser_use/tools/views.py +100 -0
- browser_use/utils.py +670 -0
- optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
- optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
- optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
- optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
- optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from browser_use.dom.views import EnhancedDOMTreeNode, NodeType
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ClickableElementDetector:
|
|
5
|
+
@staticmethod
|
|
6
|
+
def is_interactive(node: EnhancedDOMTreeNode) -> bool:
|
|
7
|
+
"""Check if this node is clickable/interactive using enhanced scoring."""
|
|
8
|
+
|
|
9
|
+
# Skip non-element nodes
|
|
10
|
+
if node.node_type != NodeType.ELEMENT_NODE:
|
|
11
|
+
return False
|
|
12
|
+
|
|
13
|
+
# # if ax ignored skip
|
|
14
|
+
# if node.ax_node and node.ax_node.ignored:
|
|
15
|
+
# return False
|
|
16
|
+
|
|
17
|
+
# remove html and body nodes
|
|
18
|
+
if node.tag_name in {'html', 'body'}:
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
# IFRAME elements should be interactive if they're large enough to potentially need scrolling
|
|
22
|
+
# Small iframes (< 100px width or height) are unlikely to have scrollable content
|
|
23
|
+
if node.tag_name and node.tag_name.upper() == 'IFRAME' or node.tag_name.upper() == 'FRAME':
|
|
24
|
+
if node.snapshot_node and node.snapshot_node.bounds:
|
|
25
|
+
width = node.snapshot_node.bounds.width
|
|
26
|
+
height = node.snapshot_node.bounds.height
|
|
27
|
+
# Only include iframes larger than 100x100px
|
|
28
|
+
if width > 100 and height > 100:
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
# RELAXED SIZE CHECK: Allow all elements including size 0 (they might be interactive overlays, etc.)
|
|
32
|
+
# Note: Size 0 elements can still be interactive (e.g., invisible clickable overlays)
|
|
33
|
+
# Visibility is determined separately by CSS styles, not just bounding box size
|
|
34
|
+
|
|
35
|
+
# SEARCH ELEMENT DETECTION: Check for search-related classes and attributes
|
|
36
|
+
if node.attributes:
|
|
37
|
+
search_indicators = {
|
|
38
|
+
'search',
|
|
39
|
+
'magnify',
|
|
40
|
+
'glass',
|
|
41
|
+
'lookup',
|
|
42
|
+
'find',
|
|
43
|
+
'query',
|
|
44
|
+
'search-icon',
|
|
45
|
+
'search-btn',
|
|
46
|
+
'search-button',
|
|
47
|
+
'searchbox',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Check class names for search indicators
|
|
51
|
+
class_list = node.attributes.get('class', '').lower().split()
|
|
52
|
+
if any(indicator in ' '.join(class_list) for indicator in search_indicators):
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
# Check id for search indicators
|
|
56
|
+
element_id = node.attributes.get('id', '').lower()
|
|
57
|
+
if any(indicator in element_id for indicator in search_indicators):
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
# Check data attributes for search functionality
|
|
61
|
+
for attr_name, attr_value in node.attributes.items():
|
|
62
|
+
if attr_name.startswith('data-') and any(indicator in attr_value.lower() for indicator in search_indicators):
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
# Enhanced accessibility property checks - direct clear indicators only
|
|
66
|
+
if node.ax_node and node.ax_node.properties:
|
|
67
|
+
for prop in node.ax_node.properties:
|
|
68
|
+
try:
|
|
69
|
+
# aria disabled
|
|
70
|
+
if prop.name == 'disabled' and prop.value:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
# aria hidden
|
|
74
|
+
if prop.name == 'hidden' and prop.value:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
# Direct interactiveness indicators
|
|
78
|
+
if prop.name in ['focusable', 'editable', 'settable'] and prop.value:
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
# Interactive state properties (presence indicates interactive widget)
|
|
82
|
+
if prop.name in ['checked', 'expanded', 'pressed', 'selected']:
|
|
83
|
+
# These properties only exist on interactive elements
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
# Form-related interactiveness
|
|
87
|
+
if prop.name in ['required', 'autocomplete'] and prop.value:
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
# Elements with keyboard shortcuts are interactive
|
|
91
|
+
if prop.name == 'keyshortcuts' and prop.value:
|
|
92
|
+
return True
|
|
93
|
+
except (AttributeError, ValueError):
|
|
94
|
+
# Skip properties we can't process
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# ENHANCED TAG CHECK: Include truly interactive elements
|
|
98
|
+
# Note: 'label' removed - labels are handled by other attribute checks below - other wise labels with "for" attribute can destroy the real clickable element on apartments.com
|
|
99
|
+
interactive_tags = {
|
|
100
|
+
'button',
|
|
101
|
+
'input',
|
|
102
|
+
'select',
|
|
103
|
+
'textarea',
|
|
104
|
+
'a',
|
|
105
|
+
'details',
|
|
106
|
+
'summary',
|
|
107
|
+
'option',
|
|
108
|
+
'optgroup',
|
|
109
|
+
}
|
|
110
|
+
# Check with case-insensitive comparison
|
|
111
|
+
if node.tag_name and node.tag_name.lower() in interactive_tags:
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
# SVG elements need special handling - only interactive if they have explicit handlers
|
|
115
|
+
# svg_tags = {'svg', 'path', 'circle', 'rect', 'polygon', 'ellipse', 'line', 'polyline', 'g'}
|
|
116
|
+
# if node.tag_name in svg_tags:
|
|
117
|
+
# # Only consider SVG elements interactive if they have:
|
|
118
|
+
# # 1. Explicit event handlers
|
|
119
|
+
# # 2. Interactive role attributes
|
|
120
|
+
# # 3. Cursor pointer style
|
|
121
|
+
# if node.attributes:
|
|
122
|
+
# # Check for event handlers
|
|
123
|
+
# if any(attr.startswith('on') for attr in node.attributes):
|
|
124
|
+
# return True
|
|
125
|
+
# # Check for interactive roles
|
|
126
|
+
# if node.attributes.get('role') in {'button', 'link', 'menuitem'}:
|
|
127
|
+
# return True
|
|
128
|
+
# # Check for cursor pointer (indicating clickability)
|
|
129
|
+
# if node.attributes.get('style') and 'cursor: pointer' in node.attributes.get('style', ''):
|
|
130
|
+
# return True
|
|
131
|
+
# # Otherwise, SVG elements are decorative
|
|
132
|
+
# return False
|
|
133
|
+
|
|
134
|
+
# Tertiary check: elements with interactive attributes
|
|
135
|
+
if node.attributes:
|
|
136
|
+
# Check for event handlers or interactive attributes
|
|
137
|
+
interactive_attributes = {'onclick', 'onmousedown', 'onmouseup', 'onkeydown', 'onkeyup', 'tabindex'}
|
|
138
|
+
if any(attr in node.attributes for attr in interactive_attributes):
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
# Check for interactive ARIA roles
|
|
142
|
+
if 'role' in node.attributes:
|
|
143
|
+
interactive_roles = {
|
|
144
|
+
'button',
|
|
145
|
+
'link',
|
|
146
|
+
'menuitem',
|
|
147
|
+
'option',
|
|
148
|
+
'radio',
|
|
149
|
+
'checkbox',
|
|
150
|
+
'tab',
|
|
151
|
+
'textbox',
|
|
152
|
+
'combobox',
|
|
153
|
+
'slider',
|
|
154
|
+
'spinbutton',
|
|
155
|
+
'search',
|
|
156
|
+
'searchbox',
|
|
157
|
+
}
|
|
158
|
+
if node.attributes['role'] in interactive_roles:
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
# Quaternary check: accessibility tree roles
|
|
162
|
+
if node.ax_node and node.ax_node.role:
|
|
163
|
+
interactive_ax_roles = {
|
|
164
|
+
'button',
|
|
165
|
+
'link',
|
|
166
|
+
'menuitem',
|
|
167
|
+
'option',
|
|
168
|
+
'radio',
|
|
169
|
+
'checkbox',
|
|
170
|
+
'tab',
|
|
171
|
+
'textbox',
|
|
172
|
+
'combobox',
|
|
173
|
+
'slider',
|
|
174
|
+
'spinbutton',
|
|
175
|
+
'listbox',
|
|
176
|
+
'search',
|
|
177
|
+
'searchbox',
|
|
178
|
+
}
|
|
179
|
+
if node.ax_node.role in interactive_ax_roles:
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
# ICON AND SMALL ELEMENT CHECK: Elements that might be icons
|
|
183
|
+
if (
|
|
184
|
+
node.snapshot_node
|
|
185
|
+
and node.snapshot_node.bounds
|
|
186
|
+
and 10 <= node.snapshot_node.bounds.width <= 50 # Icon-sized elements
|
|
187
|
+
and 10 <= node.snapshot_node.bounds.height <= 50
|
|
188
|
+
):
|
|
189
|
+
# Check if this small element has interactive properties
|
|
190
|
+
if node.attributes:
|
|
191
|
+
# Small elements with these attributes are likely interactive icons
|
|
192
|
+
icon_attributes = {'class', 'role', 'onclick', 'data-action', 'aria-label'}
|
|
193
|
+
if any(attr in node.attributes for attr in icon_attributes):
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
# Final fallback: cursor style indicates interactivity (for cases Chrome missed)
|
|
197
|
+
if node.snapshot_node and node.snapshot_node.cursor_style and node.snapshot_node.cursor_style == 'pointer':
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
return False
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# @file purpose: Ultra-compact serializer optimized for code-use agents
|
|
2
|
+
# Focuses on minimal token usage while preserving essential interactive context
|
|
3
|
+
|
|
4
|
+
from browser_use.dom.utils import cap_text_length
|
|
5
|
+
from browser_use.dom.views import (
|
|
6
|
+
EnhancedDOMTreeNode,
|
|
7
|
+
NodeType,
|
|
8
|
+
SimplifiedNode,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# Minimal but sufficient attribute list for code agents
|
|
12
|
+
CODE_USE_KEY_ATTRIBUTES = [
|
|
13
|
+
'id', # Essential for element selection
|
|
14
|
+
'name', # For form inputs
|
|
15
|
+
'type', # For input types
|
|
16
|
+
'placeholder', # For empty inputs
|
|
17
|
+
'aria-label', # For buttons without text
|
|
18
|
+
'value', # Current values
|
|
19
|
+
'alt', # For images
|
|
20
|
+
'class', # Keep top 2 classes for common selectors
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# Interactive elements agent can use
|
|
24
|
+
INTERACTIVE_ELEMENTS = {
|
|
25
|
+
'a',
|
|
26
|
+
'button',
|
|
27
|
+
'input',
|
|
28
|
+
'textarea',
|
|
29
|
+
'select',
|
|
30
|
+
'form',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Semantic structure elements - expanded to include more content containers
|
|
34
|
+
SEMANTIC_STRUCTURE = {
|
|
35
|
+
'h1',
|
|
36
|
+
'h2',
|
|
37
|
+
'h3',
|
|
38
|
+
'h4',
|
|
39
|
+
'h5',
|
|
40
|
+
'h6',
|
|
41
|
+
'nav',
|
|
42
|
+
'main',
|
|
43
|
+
'header',
|
|
44
|
+
'footer',
|
|
45
|
+
'article',
|
|
46
|
+
'section',
|
|
47
|
+
'p', # Paragraphs often contain prices and product info
|
|
48
|
+
'span', # Spans often contain prices and labels
|
|
49
|
+
'div', # Divs with useful attributes (id/class) should be shown
|
|
50
|
+
'ul',
|
|
51
|
+
'ol',
|
|
52
|
+
'li',
|
|
53
|
+
'label',
|
|
54
|
+
'img',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DOMCodeAgentSerializer:
|
|
59
|
+
"""Optimized DOM serializer for code-use agents - balances token efficiency with context."""
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def serialize_tree(node: SimplifiedNode | None, include_attributes: list[str], depth: int = 0) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Serialize DOM tree with smart token optimization.
|
|
65
|
+
|
|
66
|
+
Strategy:
|
|
67
|
+
- Keep top 2 CSS classes for querySelector compatibility
|
|
68
|
+
- Show div/span/p elements with useful attributes or text
|
|
69
|
+
- Show all interactive + semantic elements
|
|
70
|
+
- Inline text up to 80 chars for better context
|
|
71
|
+
"""
|
|
72
|
+
if not node:
|
|
73
|
+
return ''
|
|
74
|
+
|
|
75
|
+
# Skip excluded/hidden nodes
|
|
76
|
+
if hasattr(node, 'excluded_by_parent') and node.excluded_by_parent:
|
|
77
|
+
return DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth)
|
|
78
|
+
|
|
79
|
+
if not node.should_display:
|
|
80
|
+
return DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth)
|
|
81
|
+
|
|
82
|
+
formatted_text = []
|
|
83
|
+
depth_str = ' ' * depth # Use 2 spaces instead of tabs for compactness
|
|
84
|
+
|
|
85
|
+
if node.original_node.node_type == NodeType.ELEMENT_NODE:
|
|
86
|
+
tag = node.original_node.tag_name.lower()
|
|
87
|
+
is_visible = node.original_node.snapshot_node and node.original_node.is_visible
|
|
88
|
+
|
|
89
|
+
# Skip invisible (except iframes)
|
|
90
|
+
if not is_visible and tag not in ['iframe', 'frame']:
|
|
91
|
+
return DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth)
|
|
92
|
+
|
|
93
|
+
# Special handling for iframes
|
|
94
|
+
if tag in ['iframe', 'frame']:
|
|
95
|
+
return DOMCodeAgentSerializer._serialize_iframe(node, include_attributes, depth)
|
|
96
|
+
|
|
97
|
+
# Build minimal attributes
|
|
98
|
+
attributes_str = DOMCodeAgentSerializer._build_minimal_attributes(node.original_node)
|
|
99
|
+
|
|
100
|
+
# Decide if element should be shown
|
|
101
|
+
is_interactive = tag in INTERACTIVE_ELEMENTS
|
|
102
|
+
is_semantic = tag in SEMANTIC_STRUCTURE
|
|
103
|
+
has_useful_attrs = bool(attributes_str)
|
|
104
|
+
has_text = DOMCodeAgentSerializer._has_direct_text(node)
|
|
105
|
+
|
|
106
|
+
# Skip non-semantic, non-interactive containers without attributes
|
|
107
|
+
if not is_interactive and not is_semantic and not has_useful_attrs and not has_text:
|
|
108
|
+
return DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth)
|
|
109
|
+
|
|
110
|
+
# Collapse pointless wrappers
|
|
111
|
+
if tag in {'div', 'span'} and not has_useful_attrs and not has_text and len(node.children) == 1:
|
|
112
|
+
return DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth)
|
|
113
|
+
|
|
114
|
+
# Build element
|
|
115
|
+
line = f'{depth_str}<{tag}'
|
|
116
|
+
|
|
117
|
+
if attributes_str:
|
|
118
|
+
line += f' {attributes_str}'
|
|
119
|
+
|
|
120
|
+
# Inline text
|
|
121
|
+
inline_text = DOMCodeAgentSerializer._get_inline_text(node)
|
|
122
|
+
if inline_text:
|
|
123
|
+
line += f'>{inline_text}'
|
|
124
|
+
else:
|
|
125
|
+
line += '>'
|
|
126
|
+
|
|
127
|
+
formatted_text.append(line)
|
|
128
|
+
|
|
129
|
+
# Children (only if no inline text)
|
|
130
|
+
if node.children and not inline_text:
|
|
131
|
+
children_text = DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth + 1)
|
|
132
|
+
if children_text:
|
|
133
|
+
formatted_text.append(children_text)
|
|
134
|
+
|
|
135
|
+
elif node.original_node.node_type == NodeType.TEXT_NODE:
|
|
136
|
+
# Handled inline with parent
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
elif node.original_node.node_type == NodeType.DOCUMENT_FRAGMENT_NODE:
|
|
140
|
+
# Shadow DOM - minimal marker
|
|
141
|
+
if node.children:
|
|
142
|
+
formatted_text.append(f'{depth_str}#shadow')
|
|
143
|
+
children_text = DOMCodeAgentSerializer._serialize_children(node, include_attributes, depth + 1)
|
|
144
|
+
if children_text:
|
|
145
|
+
formatted_text.append(children_text)
|
|
146
|
+
|
|
147
|
+
return '\n'.join(formatted_text)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _serialize_children(node: SimplifiedNode, include_attributes: list[str], depth: int) -> str:
|
|
151
|
+
"""Serialize children."""
|
|
152
|
+
children_output = []
|
|
153
|
+
for child in node.children:
|
|
154
|
+
child_text = DOMCodeAgentSerializer.serialize_tree(child, include_attributes, depth)
|
|
155
|
+
if child_text:
|
|
156
|
+
children_output.append(child_text)
|
|
157
|
+
return '\n'.join(children_output)
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _build_minimal_attributes(node: EnhancedDOMTreeNode) -> str:
|
|
161
|
+
"""Build minimal but useful attributes - keep top 2 classes for selectors."""
|
|
162
|
+
attrs = []
|
|
163
|
+
|
|
164
|
+
if node.attributes:
|
|
165
|
+
for attr in CODE_USE_KEY_ATTRIBUTES:
|
|
166
|
+
if attr in node.attributes:
|
|
167
|
+
value = str(node.attributes[attr]).strip()
|
|
168
|
+
if value:
|
|
169
|
+
# Special handling for class - keep only first 2 classes
|
|
170
|
+
if attr == 'class':
|
|
171
|
+
classes = value.split()[:2]
|
|
172
|
+
value = ' '.join(classes)
|
|
173
|
+
# Cap at 25 chars
|
|
174
|
+
value = cap_text_length(value, 25)
|
|
175
|
+
attrs.append(f'{attr}="{value}"')
|
|
176
|
+
|
|
177
|
+
return ' '.join(attrs)
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def _has_direct_text(node: SimplifiedNode) -> bool:
|
|
181
|
+
"""Check if node has direct text children."""
|
|
182
|
+
for child in node.children:
|
|
183
|
+
if child.original_node.node_type == NodeType.TEXT_NODE:
|
|
184
|
+
text = child.original_node.node_value.strip() if child.original_node.node_value else ''
|
|
185
|
+
if len(text) > 1:
|
|
186
|
+
return True
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def _get_inline_text(node: SimplifiedNode) -> str:
|
|
191
|
+
"""Get inline text (max 80 chars for better context)."""
|
|
192
|
+
text_parts = []
|
|
193
|
+
for child in node.children:
|
|
194
|
+
if child.original_node.node_type == NodeType.TEXT_NODE:
|
|
195
|
+
text = child.original_node.node_value.strip() if child.original_node.node_value else ''
|
|
196
|
+
if text and len(text) > 1:
|
|
197
|
+
text_parts.append(text)
|
|
198
|
+
|
|
199
|
+
if not text_parts:
|
|
200
|
+
return ''
|
|
201
|
+
|
|
202
|
+
combined = ' '.join(text_parts)
|
|
203
|
+
return cap_text_length(combined, 40)
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _serialize_iframe(node: SimplifiedNode, include_attributes: list[str], depth: int) -> str:
|
|
207
|
+
"""Handle iframe minimally."""
|
|
208
|
+
formatted_text = []
|
|
209
|
+
depth_str = ' ' * depth
|
|
210
|
+
tag = node.original_node.tag_name.lower()
|
|
211
|
+
|
|
212
|
+
# Minimal iframe marker
|
|
213
|
+
attributes_str = DOMCodeAgentSerializer._build_minimal_attributes(node.original_node)
|
|
214
|
+
line = f'{depth_str}<{tag}'
|
|
215
|
+
if attributes_str:
|
|
216
|
+
line += f' {attributes_str}'
|
|
217
|
+
line += '>'
|
|
218
|
+
formatted_text.append(line)
|
|
219
|
+
|
|
220
|
+
# Iframe content
|
|
221
|
+
if node.original_node.content_document:
|
|
222
|
+
formatted_text.append(f'{depth_str} #iframe-content')
|
|
223
|
+
|
|
224
|
+
# Find and serialize body content only
|
|
225
|
+
for child_node in node.original_node.content_document.children_nodes or []:
|
|
226
|
+
if child_node.tag_name.lower() == 'html':
|
|
227
|
+
for html_child in child_node.children:
|
|
228
|
+
if html_child.tag_name.lower() == 'body':
|
|
229
|
+
for body_child in html_child.children:
|
|
230
|
+
DOMCodeAgentSerializer._serialize_document_node(
|
|
231
|
+
body_child, formatted_text, include_attributes, depth + 2
|
|
232
|
+
)
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
return '\n'.join(formatted_text)
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def _serialize_document_node(
|
|
239
|
+
dom_node: EnhancedDOMTreeNode, output: list[str], include_attributes: list[str], depth: int
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Serialize document node without SimplifiedNode wrapper."""
|
|
242
|
+
depth_str = ' ' * depth
|
|
243
|
+
|
|
244
|
+
if dom_node.node_type == NodeType.ELEMENT_NODE:
|
|
245
|
+
tag = dom_node.tag_name.lower()
|
|
246
|
+
|
|
247
|
+
# Skip invisible
|
|
248
|
+
is_visible = dom_node.snapshot_node and dom_node.is_visible
|
|
249
|
+
if not is_visible:
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
# Check if worth showing
|
|
253
|
+
is_interactive = tag in INTERACTIVE_ELEMENTS
|
|
254
|
+
is_semantic = tag in SEMANTIC_STRUCTURE
|
|
255
|
+
attributes_str = DOMCodeAgentSerializer._build_minimal_attributes(dom_node)
|
|
256
|
+
|
|
257
|
+
if not is_interactive and not is_semantic and not attributes_str:
|
|
258
|
+
# Skip but process children
|
|
259
|
+
for child in dom_node.children:
|
|
260
|
+
DOMCodeAgentSerializer._serialize_document_node(child, output, include_attributes, depth)
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
# Build element
|
|
264
|
+
line = f'{depth_str}<{tag}'
|
|
265
|
+
if attributes_str:
|
|
266
|
+
line += f' {attributes_str}'
|
|
267
|
+
|
|
268
|
+
# Get text
|
|
269
|
+
text_parts = []
|
|
270
|
+
for child in dom_node.children:
|
|
271
|
+
if child.node_type == NodeType.TEXT_NODE and child.node_value:
|
|
272
|
+
text = child.node_value.strip()
|
|
273
|
+
if text and len(text) > 1:
|
|
274
|
+
text_parts.append(text)
|
|
275
|
+
|
|
276
|
+
if text_parts:
|
|
277
|
+
combined = ' '.join(text_parts)
|
|
278
|
+
line += f'>{cap_text_length(combined, 25)}'
|
|
279
|
+
else:
|
|
280
|
+
line += '>'
|
|
281
|
+
|
|
282
|
+
output.append(line)
|
|
283
|
+
|
|
284
|
+
# Process non-text children
|
|
285
|
+
for child in dom_node.children:
|
|
286
|
+
if child.node_type != NodeType.TEXT_NODE:
|
|
287
|
+
DOMCodeAgentSerializer._serialize_document_node(child, output, include_attributes, depth + 1)
|