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,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared markdown extraction utilities for browser content processing.
|
|
3
|
+
|
|
4
|
+
This module provides a unified interface for extracting clean markdown from browser content,
|
|
5
|
+
used by both the tools service and page actor.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from browser_use.dom.serializer.html_serializer import HTMLSerializer
|
|
12
|
+
from browser_use.dom.service import DomService
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from browser_use.browser.session import BrowserSession
|
|
16
|
+
from browser_use.browser.watchdogs.dom_watchdog import DOMWatchdog
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def extract_clean_markdown(
|
|
20
|
+
browser_session: 'BrowserSession | None' = None,
|
|
21
|
+
dom_service: DomService | None = None,
|
|
22
|
+
target_id: str | None = None,
|
|
23
|
+
extract_links: bool = False,
|
|
24
|
+
) -> tuple[str, dict[str, Any]]:
|
|
25
|
+
"""Extract clean markdown from browser content using enhanced DOM tree.
|
|
26
|
+
|
|
27
|
+
This unified function can extract markdown using either a browser session (for tools service)
|
|
28
|
+
or a DOM service with target ID (for page actor).
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
browser_session: Browser session to extract content from (tools service path)
|
|
32
|
+
dom_service: DOM service instance (page actor path)
|
|
33
|
+
target_id: Target ID for the page (required when using dom_service)
|
|
34
|
+
extract_links: Whether to preserve links in markdown
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
tuple: (clean_markdown_content, content_statistics)
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ValueError: If neither browser_session nor (dom_service + target_id) are provided
|
|
41
|
+
"""
|
|
42
|
+
# Validate input parameters
|
|
43
|
+
if browser_session is not None:
|
|
44
|
+
if dom_service is not None or target_id is not None:
|
|
45
|
+
raise ValueError('Cannot specify both browser_session and dom_service/target_id')
|
|
46
|
+
# Browser session path (tools service)
|
|
47
|
+
enhanced_dom_tree = await _get_enhanced_dom_tree_from_browser_session(browser_session)
|
|
48
|
+
current_url = await browser_session.get_current_page_url()
|
|
49
|
+
method = 'enhanced_dom_tree'
|
|
50
|
+
elif dom_service is not None and target_id is not None:
|
|
51
|
+
# DOM service path (page actor)
|
|
52
|
+
enhanced_dom_tree = await dom_service.get_dom_tree(target_id=target_id)
|
|
53
|
+
current_url = None # Not available via DOM service
|
|
54
|
+
method = 'dom_service'
|
|
55
|
+
else:
|
|
56
|
+
raise ValueError('Must provide either browser_session or both dom_service and target_id')
|
|
57
|
+
|
|
58
|
+
# Use the HTML serializer with the enhanced DOM tree
|
|
59
|
+
html_serializer = HTMLSerializer(extract_links=extract_links)
|
|
60
|
+
page_html = html_serializer.serialize(enhanced_dom_tree)
|
|
61
|
+
|
|
62
|
+
original_html_length = len(page_html)
|
|
63
|
+
|
|
64
|
+
# Use markdownify for clean markdown conversion
|
|
65
|
+
from markdownify import markdownify as md
|
|
66
|
+
|
|
67
|
+
content = md(
|
|
68
|
+
page_html,
|
|
69
|
+
heading_style='ATX', # Use # style headings
|
|
70
|
+
strip=['script', 'style'], # Remove these tags
|
|
71
|
+
bullets='-', # Use - for unordered lists
|
|
72
|
+
code_language='', # Don't add language to code blocks
|
|
73
|
+
escape_asterisks=False, # Don't escape asterisks (cleaner output)
|
|
74
|
+
escape_underscores=False, # Don't escape underscores (cleaner output)
|
|
75
|
+
escape_misc=False, # Don't escape other characters (cleaner output)
|
|
76
|
+
autolinks=False, # Don't convert URLs to <> format
|
|
77
|
+
default_title=False, # Don't add default title attributes
|
|
78
|
+
keep_inline_images_in=[], # Don't keep inline images in any tags (we already filter base64 in HTML)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
initial_markdown_length = len(content)
|
|
82
|
+
|
|
83
|
+
# Minimal cleanup - markdownify already does most of the work
|
|
84
|
+
content = re.sub(r'%[0-9A-Fa-f]{2}', '', content) # Remove any remaining URL encoding
|
|
85
|
+
|
|
86
|
+
# Apply light preprocessing to clean up excessive whitespace
|
|
87
|
+
content, chars_filtered = _preprocess_markdown_content(content)
|
|
88
|
+
|
|
89
|
+
final_filtered_length = len(content)
|
|
90
|
+
|
|
91
|
+
# Content statistics
|
|
92
|
+
stats = {
|
|
93
|
+
'method': method,
|
|
94
|
+
'original_html_chars': original_html_length,
|
|
95
|
+
'initial_markdown_chars': initial_markdown_length,
|
|
96
|
+
'filtered_chars_removed': chars_filtered,
|
|
97
|
+
'final_filtered_chars': final_filtered_length,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Add URL to stats if available
|
|
101
|
+
if current_url:
|
|
102
|
+
stats['url'] = current_url
|
|
103
|
+
|
|
104
|
+
return content, stats
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def _get_enhanced_dom_tree_from_browser_session(browser_session: 'BrowserSession'):
|
|
108
|
+
"""Get enhanced DOM tree from browser session via DOMWatchdog."""
|
|
109
|
+
# Get the enhanced DOM tree from DOMWatchdog
|
|
110
|
+
# This captures the current state of the page including dynamic content, shadow roots, etc.
|
|
111
|
+
dom_watchdog: DOMWatchdog | None = browser_session._dom_watchdog
|
|
112
|
+
assert dom_watchdog is not None, 'DOMWatchdog not available'
|
|
113
|
+
|
|
114
|
+
# Use cached enhanced DOM tree if available, otherwise build it
|
|
115
|
+
if dom_watchdog.enhanced_dom_tree is not None:
|
|
116
|
+
return dom_watchdog.enhanced_dom_tree
|
|
117
|
+
|
|
118
|
+
# Build the enhanced DOM tree if not cached
|
|
119
|
+
await dom_watchdog._build_dom_tree_without_highlights()
|
|
120
|
+
enhanced_dom_tree = dom_watchdog.enhanced_dom_tree
|
|
121
|
+
assert enhanced_dom_tree is not None, 'Enhanced DOM tree not available'
|
|
122
|
+
|
|
123
|
+
return enhanced_dom_tree
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Legacy aliases removed - all code now uses the unified extract_clean_markdown function
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _preprocess_markdown_content(content: str, max_newlines: int = 3) -> tuple[str, int]:
|
|
130
|
+
"""
|
|
131
|
+
Light preprocessing of markdown output - minimal cleanup with JSON blob removal.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
content: Markdown content to lightly filter
|
|
135
|
+
max_newlines: Maximum consecutive newlines to allow
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
tuple: (filtered_content, chars_filtered)
|
|
139
|
+
"""
|
|
140
|
+
original_length = len(content)
|
|
141
|
+
|
|
142
|
+
# Remove JSON blobs (common in SPAs like LinkedIn, Facebook, etc.)
|
|
143
|
+
# These are often embedded as `{"key":"value",...}` and can be massive
|
|
144
|
+
# Match JSON objects/arrays that are at least 100 chars long
|
|
145
|
+
# This catches SPA state/config data without removing small inline JSON
|
|
146
|
+
content = re.sub(r'`\{["\w].*?\}`', '', content, flags=re.DOTALL) # Remove JSON in code blocks
|
|
147
|
+
content = re.sub(r'\{"\$type":[^}]{100,}\}', '', content) # Remove JSON with $type fields (common pattern)
|
|
148
|
+
content = re.sub(r'\{"[^"]{5,}":\{[^}]{100,}\}', '', content) # Remove nested JSON objects
|
|
149
|
+
|
|
150
|
+
# Compress consecutive newlines (4+ newlines become max_newlines)
|
|
151
|
+
content = re.sub(r'\n{4,}', '\n' * max_newlines, content)
|
|
152
|
+
|
|
153
|
+
# Remove lines that are only whitespace or very short (likely artifacts)
|
|
154
|
+
lines = content.split('\n')
|
|
155
|
+
filtered_lines = []
|
|
156
|
+
for line in lines:
|
|
157
|
+
stripped = line.strip()
|
|
158
|
+
# Keep lines with substantial content
|
|
159
|
+
if len(stripped) > 2:
|
|
160
|
+
# Skip lines that look like JSON (start with { or [ and are very long)
|
|
161
|
+
if (stripped.startswith('{') or stripped.startswith('[')) and len(stripped) > 100:
|
|
162
|
+
continue
|
|
163
|
+
filtered_lines.append(line)
|
|
164
|
+
|
|
165
|
+
content = '\n'.join(filtered_lines)
|
|
166
|
+
content = content.strip()
|
|
167
|
+
|
|
168
|
+
chars_filtered = original_length - len(content)
|
|
169
|
+
return content, chars_filtered
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
import anyio
|
|
7
|
+
import pyperclip
|
|
8
|
+
import tiktoken
|
|
9
|
+
|
|
10
|
+
from browser_use.agent.prompts import AgentMessagePrompt
|
|
11
|
+
from browser_use.browser import BrowserProfile, BrowserSession
|
|
12
|
+
from browser_use.browser.events import ClickElementEvent, TypeTextEvent
|
|
13
|
+
from browser_use.browser.profile import ViewportSize
|
|
14
|
+
from browser_use.dom.service import DomService
|
|
15
|
+
from browser_use.dom.views import DEFAULT_INCLUDE_ATTRIBUTES
|
|
16
|
+
from browser_use.filesystem.file_system import FileSystem
|
|
17
|
+
|
|
18
|
+
TIMEOUT = 60
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def test_focus_vs_all_elements():
|
|
22
|
+
browser_session = BrowserSession(
|
|
23
|
+
browser_profile=BrowserProfile(
|
|
24
|
+
# executable_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
25
|
+
window_size=ViewportSize(width=1100, height=1000),
|
|
26
|
+
disable_security=False,
|
|
27
|
+
wait_for_network_idle_page_load_time=1,
|
|
28
|
+
headless=False,
|
|
29
|
+
args=['--incognito'],
|
|
30
|
+
paint_order_filtering=True,
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# 10 Sample websites with various interactive elements
|
|
35
|
+
sample_websites = [
|
|
36
|
+
'https://browser-use.github.io/stress-tests/challenges/iframe-inception-level2.html',
|
|
37
|
+
'https://www.google.com/travel/flights',
|
|
38
|
+
'https://v0-simple-ui-test-site.vercel.app',
|
|
39
|
+
'https://browser-use.github.io/stress-tests/challenges/iframe-inception-level1.html',
|
|
40
|
+
'https://browser-use.github.io/stress-tests/challenges/angular-form.html',
|
|
41
|
+
'https://www.google.com/travel/flights',
|
|
42
|
+
'https://www.amazon.com/s?k=laptop',
|
|
43
|
+
'https://github.com/trending',
|
|
44
|
+
'https://www.reddit.com',
|
|
45
|
+
'https://www.ycombinator.com/companies',
|
|
46
|
+
'https://www.kayak.com/flights',
|
|
47
|
+
'https://www.booking.com',
|
|
48
|
+
'https://www.airbnb.com',
|
|
49
|
+
'https://www.linkedin.com/jobs',
|
|
50
|
+
'https://stackoverflow.com/questions',
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# 5 Difficult websites with complex elements (iframes, canvas, dropdowns, etc.)
|
|
54
|
+
difficult_websites = [
|
|
55
|
+
'https://www.w3schools.com/html/tryit.asp?filename=tryhtml_iframe', # Nested iframes
|
|
56
|
+
'https://semantic-ui.com/modules/dropdown.html', # Complex dropdowns
|
|
57
|
+
'https://www.dezlearn.com/nested-iframes-example/', # Cross-origin nested iframes
|
|
58
|
+
'https://codepen.io/towc/pen/mJzOWJ', # Canvas elements with interactions
|
|
59
|
+
'https://jqueryui.com/accordion/', # Complex accordion/dropdown widgets
|
|
60
|
+
'https://v0-simple-landing-page-seven-xi.vercel.app/', # Simple landing page with iframe
|
|
61
|
+
'https://www.unesco.org/en',
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# Descriptions for difficult websites
|
|
65
|
+
difficult_descriptions = {
|
|
66
|
+
'https://www.w3schools.com/html/tryit.asp?filename=tryhtml_iframe': '🔸 NESTED IFRAMES: Multiple iframe layers',
|
|
67
|
+
'https://semantic-ui.com/modules/dropdown.html': '🔸 COMPLEX DROPDOWNS: Custom dropdown components',
|
|
68
|
+
'https://www.dezlearn.com/nested-iframes-example/': '🔸 CROSS-ORIGIN IFRAMES: Different domain iframes',
|
|
69
|
+
'https://codepen.io/towc/pen/mJzOWJ': '🔸 CANVAS ELEMENTS: Interactive canvas graphics',
|
|
70
|
+
'https://jqueryui.com/accordion/': '🔸 ACCORDION WIDGETS: Collapsible content sections',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
websites = sample_websites + difficult_websites
|
|
74
|
+
current_website_index = 0
|
|
75
|
+
|
|
76
|
+
def get_website_list_for_prompt() -> str:
|
|
77
|
+
"""Get a compact website list for the input prompt."""
|
|
78
|
+
lines = []
|
|
79
|
+
lines.append('📋 Websites:')
|
|
80
|
+
|
|
81
|
+
# Sample websites (1-10)
|
|
82
|
+
for i, site in enumerate(sample_websites, 1):
|
|
83
|
+
current_marker = ' ←' if (i - 1) == current_website_index else ''
|
|
84
|
+
domain = site.replace('https://', '').split('/')[0]
|
|
85
|
+
lines.append(f' {i:2d}.{domain[:15]:<15}{current_marker}')
|
|
86
|
+
|
|
87
|
+
# Difficult websites (11-15)
|
|
88
|
+
for i, site in enumerate(difficult_websites, len(sample_websites) + 1):
|
|
89
|
+
current_marker = ' ←' if (i - 1) == current_website_index else ''
|
|
90
|
+
domain = site.replace('https://', '').split('/')[0]
|
|
91
|
+
desc = difficult_descriptions.get(site, '')
|
|
92
|
+
challenge = desc.split(': ')[1][:15] if ': ' in desc else ''
|
|
93
|
+
lines.append(f' {i:2d}.{domain[:15]:<15} ({challenge}){current_marker}')
|
|
94
|
+
|
|
95
|
+
return '\n'.join(lines)
|
|
96
|
+
|
|
97
|
+
await browser_session.start()
|
|
98
|
+
|
|
99
|
+
# Show startup info
|
|
100
|
+
print('\n🌐 BROWSER-USE DOM EXTRACTION TESTER')
|
|
101
|
+
print(f'📊 {len(websites)} websites total: {len(sample_websites)} standard + {len(difficult_websites)} complex')
|
|
102
|
+
print('🔧 Controls: Type 1-15 to jump | Enter to re-run | "n" next | "q" quit')
|
|
103
|
+
print('💾 Outputs: tmp/user_message.txt & tmp/element_tree.json\n')
|
|
104
|
+
|
|
105
|
+
dom_service = DomService(browser_session)
|
|
106
|
+
|
|
107
|
+
while True:
|
|
108
|
+
# Cycle through websites
|
|
109
|
+
if current_website_index >= len(websites):
|
|
110
|
+
current_website_index = 0
|
|
111
|
+
print('Cycled back to first website!')
|
|
112
|
+
|
|
113
|
+
website = websites[current_website_index]
|
|
114
|
+
# sleep 2
|
|
115
|
+
await browser_session._cdp_navigate(website)
|
|
116
|
+
await asyncio.sleep(1)
|
|
117
|
+
|
|
118
|
+
last_clicked_index = None # Track the index for text input
|
|
119
|
+
while True:
|
|
120
|
+
try:
|
|
121
|
+
# all_elements_state = await dom_service.get_serialized_dom_tree()
|
|
122
|
+
|
|
123
|
+
website_type = 'DIFFICULT' if website in difficult_websites else 'SAMPLE'
|
|
124
|
+
print(f'\n{"=" * 60}')
|
|
125
|
+
print(f'[{current_website_index + 1}/{len(websites)}] [{website_type}] Testing: {website}')
|
|
126
|
+
if website in difficult_descriptions:
|
|
127
|
+
print(f'{difficult_descriptions[website]}')
|
|
128
|
+
print(f'{"=" * 60}')
|
|
129
|
+
|
|
130
|
+
# Get/refresh the state (includes removing old highlights)
|
|
131
|
+
print('\nGetting page state...')
|
|
132
|
+
|
|
133
|
+
start_time = time.time()
|
|
134
|
+
all_elements_state = await browser_session.get_browser_state_summary(True)
|
|
135
|
+
end_time = time.time()
|
|
136
|
+
get_state_time = end_time - start_time
|
|
137
|
+
print(f'get_state_summary took {get_state_time:.2f} seconds')
|
|
138
|
+
|
|
139
|
+
# Get detailed timing info from DOM service
|
|
140
|
+
print('\nGetting detailed DOM timing...')
|
|
141
|
+
serialized_state, _, timing_info = await dom_service.get_serialized_dom_tree()
|
|
142
|
+
|
|
143
|
+
# Combine all timing info
|
|
144
|
+
all_timing = {'get_state_summary_total': get_state_time, **timing_info}
|
|
145
|
+
|
|
146
|
+
selector_map = all_elements_state.dom_state.selector_map
|
|
147
|
+
total_elements = len(selector_map.keys())
|
|
148
|
+
print(f'Total number of elements: {total_elements}')
|
|
149
|
+
|
|
150
|
+
# print(all_elements_state.element_tree.clickable_elements_to_string())
|
|
151
|
+
prompt = AgentMessagePrompt(
|
|
152
|
+
browser_state_summary=all_elements_state,
|
|
153
|
+
file_system=FileSystem(base_dir='./tmp'),
|
|
154
|
+
include_attributes=DEFAULT_INCLUDE_ATTRIBUTES,
|
|
155
|
+
step_info=None,
|
|
156
|
+
)
|
|
157
|
+
# Write the user message to a file for analysis
|
|
158
|
+
user_message = prompt.get_user_message(use_vision=False).text
|
|
159
|
+
|
|
160
|
+
# clickable_elements_str = all_elements_state.element_tree.clickable_elements_to_string()
|
|
161
|
+
|
|
162
|
+
text_to_save = user_message
|
|
163
|
+
|
|
164
|
+
os.makedirs('./tmp', exist_ok=True)
|
|
165
|
+
async with await anyio.open_file('./tmp/user_message.txt', 'w', encoding='utf-8') as f:
|
|
166
|
+
await f.write(text_to_save)
|
|
167
|
+
|
|
168
|
+
# save pure clickable elements to a file
|
|
169
|
+
if all_elements_state.dom_state._root:
|
|
170
|
+
async with await anyio.open_file('./tmp/simplified_element_tree.json', 'w', encoding='utf-8') as f:
|
|
171
|
+
await f.write(json.dumps(all_elements_state.dom_state._root.__json__(), indent=2))
|
|
172
|
+
|
|
173
|
+
async with await anyio.open_file('./tmp/original_element_tree.json', 'w', encoding='utf-8') as f:
|
|
174
|
+
await f.write(json.dumps(all_elements_state.dom_state._root.original_node.__json__(), indent=2))
|
|
175
|
+
|
|
176
|
+
# copy the user message to the clipboard
|
|
177
|
+
# pyperclip.copy(text_to_save)
|
|
178
|
+
|
|
179
|
+
encoding = tiktoken.encoding_for_model('gpt-4.1-mini')
|
|
180
|
+
token_count = len(encoding.encode(text_to_save))
|
|
181
|
+
print(f'Token count: {token_count}')
|
|
182
|
+
|
|
183
|
+
print('User message written to ./tmp/user_message.txt')
|
|
184
|
+
print('Element tree written to ./tmp/simplified_element_tree.json')
|
|
185
|
+
print('Original element tree written to ./tmp/original_element_tree.json')
|
|
186
|
+
|
|
187
|
+
# Save timing information
|
|
188
|
+
timing_text = '🔍 DOM EXTRACTION PERFORMANCE ANALYSIS\n'
|
|
189
|
+
timing_text += f'{"=" * 50}\n\n'
|
|
190
|
+
timing_text += f'📄 Website: {website}\n'
|
|
191
|
+
timing_text += f'📊 Total Elements: {total_elements}\n'
|
|
192
|
+
timing_text += f'🎯 Token Count: {token_count}\n\n'
|
|
193
|
+
|
|
194
|
+
timing_text += '⏱️ TIMING BREAKDOWN:\n'
|
|
195
|
+
timing_text += f'{"─" * 30}\n'
|
|
196
|
+
for key, value in all_timing.items():
|
|
197
|
+
timing_text += f'{key:<35}: {value * 1000:>8.2f} ms\n'
|
|
198
|
+
|
|
199
|
+
# Calculate percentages
|
|
200
|
+
total_time = all_timing.get('get_state_summary_total', 0)
|
|
201
|
+
if total_time > 0 and total_elements > 0:
|
|
202
|
+
timing_text += '\n📈 PERCENTAGE BREAKDOWN:\n'
|
|
203
|
+
timing_text += f'{"─" * 30}\n'
|
|
204
|
+
for key, value in all_timing.items():
|
|
205
|
+
if key != 'get_state_summary_total':
|
|
206
|
+
percentage = (value / total_time) * 100
|
|
207
|
+
timing_text += f'{key:<35}: {percentage:>7.1f}%\n'
|
|
208
|
+
|
|
209
|
+
timing_text += '\n🎯 CLICKABLE DETECTION ANALYSIS:\n'
|
|
210
|
+
timing_text += f'{"─" * 35}\n'
|
|
211
|
+
clickable_time = all_timing.get('clickable_detection_time', 0)
|
|
212
|
+
if clickable_time > 0 and total_elements > 0:
|
|
213
|
+
avg_per_element = (clickable_time / total_elements) * 1000000 # microseconds
|
|
214
|
+
timing_text += f'Total clickable detection time: {clickable_time * 1000:.2f} ms\n'
|
|
215
|
+
timing_text += f'Average per element: {avg_per_element:.2f} μs\n'
|
|
216
|
+
timing_text += f'Clickable detection calls: ~{total_elements} (approx)\n'
|
|
217
|
+
|
|
218
|
+
async with await anyio.open_file('./tmp/timing_analysis.txt', 'w', encoding='utf-8') as f:
|
|
219
|
+
await f.write(timing_text)
|
|
220
|
+
|
|
221
|
+
print('Timing analysis written to ./tmp/timing_analysis.txt')
|
|
222
|
+
|
|
223
|
+
# also save all_elements_state.element_tree.clickable_elements_to_string() to a file
|
|
224
|
+
# with open('./tmp/clickable_elements.json', 'w', encoding='utf-8') as f:
|
|
225
|
+
# f.write(json.dumps(all_elements_state.element_tree.__json__(), indent=2))
|
|
226
|
+
# print('Clickable elements written to ./tmp/clickable_elements.json')
|
|
227
|
+
|
|
228
|
+
website_list = get_website_list_for_prompt()
|
|
229
|
+
answer = input(
|
|
230
|
+
"🎮 Enter: element index | 'index' click (clickable) | 'index,text' input | 'c,index' copy | Enter re-run | 'n' next | 'q' quit: "
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if answer.lower() == 'q':
|
|
234
|
+
return # Exit completely
|
|
235
|
+
elif answer.lower() == 'n':
|
|
236
|
+
print('Moving to next website...')
|
|
237
|
+
current_website_index += 1
|
|
238
|
+
break # Break inner loop to go to next website
|
|
239
|
+
elif answer.strip() == '':
|
|
240
|
+
print('Re-running extraction on current page state...')
|
|
241
|
+
continue # Continue inner loop to re-extract DOM without reloading page
|
|
242
|
+
elif answer.strip().isdigit():
|
|
243
|
+
# Click element format: index
|
|
244
|
+
try:
|
|
245
|
+
clicked_index = int(answer)
|
|
246
|
+
if clicked_index in selector_map:
|
|
247
|
+
element_node = selector_map[clicked_index]
|
|
248
|
+
print(f'Clicking element {clicked_index}: {element_node.tag_name}')
|
|
249
|
+
event = browser_session.event_bus.dispatch(ClickElementEvent(node=element_node))
|
|
250
|
+
await event
|
|
251
|
+
print('Click successful.')
|
|
252
|
+
except ValueError:
|
|
253
|
+
print(f"Invalid input: '{answer}'. Enter an index, 'index,text', 'c,index', or 'q'.")
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
if answer.lower().startswith('c,'):
|
|
258
|
+
# Copy element JSON format: c,index
|
|
259
|
+
parts = answer.split(',', 1)
|
|
260
|
+
if len(parts) == 2:
|
|
261
|
+
try:
|
|
262
|
+
target_index = int(parts[1].strip())
|
|
263
|
+
if target_index in selector_map:
|
|
264
|
+
element_node = selector_map[target_index]
|
|
265
|
+
element_json = json.dumps(element_node.__json__(), indent=2, default=str)
|
|
266
|
+
pyperclip.copy(element_json)
|
|
267
|
+
print(f'Copied element {target_index} JSON to clipboard: {element_node.tag_name}')
|
|
268
|
+
else:
|
|
269
|
+
print(f'Invalid index: {target_index}')
|
|
270
|
+
except ValueError:
|
|
271
|
+
print(f'Invalid index format: {parts[1]}')
|
|
272
|
+
else:
|
|
273
|
+
print("Invalid input format. Use 'c,index'.")
|
|
274
|
+
elif ',' in answer:
|
|
275
|
+
# Input text format: index,text
|
|
276
|
+
parts = answer.split(',', 1)
|
|
277
|
+
if len(parts) == 2:
|
|
278
|
+
try:
|
|
279
|
+
target_index = int(parts[0].strip())
|
|
280
|
+
text_to_input = parts[1]
|
|
281
|
+
if target_index in selector_map:
|
|
282
|
+
element_node = selector_map[target_index]
|
|
283
|
+
print(
|
|
284
|
+
f"Inputting text '{text_to_input}' into element {target_index}: {element_node.tag_name}"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
event = await browser_session.event_bus.dispatch(
|
|
288
|
+
TypeTextEvent(node=element_node, text=text_to_input)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
print('Input successful.')
|
|
292
|
+
else:
|
|
293
|
+
print(f'Invalid index: {target_index}')
|
|
294
|
+
except ValueError:
|
|
295
|
+
print(f'Invalid index format: {parts[0]}')
|
|
296
|
+
else:
|
|
297
|
+
print("Invalid input format. Use 'index,text'.")
|
|
298
|
+
|
|
299
|
+
except Exception as action_e:
|
|
300
|
+
print(f'Action failed: {action_e}')
|
|
301
|
+
|
|
302
|
+
# No explicit highlight removal here, get_state handles it at the start of the loop
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
print(f'Error in loop: {e}')
|
|
306
|
+
# Optionally add a small delay before retrying
|
|
307
|
+
await asyncio.sleep(1)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
if __name__ == '__main__':
|
|
311
|
+
asyncio.run(test_focus_vs_all_elements())
|
|
312
|
+
# asyncio.run(test_process_html_file()) # Commented out the other test
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from browser_use import Agent
|
|
2
|
+
from browser_use.browser import BrowserProfile, BrowserSession
|
|
3
|
+
from browser_use.browser.profile import ViewportSize
|
|
4
|
+
from browser_use.llm import ChatAzureOpenAI
|
|
5
|
+
|
|
6
|
+
# Initialize the Azure OpenAI client
|
|
7
|
+
llm = ChatAzureOpenAI(
|
|
8
|
+
model='gpt-4.1-mini',
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
TASK = """
|
|
13
|
+
Go to https://browser-use.github.io/stress-tests/challenges/react-native-web-form.html and complete the React Native Web form by filling in all required fields and submitting.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def main():
|
|
18
|
+
browser = BrowserSession(
|
|
19
|
+
browser_profile=BrowserProfile(
|
|
20
|
+
window_size=ViewportSize(width=1100, height=1000),
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
agent = Agent(task=TASK, llm=llm)
|
|
25
|
+
|
|
26
|
+
await agent.run()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == '__main__':
|
|
30
|
+
import asyncio
|
|
31
|
+
|
|
32
|
+
asyncio.run(main())
|