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,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())