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,665 @@
1
+ """Namespace initialization for code-use mode.
2
+
3
+ This module creates a namespace with all browser tools available as functions,
4
+ similar to a Jupyter notebook environment.
5
+ """
6
+
7
+ import asyncio
8
+ import csv
9
+ import datetime
10
+ import json
11
+ import logging
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ import requests
17
+
18
+ from browser_use.browser import BrowserSession
19
+ from browser_use.filesystem.file_system import FileSystem
20
+ from browser_use.llm.base import BaseChatModel
21
+ from browser_use.tools.service import CodeAgentTools, Tools
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Try to import optional data science libraries
26
+ try:
27
+ import numpy as np # type: ignore
28
+
29
+ NUMPY_AVAILABLE = True
30
+ except ImportError:
31
+ NUMPY_AVAILABLE = False
32
+
33
+ try:
34
+ import pandas as pd # type: ignore
35
+
36
+ PANDAS_AVAILABLE = True
37
+ except ImportError:
38
+ PANDAS_AVAILABLE = False
39
+
40
+ try:
41
+ import matplotlib.pyplot as plt # type: ignore
42
+
43
+ MATPLOTLIB_AVAILABLE = True
44
+ except ImportError:
45
+ MATPLOTLIB_AVAILABLE = False
46
+
47
+ try:
48
+ from bs4 import BeautifulSoup # type: ignore
49
+
50
+ BS4_AVAILABLE = True
51
+ except ImportError:
52
+ BS4_AVAILABLE = False
53
+
54
+ try:
55
+ from pypdf import PdfReader # type: ignore
56
+
57
+ PYPDF_AVAILABLE = True
58
+ except ImportError:
59
+ PYPDF_AVAILABLE = False
60
+
61
+ try:
62
+ from tabulate import tabulate # type: ignore
63
+
64
+ TABULATE_AVAILABLE = True
65
+ except ImportError:
66
+ TABULATE_AVAILABLE = False
67
+
68
+
69
+ def _strip_js_comments(js_code: str) -> str:
70
+ """
71
+ Remove JavaScript comments before CDP evaluation.
72
+ CDP's Runtime.evaluate doesn't handle comments in all contexts.
73
+
74
+ Args:
75
+ js_code: JavaScript code potentially containing comments
76
+
77
+ Returns:
78
+ JavaScript code with comments stripped
79
+ """
80
+ # Remove multi-line comments (/* ... */)
81
+ js_code = re.sub(r'/\*.*?\*/', '', js_code, flags=re.DOTALL)
82
+
83
+ # Remove single-line comments - only lines that START with // (after whitespace)
84
+ # This avoids breaking XPath strings, URLs, regex patterns, etc.
85
+ js_code = re.sub(r'^\s*//.*$', '', js_code, flags=re.MULTILINE)
86
+
87
+ return js_code
88
+
89
+
90
+ class EvaluateError(Exception):
91
+ """Special exception raised by evaluate() to stop Python execution immediately."""
92
+
93
+ pass
94
+
95
+
96
+ async def validate_task_completion(
97
+ task: str,
98
+ output: str | None,
99
+ llm: BaseChatModel,
100
+ ) -> tuple[bool, str]:
101
+ """
102
+ Validate if task is truly complete by asking LLM without system prompt or history.
103
+
104
+ Args:
105
+ task: The original task description
106
+ output: The output from the done() call
107
+ llm: The LLM to use for validation
108
+
109
+ Returns:
110
+ Tuple of (is_complete, reasoning)
111
+ """
112
+ from browser_use.llm.messages import UserMessage
113
+
114
+ # Build validation prompt
115
+ validation_prompt = f"""You are a task completion validator. Analyze if the agent has truly completed the user's task.
116
+
117
+ **Original Task:**
118
+ {task}
119
+
120
+ **Agent's Output:**
121
+ {output[:100000] if output else '(No output provided)'}
122
+
123
+ **Your Task:**
124
+ Determine if the agent has successfully completed the user's task. Consider:
125
+ 1. Has the agent delivered what the user requested?
126
+ 2. If data extraction was requested, is there actual data?
127
+ 3. If the task is impossible (e.g., localhost website, login required but no credentials), is it truly impossible?
128
+ 4. Could the agent continue and make meaningful progress?
129
+
130
+ **Response Format:**
131
+ Reasoning: [Your analysis of whether the task is complete]
132
+ Verdict: [YES or NO]
133
+
134
+ YES = Task is complete OR truly impossible to complete
135
+ NO = Agent should continue working"""
136
+
137
+ try:
138
+ # Call LLM with just the validation prompt (no system prompt, no history)
139
+ response = await llm.ainvoke([UserMessage(content=validation_prompt)])
140
+ response_text = response.completion
141
+
142
+ # Parse the response
143
+ reasoning = ''
144
+ verdict = 'NO'
145
+
146
+ # Extract reasoning and verdict
147
+ lines = response_text.split('\n')
148
+ for line in lines:
149
+ if line.strip().lower().startswith('reasoning:'):
150
+ reasoning = line.split(':', 1)[1].strip()
151
+ elif line.strip().lower().startswith('verdict:'):
152
+ verdict_text = line.split(':', 1)[1].strip().upper()
153
+ if 'YES' in verdict_text:
154
+ verdict = 'YES'
155
+ elif 'NO' in verdict_text:
156
+ verdict = 'NO'
157
+
158
+ # If we couldn't parse, try to find YES/NO in the response
159
+ if not reasoning:
160
+ reasoning = response_text
161
+
162
+ is_complete = verdict == 'YES'
163
+
164
+ logger.info(f'Task validation: {verdict}')
165
+ logger.debug(f'Validation reasoning: {reasoning}')
166
+
167
+ return is_complete, reasoning
168
+
169
+ except Exception as e:
170
+ logger.warning(f'Failed to validate task completion: {e}')
171
+ # On error, assume the agent knows what they're doing
172
+ return True, f'Validation failed: {e}'
173
+
174
+
175
+ async def evaluate(code: str, browser_session: BrowserSession) -> Any:
176
+ """
177
+ Execute JavaScript code in the browser and return the result.
178
+
179
+ Args:
180
+ code: JavaScript code to execute (must be wrapped in IIFE)
181
+
182
+ Returns:
183
+ The result of the JavaScript execution
184
+
185
+ Raises:
186
+ EvaluateError: If JavaScript execution fails. This stops Python execution immediately.
187
+
188
+ Example:
189
+ result = await evaluate('''
190
+ (function(){
191
+ return Array.from(document.querySelectorAll('.product')).map(p => ({
192
+ name: p.querySelector('.name').textContent,
193
+ price: p.querySelector('.price').textContent
194
+ }))
195
+ })()
196
+ ''')
197
+ """
198
+ # Strip JavaScript comments before CDP evaluation (CDP doesn't support them in all contexts)
199
+ code = _strip_js_comments(code)
200
+
201
+ cdp_session = await browser_session.get_or_create_cdp_session()
202
+
203
+ try:
204
+ # Execute JavaScript with proper error handling
205
+ result = await cdp_session.cdp_client.send.Runtime.evaluate(
206
+ params={'expression': code, 'returnByValue': True, 'awaitPromise': True},
207
+ session_id=cdp_session.session_id,
208
+ )
209
+
210
+ # Check for JavaScript execution errors
211
+ if result.get('exceptionDetails'):
212
+ exception = result['exceptionDetails']
213
+ error_text = exception.get('text', 'Unknown error')
214
+
215
+ # Try to get more details from the exception
216
+ error_details = []
217
+ if 'exception' in exception:
218
+ exc_obj = exception['exception']
219
+ if 'description' in exc_obj:
220
+ error_details.append(exc_obj['description'])
221
+ elif 'value' in exc_obj:
222
+ error_details.append(str(exc_obj['value']))
223
+
224
+ # Build comprehensive error message with full CDP context
225
+ error_msg = f'JavaScript execution error: {error_text}'
226
+ if error_details:
227
+ error_msg += f'\nDetails: {" | ".join(error_details)}'
228
+
229
+ # Raise special exception that will stop Python execution immediately
230
+ raise EvaluateError(error_msg)
231
+
232
+ # Get the result data
233
+ result_data = result.get('result', {})
234
+
235
+ # Get the actual value
236
+ value = result_data.get('value')
237
+
238
+ # Return the value directly
239
+ if value is None:
240
+ return None if 'value' in result_data else 'undefined'
241
+ elif isinstance(value, (dict, list)):
242
+ # Complex objects - already deserialized by returnByValue
243
+ return value
244
+ else:
245
+ # Primitive values
246
+ return value
247
+
248
+ except EvaluateError:
249
+ # Re-raise EvaluateError as-is to stop Python execution
250
+ raise
251
+ except Exception as e:
252
+ # Wrap other exceptions in EvaluateError
253
+ raise EvaluateError(f'Failed to execute JavaScript: {type(e).__name__}: {e}') from e
254
+
255
+
256
+ def create_namespace(
257
+ browser_session: BrowserSession,
258
+ tools: Tools | None = None,
259
+ page_extraction_llm: BaseChatModel | None = None,
260
+ file_system: FileSystem | None = None,
261
+ available_file_paths: list[str] | None = None,
262
+ sensitive_data: dict[str, str | dict[str, str]] | None = None,
263
+ ) -> dict[str, Any]:
264
+ """
265
+ Create a namespace with all browser tools available as functions.
266
+
267
+ This function creates a dictionary of functions that can be used to interact
268
+ with the browser, similar to a Jupyter notebook environment.
269
+
270
+ Args:
271
+ browser_session: The browser session to use
272
+ tools: Optional Tools instance (will create default if not provided)
273
+ page_extraction_llm: Optional LLM for page extraction
274
+ file_system: Optional file system for file operations
275
+ available_file_paths: Optional list of available file paths
276
+ sensitive_data: Optional sensitive data dictionary
277
+
278
+ Returns:
279
+ Dictionary containing all available functions and objects
280
+
281
+ Example:
282
+ namespace = create_namespace(browser_session)
283
+ await namespace['navigate'](url='https://google.com')
284
+ result = await namespace['evaluate']('document.title')
285
+ """
286
+ if tools is None:
287
+ # Use CodeAgentTools with default exclusions optimized for code-use mode
288
+ # For code-use, we keep: navigate, evaluate, wait, done
289
+ # and exclude: most browser interaction, file system actions (use Python instead)
290
+ tools = CodeAgentTools()
291
+
292
+ if available_file_paths is None:
293
+ available_file_paths = []
294
+
295
+ namespace: dict[str, Any] = {
296
+ # Core objects
297
+ 'browser': browser_session,
298
+ 'file_system': file_system,
299
+ # Standard library modules (always available)
300
+ 'json': json,
301
+ 'asyncio': asyncio,
302
+ 'Path': Path,
303
+ 'csv': csv,
304
+ 're': re,
305
+ 'datetime': datetime,
306
+ 'requests': requests,
307
+ }
308
+
309
+ # Add optional data science libraries if available
310
+ if NUMPY_AVAILABLE:
311
+ namespace['np'] = np
312
+ namespace['numpy'] = np
313
+ if PANDAS_AVAILABLE:
314
+ namespace['pd'] = pd
315
+ namespace['pandas'] = pd
316
+ if MATPLOTLIB_AVAILABLE:
317
+ namespace['plt'] = plt
318
+ namespace['matplotlib'] = plt
319
+ if BS4_AVAILABLE:
320
+ namespace['BeautifulSoup'] = BeautifulSoup
321
+ namespace['bs4'] = BeautifulSoup
322
+ if PYPDF_AVAILABLE:
323
+ namespace['PdfReader'] = PdfReader
324
+ namespace['pypdf'] = PdfReader
325
+ if TABULATE_AVAILABLE:
326
+ namespace['tabulate'] = tabulate
327
+
328
+ # Track failed evaluate() calls to detect repeated failed approaches
329
+ if '_evaluate_failures' not in namespace:
330
+ namespace['_evaluate_failures'] = []
331
+
332
+ # Add custom evaluate function that returns values directly
333
+ async def evaluate_wrapper(
334
+ code: str | None = None, variables: dict[str, Any] | None = None, *_args: Any, **kwargs: Any
335
+ ) -> Any:
336
+ # Handle both positional and keyword argument styles
337
+ if code is None:
338
+ # Check if code was passed as keyword arg
339
+ code = kwargs.get('code', kwargs.get('js_code', kwargs.get('expression', '')))
340
+ # Extract variables if passed as kwarg
341
+ if variables is None:
342
+ variables = kwargs.get('variables')
343
+
344
+ if not code:
345
+ raise ValueError('No JavaScript code provided to evaluate()')
346
+
347
+ # Inject variables if provided
348
+ if variables:
349
+ vars_json = json.dumps(variables)
350
+ stripped = code.strip()
351
+
352
+ # Check if code is already a function expression expecting params
353
+ # Pattern: (function(params) { ... }) or (async function(params) { ... })
354
+ if re.match(r'\((?:async\s+)?function\s*\(\s*\w+\s*\)', stripped):
355
+ # Already expects params, wrap to call it with our variables
356
+ code = f'(function(){{ const params = {vars_json}; return {stripped}(params); }})()'
357
+ else:
358
+ # Not a parameterized function, inject params in scope
359
+ # Check if already wrapped in IIFE (including arrow function IIFEs)
360
+ is_wrapped = (
361
+ (stripped.startswith('(function()') and '})()' in stripped[-10:])
362
+ or (stripped.startswith('(async function()') and '})()' in stripped[-10:])
363
+ or (stripped.startswith('(() =>') and ')()' in stripped[-10:])
364
+ or (stripped.startswith('(async () =>') and ')()' in stripped[-10:])
365
+ )
366
+ if is_wrapped:
367
+ # Already wrapped, inject params at the start
368
+ # Try to match regular function IIFE
369
+ match = re.match(r'(\((?:async\s+)?function\s*\(\s*\)\s*\{)', stripped)
370
+ if match:
371
+ prefix = match.group(1)
372
+ rest = stripped[len(prefix) :]
373
+ code = f'{prefix} const params = {vars_json}; {rest}'
374
+ else:
375
+ # Try to match arrow function IIFE
376
+ # Patterns: (() => expr)() or (() => { ... })() or (async () => ...)()
377
+ arrow_match = re.match(r'(\((?:async\s+)?\(\s*\)\s*=>\s*\{)', stripped)
378
+ if arrow_match:
379
+ # Arrow function with block body: (() => { ... })()
380
+ prefix = arrow_match.group(1)
381
+ rest = stripped[len(prefix) :]
382
+ code = f'{prefix} const params = {vars_json}; {rest}'
383
+ else:
384
+ # Arrow function with expression body or fallback: wrap in outer function
385
+ code = f'(function(){{ const params = {vars_json}; return {stripped}; }})()'
386
+ else:
387
+ # Not wrapped, wrap with params
388
+ code = f'(function(){{ const params = {vars_json}; {code} }})()'
389
+ # Skip auto-wrap below
390
+ return await evaluate(code, browser_session)
391
+
392
+ # Auto-wrap in IIFE if not already wrapped (and no variables were injected)
393
+ if not variables:
394
+ stripped = code.strip()
395
+ # Check for regular function IIFEs, async function IIFEs, and arrow function IIFEs
396
+ is_wrapped = (
397
+ (stripped.startswith('(function()') and '})()' in stripped[-10:])
398
+ or (stripped.startswith('(async function()') and '})()' in stripped[-10:])
399
+ or (stripped.startswith('(() =>') and ')()' in stripped[-10:])
400
+ or (stripped.startswith('(async () =>') and ')()' in stripped[-10:])
401
+ )
402
+ if not is_wrapped:
403
+ code = f'(function(){{{code}}})()'
404
+
405
+ # Execute and track failures
406
+ try:
407
+ result = await evaluate(code, browser_session)
408
+
409
+ # Print result structure for debugging
410
+ if isinstance(result, list) and result and isinstance(result[0], dict):
411
+ result_preview = f'list of dicts - len={len(result)}, example 1:\n'
412
+ sample_result = result[0]
413
+ for key, value in list(sample_result.items())[:10]:
414
+ value_str = str(value)[:10] if not isinstance(value, (int, float, bool, type(None))) else str(value)
415
+ result_preview += f' {key}: {value_str}...\n'
416
+ if len(sample_result) > 10:
417
+ result_preview += f' ... {len(sample_result) - 10} more keys'
418
+ print(result_preview)
419
+
420
+ elif isinstance(result, list):
421
+ if len(result) == 0:
422
+ print('type=list, len=0')
423
+ else:
424
+ result_preview = str(result)[:100]
425
+ print(f'type=list, len={len(result)}, preview={result_preview}...')
426
+ elif isinstance(result, dict):
427
+ result_preview = f'type=dict, len={len(result)}, sample keys:\n'
428
+ for key, value in list(result.items())[:10]:
429
+ value_str = str(value)[:10] if not isinstance(value, (int, float, bool, type(None))) else str(value)
430
+ result_preview += f' {key}: {value_str}...\n'
431
+ if len(result) > 10:
432
+ result_preview += f' ... {len(result) - 10} more keys'
433
+ print(result_preview)
434
+
435
+ else:
436
+ print(f'type={type(result).__name__}, value={repr(result)[:50]}')
437
+
438
+ return result
439
+ except Exception as e:
440
+ # Track errors for pattern detection
441
+ namespace['_evaluate_failures'].append({'error': str(e), 'type': 'exception'})
442
+ raise
443
+
444
+ namespace['evaluate'] = evaluate_wrapper
445
+
446
+ # Add get_selector_from_index helper for code_use mode
447
+ async def get_selector_from_index_wrapper(index: int) -> str:
448
+ """
449
+ Get the CSS selector for an element by its interactive index.
450
+
451
+ This allows you to use the element's index from the browser state to get
452
+ its CSS selector for use in JavaScript evaluate() calls.
453
+
454
+ Args:
455
+ index: The interactive index from the browser state (e.g., [123])
456
+
457
+ Returns:
458
+ str: CSS selector that can be used in JavaScript
459
+
460
+ Example:
461
+ selector = await get_selector_from_index(123)
462
+ await evaluate(f'''
463
+ (function(){{
464
+ const el = document.querySelector({json.dumps(selector)});
465
+ if (el) el.click();
466
+ }})()
467
+ ''')
468
+ """
469
+ from browser_use.dom.utils import generate_css_selector_for_element
470
+
471
+ # Get element by index from browser session
472
+ node = await browser_session.get_element_by_index(index)
473
+ if node is None:
474
+ msg = f'Element index {index} not available - page may have changed. Try refreshing browser state.'
475
+ logger.warning(f'⚠️ {msg}')
476
+ raise RuntimeError(msg)
477
+
478
+ # Check if element is in shadow DOM
479
+ shadow_hosts = []
480
+ current = node.parent_node
481
+ while current:
482
+ if current.shadow_root_type is not None:
483
+ # This is a shadow host
484
+ host_tag = current.tag_name.lower()
485
+ host_id = current.attributes.get('id', '') if current.attributes else ''
486
+ host_desc = f'{host_tag}#{host_id}' if host_id else host_tag
487
+ shadow_hosts.insert(0, host_desc)
488
+ current = current.parent_node
489
+
490
+ # Check if in iframe
491
+ in_iframe = False
492
+ current = node.parent_node
493
+ while current:
494
+ if current.tag_name.lower() == 'iframe':
495
+ in_iframe = True
496
+ break
497
+ current = current.parent_node
498
+
499
+ # Use the robust selector generation function (now handles special chars in IDs)
500
+ selector = generate_css_selector_for_element(node)
501
+
502
+ # Log shadow DOM/iframe info if detected
503
+ if shadow_hosts:
504
+ shadow_path = ' > '.join(shadow_hosts)
505
+ logger.info(f'Element [{index}] is inside Shadow DOM. Path: {shadow_path}')
506
+ logger.info(f' Selector: {selector}')
507
+ logger.info(
508
+ f' To access: document.querySelector("{shadow_hosts[0].split("#")[0]}").shadowRoot.querySelector("{selector}")'
509
+ )
510
+ if in_iframe:
511
+ logger.info(f"Element [{index}] is inside an iframe. Regular querySelector won't work.")
512
+
513
+ if selector:
514
+ return selector
515
+
516
+ # Fallback: just use tag name if available
517
+ if node.tag_name:
518
+ return node.tag_name.lower()
519
+
520
+ raise ValueError(f'Could not generate selector for element index {index}')
521
+
522
+ namespace['get_selector_from_index'] = get_selector_from_index_wrapper
523
+
524
+ # Inject all tools as functions into the namespace
525
+ # Skip 'evaluate' since we have a custom implementation above
526
+ for action_name, action in tools.registry.registry.actions.items():
527
+ if action_name == 'evaluate':
528
+ continue # Skip - use custom evaluate that returns Python objects directly
529
+ param_model = action.param_model
530
+ action_function = action.function
531
+
532
+ # Create a closure to capture the current action_name, param_model, and action_function
533
+ def make_action_wrapper(act_name, par_model, act_func):
534
+ async def action_wrapper(*args, **kwargs):
535
+ # Convert positional args to kwargs based on param model fields
536
+ if args:
537
+ # Get the field names from the pydantic model
538
+ field_names = list(par_model.model_fields.keys())
539
+ for i, arg in enumerate(args):
540
+ if i < len(field_names):
541
+ kwargs[field_names[i]] = arg
542
+
543
+ # Create params from kwargs
544
+ try:
545
+ params = par_model(**kwargs)
546
+ except Exception as e:
547
+ raise ValueError(f'Invalid parameters for {act_name}: {e}') from e
548
+
549
+ # Special validation for done() - enforce minimal code cell
550
+ if act_name == 'done':
551
+ consecutive_failures = namespace.get('_consecutive_errors')
552
+ if consecutive_failures and consecutive_failures > 3:
553
+ pass
554
+
555
+ else:
556
+ # Check if there are multiple Python blocks in this response
557
+ all_blocks = namespace.get('_all_code_blocks', {})
558
+ python_blocks = [k for k in sorted(all_blocks.keys()) if k.startswith('python_')]
559
+
560
+ if len(python_blocks) > 1:
561
+ msg = (
562
+ 'done() should be the ONLY code block in the response.\n'
563
+ 'You have multiple Python blocks in this response. Consider calling done() in a separate response '
564
+ 'Now verify the last output and if it satisfies the task, call done(), else continue working.'
565
+ )
566
+ print(msg)
567
+
568
+ # Get the current cell code from namespace (injected by service.py before execution)
569
+ current_code = namespace.get('_current_cell_code')
570
+ if current_code and isinstance(current_code, str):
571
+ # Count non-empty, non-comment lines
572
+ lines = [line.strip() for line in current_code.strip().split('\n')]
573
+ code_lines = [line for line in lines if line and not line.startswith('#')]
574
+
575
+ # Check if the line above await done() contains an if block
576
+ done_line_index = -1
577
+ for i, line in enumerate(reversed(code_lines)):
578
+ if 'await done()' in line or 'await done(' in line:
579
+ done_line_index = len(code_lines) - 1 - i
580
+ break
581
+
582
+ has_if_above = False
583
+ has_else_above = False
584
+ has_elif_above = False
585
+ if done_line_index > 0:
586
+ line_above = code_lines[done_line_index - 1]
587
+ has_if_above = line_above.strip().startswith('if ') and line_above.strip().endswith(':')
588
+ has_else_above = line_above.strip().startswith('else:')
589
+ has_elif_above = line_above.strip().startswith('elif ')
590
+ if has_if_above or has_else_above or has_elif_above:
591
+ msg = (
592
+ 'done() should be called individually after verifying the result from any logic.\n'
593
+ 'Consider validating your output first, THEN call done() in a final step without if/else/elif blocks only if the task is truly complete.'
594
+ )
595
+ logger.error(msg)
596
+ print(msg)
597
+ raise RuntimeError(msg)
598
+
599
+ # Build special context
600
+ special_context = {
601
+ 'browser_session': browser_session,
602
+ 'page_extraction_llm': page_extraction_llm,
603
+ 'available_file_paths': available_file_paths,
604
+ 'has_sensitive_data': False, # Can be handled separately if needed
605
+ 'file_system': file_system,
606
+ }
607
+
608
+ # Execute the action
609
+ result = await act_func(params=params, **special_context)
610
+
611
+ # For code-use mode, we want to return the result directly
612
+ # not wrapped in ActionResult
613
+ if hasattr(result, 'extracted_content'):
614
+ # Special handling for done action - mark task as complete
615
+ if act_name == 'done' and hasattr(result, 'is_done') and result.is_done:
616
+ namespace['_task_done'] = True
617
+ # Store the extracted content as the final result
618
+ if result.extracted_content:
619
+ namespace['_task_result'] = result.extracted_content
620
+ # Store the self-reported success status
621
+ if hasattr(result, 'success'):
622
+ namespace['_task_success'] = result.success
623
+
624
+ # If there's extracted content, return it
625
+ if result.extracted_content:
626
+ return result.extracted_content
627
+ # If there's an error, raise it
628
+ if result.error:
629
+ raise RuntimeError(result.error)
630
+ # Otherwise return None
631
+ return None
632
+ return result
633
+
634
+ return action_wrapper
635
+
636
+ # Rename 'input' to 'input_text' to avoid shadowing Python's built-in input()
637
+ namespace_action_name = 'input_text' if action_name == 'input' else action_name
638
+
639
+ # Add the wrapper to the namespace
640
+ namespace[namespace_action_name] = make_action_wrapper(action_name, param_model, action_function)
641
+
642
+ return namespace
643
+
644
+
645
+ def get_namespace_documentation(namespace: dict[str, Any]) -> str:
646
+ """
647
+ Generate documentation for all available functions in the namespace.
648
+
649
+ Args:
650
+ namespace: The namespace dictionary
651
+
652
+ Returns:
653
+ Markdown-formatted documentation string
654
+ """
655
+ docs = ['# Available Functions\n']
656
+
657
+ # Document each function
658
+ for name, obj in sorted(namespace.items()):
659
+ if callable(obj) and not name.startswith('_'):
660
+ # Get function signature and docstring
661
+ if hasattr(obj, '__doc__') and obj.__doc__:
662
+ docs.append(f'## {name}\n')
663
+ docs.append(f'{obj.__doc__}\n')
664
+
665
+ return '\n'.join(docs)