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,637 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import asyncio
|
|
3
|
+
import base64
|
|
4
|
+
import dataclasses
|
|
5
|
+
import enum
|
|
6
|
+
import inspect
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import textwrap
|
|
11
|
+
from collections.abc import Callable, Coroutine
|
|
12
|
+
from functools import wraps
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, Union, cast, get_args, get_origin
|
|
14
|
+
|
|
15
|
+
import cloudpickle
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from browser_use.sandbox.views import (
|
|
19
|
+
BrowserCreatedData,
|
|
20
|
+
ErrorData,
|
|
21
|
+
LogData,
|
|
22
|
+
ResultData,
|
|
23
|
+
SandboxError,
|
|
24
|
+
SSEEvent,
|
|
25
|
+
SSEEventType,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from browser_use.browser import BrowserSession
|
|
30
|
+
|
|
31
|
+
T = TypeVar('T')
|
|
32
|
+
P = ParamSpec('P')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_terminal_width() -> int:
|
|
36
|
+
"""Get terminal width, default to 80 if unable to detect"""
|
|
37
|
+
try:
|
|
38
|
+
return os.get_terminal_size().columns
|
|
39
|
+
except (AttributeError, OSError):
|
|
40
|
+
return 80
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def _call_callback(callback: Callable[..., Any], *args: Any) -> None:
|
|
44
|
+
"""Call a callback that can be either sync or async"""
|
|
45
|
+
result = callback(*args)
|
|
46
|
+
if asyncio.iscoroutine(result):
|
|
47
|
+
await result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_function_source_without_decorator(func: Callable) -> str:
|
|
51
|
+
"""Get function source code with decorator removed"""
|
|
52
|
+
source = inspect.getsource(func)
|
|
53
|
+
source = textwrap.dedent(source)
|
|
54
|
+
|
|
55
|
+
# Parse and remove decorator
|
|
56
|
+
tree = ast.parse(source)
|
|
57
|
+
for node in ast.walk(tree):
|
|
58
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
59
|
+
node.decorator_list = []
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
return ast.unparse(tree)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_imports_used_in_function(func: Callable) -> str:
|
|
66
|
+
"""Extract only imports that are referenced in the function body or type annotations"""
|
|
67
|
+
# Get all names referenced in the function
|
|
68
|
+
code = func.__code__
|
|
69
|
+
referenced_names = set(code.co_names)
|
|
70
|
+
|
|
71
|
+
# Also get names from type annotations (recursively for complex types like Union, Literal, etc.)
|
|
72
|
+
def extract_type_names(annotation):
|
|
73
|
+
"""Recursively extract all type names from annotation"""
|
|
74
|
+
if annotation is None or annotation == inspect.Parameter.empty:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# Handle simple types with __name__
|
|
78
|
+
if hasattr(annotation, '__name__'):
|
|
79
|
+
referenced_names.add(annotation.__name__)
|
|
80
|
+
|
|
81
|
+
# Handle string annotations
|
|
82
|
+
if isinstance(annotation, str):
|
|
83
|
+
referenced_names.add(annotation)
|
|
84
|
+
|
|
85
|
+
# Handle generic types like Union[X, Y], Literal['x'], etc.
|
|
86
|
+
origin = get_origin(annotation)
|
|
87
|
+
args = get_args(annotation)
|
|
88
|
+
|
|
89
|
+
if origin:
|
|
90
|
+
# Add the origin type name (e.g., 'Union', 'Literal')
|
|
91
|
+
if hasattr(origin, '__name__'):
|
|
92
|
+
referenced_names.add(origin.__name__)
|
|
93
|
+
|
|
94
|
+
# Recursively extract from generic args
|
|
95
|
+
if args:
|
|
96
|
+
for arg in args:
|
|
97
|
+
extract_type_names(arg)
|
|
98
|
+
|
|
99
|
+
sig = inspect.signature(func)
|
|
100
|
+
for param in sig.parameters.values():
|
|
101
|
+
if param.annotation != inspect.Parameter.empty:
|
|
102
|
+
extract_type_names(param.annotation)
|
|
103
|
+
|
|
104
|
+
# Get return annotation (also extract recursively)
|
|
105
|
+
if 'return' in func.__annotations__:
|
|
106
|
+
extract_type_names(func.__annotations__['return'])
|
|
107
|
+
|
|
108
|
+
# Get the module where function is defined
|
|
109
|
+
module = inspect.getmodule(func)
|
|
110
|
+
if not module or not hasattr(module, '__file__') or module.__file__ is None:
|
|
111
|
+
return ''
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
with open(module.__file__) as f:
|
|
115
|
+
module_source = f.read()
|
|
116
|
+
|
|
117
|
+
tree = ast.parse(module_source)
|
|
118
|
+
needed_imports: list[str] = []
|
|
119
|
+
|
|
120
|
+
for node in tree.body:
|
|
121
|
+
if isinstance(node, ast.Import):
|
|
122
|
+
# import X, Y
|
|
123
|
+
for alias in node.names:
|
|
124
|
+
import_name = alias.asname if alias.asname else alias.name
|
|
125
|
+
if import_name in referenced_names:
|
|
126
|
+
needed_imports.append(ast.unparse(node))
|
|
127
|
+
break
|
|
128
|
+
elif isinstance(node, ast.ImportFrom):
|
|
129
|
+
# from X import Y, Z
|
|
130
|
+
imported_names = []
|
|
131
|
+
for alias in node.names:
|
|
132
|
+
import_name = alias.asname if alias.asname else alias.name
|
|
133
|
+
if import_name in referenced_names:
|
|
134
|
+
imported_names.append(alias)
|
|
135
|
+
|
|
136
|
+
if imported_names:
|
|
137
|
+
# Create filtered import statement
|
|
138
|
+
filtered_import = ast.ImportFrom(module=node.module, names=imported_names, level=node.level)
|
|
139
|
+
needed_imports.append(ast.unparse(filtered_import))
|
|
140
|
+
|
|
141
|
+
return '\n'.join(needed_imports)
|
|
142
|
+
except Exception:
|
|
143
|
+
return ''
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _extract_all_params(func: Callable, args: tuple, kwargs: dict) -> dict[str, Any]:
|
|
147
|
+
"""Extract all parameters including explicit params and closure variables
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
func: The function being decorated
|
|
151
|
+
args: Positional arguments passed to the function
|
|
152
|
+
kwargs: Keyword arguments passed to the function
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Dictionary of all parameters {name: value}
|
|
156
|
+
"""
|
|
157
|
+
sig = inspect.signature(func)
|
|
158
|
+
bound_args = sig.bind_partial(*args, **kwargs)
|
|
159
|
+
bound_args.apply_defaults()
|
|
160
|
+
|
|
161
|
+
all_params: dict[str, Any] = {}
|
|
162
|
+
|
|
163
|
+
# 1. Extract explicit parameters (skip 'browser' and 'self')
|
|
164
|
+
for param_name, param_value in bound_args.arguments.items():
|
|
165
|
+
if param_name == 'browser':
|
|
166
|
+
continue
|
|
167
|
+
if param_name == 'self' and hasattr(param_value, '__dict__'):
|
|
168
|
+
# Extract self attributes as individual variables
|
|
169
|
+
for attr_name, attr_value in param_value.__dict__.items():
|
|
170
|
+
all_params[attr_name] = attr_value
|
|
171
|
+
else:
|
|
172
|
+
all_params[param_name] = param_value
|
|
173
|
+
|
|
174
|
+
# 2. Extract closure variables
|
|
175
|
+
if func.__closure__:
|
|
176
|
+
closure_vars = func.__code__.co_freevars
|
|
177
|
+
closure_values = [cell.cell_contents for cell in func.__closure__]
|
|
178
|
+
|
|
179
|
+
for name, value in zip(closure_vars, closure_values):
|
|
180
|
+
# Skip if already captured from explicit params
|
|
181
|
+
if name in all_params:
|
|
182
|
+
continue
|
|
183
|
+
# Special handling for 'self' in closures
|
|
184
|
+
if name == 'self' and hasattr(value, '__dict__'):
|
|
185
|
+
for attr_name, attr_value in value.__dict__.items():
|
|
186
|
+
if attr_name not in all_params:
|
|
187
|
+
all_params[attr_name] = attr_value
|
|
188
|
+
else:
|
|
189
|
+
all_params[name] = value
|
|
190
|
+
|
|
191
|
+
# 3. Extract referenced globals (like logger, module-level vars, etc.)
|
|
192
|
+
# Let cloudpickle handle serialization instead of special-casing
|
|
193
|
+
for name in func.__code__.co_names:
|
|
194
|
+
if name in all_params:
|
|
195
|
+
continue
|
|
196
|
+
if name in func.__globals__:
|
|
197
|
+
all_params[name] = func.__globals__[name]
|
|
198
|
+
|
|
199
|
+
return all_params
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def sandbox(
|
|
203
|
+
BROWSER_USE_API_KEY: str | None = None,
|
|
204
|
+
cloud_profile_id: str | None = None,
|
|
205
|
+
cloud_proxy_country_code: str | None = None,
|
|
206
|
+
cloud_timeout: int | None = None,
|
|
207
|
+
server_url: str | None = None,
|
|
208
|
+
log_level: str = 'INFO',
|
|
209
|
+
quiet: bool = False,
|
|
210
|
+
headers: dict[str, str] | None = None,
|
|
211
|
+
on_browser_created: Callable[[BrowserCreatedData], None]
|
|
212
|
+
| Callable[[BrowserCreatedData], Coroutine[Any, Any, None]]
|
|
213
|
+
| None = None,
|
|
214
|
+
on_instance_ready: Callable[[], None] | Callable[[], Coroutine[Any, Any, None]] | None = None,
|
|
215
|
+
on_log: Callable[[LogData], None] | Callable[[LogData], Coroutine[Any, Any, None]] | None = None,
|
|
216
|
+
on_result: Callable[[ResultData], None] | Callable[[ResultData], Coroutine[Any, Any, None]] | None = None,
|
|
217
|
+
on_error: Callable[[ErrorData], None] | Callable[[ErrorData], Coroutine[Any, Any, None]] | None = None,
|
|
218
|
+
**env_vars: str,
|
|
219
|
+
) -> Callable[[Callable[Concatenate['BrowserSession', P], Coroutine[Any, Any, T]]], Callable[P, Coroutine[Any, Any, T]]]:
|
|
220
|
+
"""Decorator to execute browser automation code in a sandbox environment.
|
|
221
|
+
|
|
222
|
+
The decorated function MUST have 'browser: Browser' as its first parameter.
|
|
223
|
+
The browser parameter will be automatically injected - do NOT pass it when calling the decorated function.
|
|
224
|
+
All other parameters (explicit or from closure) will be captured and sent via cloudpickle.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
BROWSER_USE_API_KEY: API key (defaults to BROWSER_USE_API_KEY env var)
|
|
228
|
+
cloud_profile_id: The ID of the profile to use for the browser session
|
|
229
|
+
cloud_proxy_country_code: Country code for proxy location (e.g., 'us', 'uk', 'fr')
|
|
230
|
+
cloud_timeout: The timeout for the browser session in minutes (max 240 = 4 hours)
|
|
231
|
+
server_url: Sandbox server URL (defaults to https://sandbox.api.browser-use.com/sandbox-stream)
|
|
232
|
+
log_level: Logging level (INFO, DEBUG, WARNING, ERROR)
|
|
233
|
+
quiet: Suppress console output
|
|
234
|
+
headers: Additional HTTP headers to send with the request
|
|
235
|
+
on_browser_created: Callback when browser is created
|
|
236
|
+
on_instance_ready: Callback when instance is ready
|
|
237
|
+
on_log: Callback for log events
|
|
238
|
+
on_result: Callback when execution completes
|
|
239
|
+
on_error: Callback for errors
|
|
240
|
+
**env_vars: Additional environment variables
|
|
241
|
+
|
|
242
|
+
Example:
|
|
243
|
+
@sandbox()
|
|
244
|
+
async def task(browser: Browser, url: str, max_steps: int) -> str:
|
|
245
|
+
agent = Agent(task=url, browser=browser)
|
|
246
|
+
await agent.run(max_steps=max_steps)
|
|
247
|
+
return "done"
|
|
248
|
+
|
|
249
|
+
# Call with:
|
|
250
|
+
result = await task(url="https://example.com", max_steps=10)
|
|
251
|
+
|
|
252
|
+
# With cloud parameters:
|
|
253
|
+
@sandbox(cloud_proxy_country_code='us', cloud_timeout=60)
|
|
254
|
+
async def task_with_proxy(browser: Browser) -> str:
|
|
255
|
+
...
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
def decorator(
|
|
259
|
+
func: Callable[Concatenate['BrowserSession', P], Coroutine[Any, Any, T]],
|
|
260
|
+
) -> Callable[P, Coroutine[Any, Any, T]]:
|
|
261
|
+
# Validate function has browser parameter
|
|
262
|
+
sig = inspect.signature(func)
|
|
263
|
+
if 'browser' not in sig.parameters:
|
|
264
|
+
raise TypeError(f'{func.__name__}() must have a "browser" parameter')
|
|
265
|
+
|
|
266
|
+
browser_param = sig.parameters['browser']
|
|
267
|
+
if browser_param.annotation != inspect.Parameter.empty:
|
|
268
|
+
annotation_str = str(browser_param.annotation)
|
|
269
|
+
if 'Browser' not in annotation_str:
|
|
270
|
+
raise TypeError(f'{func.__name__}() browser parameter must be typed as Browser, got {annotation_str}')
|
|
271
|
+
|
|
272
|
+
@wraps(func)
|
|
273
|
+
async def wrapper(*args, **kwargs) -> T:
|
|
274
|
+
# 1. Get API key
|
|
275
|
+
api_key = BROWSER_USE_API_KEY or os.getenv('BROWSER_USE_API_KEY')
|
|
276
|
+
if not api_key:
|
|
277
|
+
raise SandboxError('BROWSER_USE_API_KEY is required')
|
|
278
|
+
|
|
279
|
+
# 2. Extract all parameters (explicit + closure)
|
|
280
|
+
all_params = _extract_all_params(func, args, kwargs)
|
|
281
|
+
|
|
282
|
+
# 3. Get function source without decorator and only needed imports
|
|
283
|
+
func_source = _get_function_source_without_decorator(func)
|
|
284
|
+
needed_imports = _get_imports_used_in_function(func)
|
|
285
|
+
|
|
286
|
+
# Always include Browser import since it's required for the function signature
|
|
287
|
+
if needed_imports:
|
|
288
|
+
needed_imports = 'from browser_use import Browser\n' + needed_imports
|
|
289
|
+
else:
|
|
290
|
+
needed_imports = 'from browser_use import Browser'
|
|
291
|
+
|
|
292
|
+
# 4. Pickle parameters using cloudpickle for robust serialization
|
|
293
|
+
pickled_params = base64.b64encode(cloudpickle.dumps(all_params)).decode()
|
|
294
|
+
|
|
295
|
+
# 5. Determine which params are in the function signature vs closure/globals
|
|
296
|
+
func_param_names = {p.name for p in sig.parameters.values() if p.name != 'browser'}
|
|
297
|
+
non_explicit_params = {k: v for k, v in all_params.items() if k not in func_param_names}
|
|
298
|
+
explicit_params = {k: v for k, v in all_params.items() if k in func_param_names}
|
|
299
|
+
|
|
300
|
+
# Inject closure variables and globals as module-level vars
|
|
301
|
+
var_injections = []
|
|
302
|
+
for var_name in non_explicit_params.keys():
|
|
303
|
+
var_injections.append(f"{var_name} = _params['{var_name}']")
|
|
304
|
+
|
|
305
|
+
var_injection_code = '\n'.join(var_injections) if var_injections else '# No closure variables or globals'
|
|
306
|
+
|
|
307
|
+
# Build function call
|
|
308
|
+
if explicit_params:
|
|
309
|
+
function_call = (
|
|
310
|
+
f'await {func.__name__}(browser=browser, **{{k: _params[k] for k in {list(explicit_params.keys())!r}}})'
|
|
311
|
+
)
|
|
312
|
+
else:
|
|
313
|
+
function_call = f'await {func.__name__}(browser=browser)'
|
|
314
|
+
|
|
315
|
+
# 6. Create wrapper code that unpickles params and calls function
|
|
316
|
+
execution_code = f"""import cloudpickle
|
|
317
|
+
import base64
|
|
318
|
+
|
|
319
|
+
# Imports used in function
|
|
320
|
+
{needed_imports}
|
|
321
|
+
|
|
322
|
+
# Unpickle all parameters (explicit, closure, and globals)
|
|
323
|
+
_pickled_params = base64.b64decode({repr(pickled_params)})
|
|
324
|
+
_params = cloudpickle.loads(_pickled_params)
|
|
325
|
+
|
|
326
|
+
# Inject closure variables and globals into module scope
|
|
327
|
+
{var_injection_code}
|
|
328
|
+
|
|
329
|
+
# Original function (decorator removed)
|
|
330
|
+
{func_source}
|
|
331
|
+
|
|
332
|
+
# Wrapper function that passes explicit params
|
|
333
|
+
async def run(browser):
|
|
334
|
+
return {function_call}
|
|
335
|
+
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
# 9. Send to server
|
|
339
|
+
payload: dict[str, Any] = {'code': base64.b64encode(execution_code.encode()).decode()}
|
|
340
|
+
|
|
341
|
+
combined_env: dict[str, str] = env_vars.copy() if env_vars else {}
|
|
342
|
+
combined_env['LOG_LEVEL'] = log_level.upper()
|
|
343
|
+
payload['env'] = combined_env
|
|
344
|
+
|
|
345
|
+
# Add cloud parameters if provided
|
|
346
|
+
if cloud_profile_id is not None:
|
|
347
|
+
payload['cloud_profile_id'] = cloud_profile_id
|
|
348
|
+
if cloud_proxy_country_code is not None:
|
|
349
|
+
payload['cloud_proxy_country_code'] = cloud_proxy_country_code
|
|
350
|
+
if cloud_timeout is not None:
|
|
351
|
+
payload['cloud_timeout'] = cloud_timeout
|
|
352
|
+
|
|
353
|
+
url = server_url or 'https://sandbox.api.browser-use.com/sandbox-stream'
|
|
354
|
+
|
|
355
|
+
request_headers = {'X-API-Key': api_key}
|
|
356
|
+
if headers:
|
|
357
|
+
request_headers.update(headers)
|
|
358
|
+
|
|
359
|
+
# 10. Handle SSE streaming
|
|
360
|
+
_NO_RESULT = object()
|
|
361
|
+
execution_result = _NO_RESULT
|
|
362
|
+
live_url_shown = False
|
|
363
|
+
execution_started = False
|
|
364
|
+
received_final_event = False
|
|
365
|
+
|
|
366
|
+
async with httpx.AsyncClient(timeout=1800.0) as client:
|
|
367
|
+
async with client.stream('POST', url, json=payload, headers=request_headers) as response:
|
|
368
|
+
response.raise_for_status()
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
async for line in response.aiter_lines():
|
|
372
|
+
if not line or not line.startswith('data: '):
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
event_json = line[6:]
|
|
376
|
+
try:
|
|
377
|
+
event = SSEEvent.from_json(event_json)
|
|
378
|
+
|
|
379
|
+
if event.type == SSEEventType.BROWSER_CREATED:
|
|
380
|
+
assert isinstance(event.data, BrowserCreatedData)
|
|
381
|
+
|
|
382
|
+
if on_browser_created:
|
|
383
|
+
try:
|
|
384
|
+
await _call_callback(on_browser_created, event.data)
|
|
385
|
+
except Exception as e:
|
|
386
|
+
if not quiet:
|
|
387
|
+
print(f'⚠️ Error in on_browser_created callback: {e}')
|
|
388
|
+
|
|
389
|
+
if not quiet and event.data.live_url and not live_url_shown:
|
|
390
|
+
width = get_terminal_width()
|
|
391
|
+
print('\n' + '━' * width)
|
|
392
|
+
print('👁️ LIVE BROWSER VIEW (Click to watch)')
|
|
393
|
+
print(f'🔗 {event.data.live_url}')
|
|
394
|
+
print('━' * width)
|
|
395
|
+
live_url_shown = True
|
|
396
|
+
|
|
397
|
+
elif event.type == SSEEventType.LOG:
|
|
398
|
+
assert isinstance(event.data, LogData)
|
|
399
|
+
message = event.data.message
|
|
400
|
+
level = event.data.level
|
|
401
|
+
|
|
402
|
+
if on_log:
|
|
403
|
+
try:
|
|
404
|
+
await _call_callback(on_log, event.data)
|
|
405
|
+
except Exception as e:
|
|
406
|
+
if not quiet:
|
|
407
|
+
print(f'⚠️ Error in on_log callback: {e}')
|
|
408
|
+
|
|
409
|
+
if level == 'stdout':
|
|
410
|
+
if not quiet:
|
|
411
|
+
if not execution_started:
|
|
412
|
+
width = get_terminal_width()
|
|
413
|
+
print('\n' + '─' * width)
|
|
414
|
+
print('⚡ Runtime Output')
|
|
415
|
+
print('─' * width)
|
|
416
|
+
execution_started = True
|
|
417
|
+
print(f' {message}', end='')
|
|
418
|
+
elif level == 'stderr':
|
|
419
|
+
if not quiet:
|
|
420
|
+
if not execution_started:
|
|
421
|
+
width = get_terminal_width()
|
|
422
|
+
print('\n' + '─' * width)
|
|
423
|
+
print('⚡ Runtime Output')
|
|
424
|
+
print('─' * width)
|
|
425
|
+
execution_started = True
|
|
426
|
+
print(f'⚠️ {message}', end='', file=sys.stderr)
|
|
427
|
+
elif level == 'info':
|
|
428
|
+
if not quiet:
|
|
429
|
+
if 'credit' in message.lower():
|
|
430
|
+
import re
|
|
431
|
+
|
|
432
|
+
match = re.search(r'\$[\d,]+\.?\d*', message)
|
|
433
|
+
if match:
|
|
434
|
+
print(f'💰 You have {match.group()} credits')
|
|
435
|
+
else:
|
|
436
|
+
print(f'ℹ️ {message}')
|
|
437
|
+
else:
|
|
438
|
+
if not quiet:
|
|
439
|
+
print(f' {message}')
|
|
440
|
+
|
|
441
|
+
elif event.type == SSEEventType.INSTANCE_READY:
|
|
442
|
+
if on_instance_ready:
|
|
443
|
+
try:
|
|
444
|
+
await _call_callback(on_instance_ready)
|
|
445
|
+
except Exception as e:
|
|
446
|
+
if not quiet:
|
|
447
|
+
print(f'⚠️ Error in on_instance_ready callback: {e}')
|
|
448
|
+
|
|
449
|
+
if not quiet:
|
|
450
|
+
print('✅ Browser ready, starting execution...\n')
|
|
451
|
+
|
|
452
|
+
elif event.type == SSEEventType.RESULT:
|
|
453
|
+
assert isinstance(event.data, ResultData)
|
|
454
|
+
exec_response = event.data.execution_response
|
|
455
|
+
received_final_event = True
|
|
456
|
+
|
|
457
|
+
if on_result:
|
|
458
|
+
try:
|
|
459
|
+
await _call_callback(on_result, event.data)
|
|
460
|
+
except Exception as e:
|
|
461
|
+
if not quiet:
|
|
462
|
+
print(f'⚠️ Error in on_result callback: {e}')
|
|
463
|
+
|
|
464
|
+
if exec_response.success:
|
|
465
|
+
execution_result = exec_response.result
|
|
466
|
+
if not quiet and execution_started:
|
|
467
|
+
width = get_terminal_width()
|
|
468
|
+
print('\n' + '─' * width)
|
|
469
|
+
print()
|
|
470
|
+
else:
|
|
471
|
+
error_msg = exec_response.error or 'Unknown error'
|
|
472
|
+
raise SandboxError(f'Execution failed: {error_msg}')
|
|
473
|
+
|
|
474
|
+
elif event.type == SSEEventType.ERROR:
|
|
475
|
+
assert isinstance(event.data, ErrorData)
|
|
476
|
+
received_final_event = True
|
|
477
|
+
|
|
478
|
+
if on_error:
|
|
479
|
+
try:
|
|
480
|
+
await _call_callback(on_error, event.data)
|
|
481
|
+
except Exception as e:
|
|
482
|
+
if not quiet:
|
|
483
|
+
print(f'⚠️ Error in on_error callback: {e}')
|
|
484
|
+
|
|
485
|
+
raise SandboxError(f'Execution failed: {event.data.error}')
|
|
486
|
+
|
|
487
|
+
except (json.JSONDecodeError, ValueError):
|
|
488
|
+
continue
|
|
489
|
+
|
|
490
|
+
except (httpx.RemoteProtocolError, httpx.ReadError, httpx.StreamClosed) as e:
|
|
491
|
+
# With deterministic handshake, these should never happen
|
|
492
|
+
# If they do, it's a real error
|
|
493
|
+
raise SandboxError(
|
|
494
|
+
f'Stream error: {e.__class__.__name__}: {e or "connection closed unexpectedly"}'
|
|
495
|
+
) from e
|
|
496
|
+
|
|
497
|
+
# 11. Parse result with type annotation
|
|
498
|
+
if execution_result is not _NO_RESULT:
|
|
499
|
+
return_annotation = func.__annotations__.get('return')
|
|
500
|
+
if return_annotation:
|
|
501
|
+
parsed_result = _parse_with_type_annotation(execution_result, return_annotation)
|
|
502
|
+
return parsed_result
|
|
503
|
+
return execution_result # type: ignore[return-value]
|
|
504
|
+
|
|
505
|
+
raise SandboxError('No result received from execution')
|
|
506
|
+
|
|
507
|
+
# Update wrapper signature to remove browser parameter
|
|
508
|
+
wrapper.__annotations__ = func.__annotations__.copy()
|
|
509
|
+
if 'browser' in wrapper.__annotations__:
|
|
510
|
+
del wrapper.__annotations__['browser']
|
|
511
|
+
|
|
512
|
+
params = [p for p in sig.parameters.values() if p.name != 'browser']
|
|
513
|
+
wrapper.__signature__ = sig.replace(parameters=params) # type: ignore[attr-defined]
|
|
514
|
+
|
|
515
|
+
return cast(Callable[P, Coroutine[Any, Any, T]], wrapper)
|
|
516
|
+
|
|
517
|
+
return decorator
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _parse_with_type_annotation(data: Any, annotation: Any) -> Any:
|
|
521
|
+
"""Parse data with type annotation without validation, recursively handling nested types
|
|
522
|
+
|
|
523
|
+
This function reconstructs Pydantic models, dataclasses, and enums from JSON dicts
|
|
524
|
+
without running validation logic. It recursively parses nested fields to ensure
|
|
525
|
+
complete type fidelity.
|
|
526
|
+
"""
|
|
527
|
+
try:
|
|
528
|
+
if data is None:
|
|
529
|
+
return None
|
|
530
|
+
|
|
531
|
+
origin = get_origin(annotation)
|
|
532
|
+
args = get_args(annotation)
|
|
533
|
+
|
|
534
|
+
# Handle Union types
|
|
535
|
+
if origin is Union or (hasattr(annotation, '__class__') and annotation.__class__.__name__ == 'UnionType'):
|
|
536
|
+
union_args = args or getattr(annotation, '__args__', [])
|
|
537
|
+
for arg in union_args:
|
|
538
|
+
if arg is type(None) and data is None:
|
|
539
|
+
return None
|
|
540
|
+
if arg is not type(None):
|
|
541
|
+
try:
|
|
542
|
+
return _parse_with_type_annotation(data, arg)
|
|
543
|
+
except Exception:
|
|
544
|
+
continue
|
|
545
|
+
return data
|
|
546
|
+
|
|
547
|
+
# Handle List types
|
|
548
|
+
if origin is list:
|
|
549
|
+
if not isinstance(data, list):
|
|
550
|
+
return data
|
|
551
|
+
if args:
|
|
552
|
+
return [_parse_with_type_annotation(item, args[0]) for item in data]
|
|
553
|
+
return data
|
|
554
|
+
|
|
555
|
+
# Handle Tuple types (JSON serializes tuples as lists)
|
|
556
|
+
if origin is tuple:
|
|
557
|
+
if not isinstance(data, (list, tuple)):
|
|
558
|
+
return data
|
|
559
|
+
if args:
|
|
560
|
+
# Parse each element according to its type annotation
|
|
561
|
+
parsed_items = []
|
|
562
|
+
for i, item in enumerate(data):
|
|
563
|
+
# Use the corresponding type arg, or the last one if fewer args than items
|
|
564
|
+
type_arg = args[i] if i < len(args) else args[-1] if args else Any
|
|
565
|
+
parsed_items.append(_parse_with_type_annotation(item, type_arg))
|
|
566
|
+
return tuple(parsed_items)
|
|
567
|
+
return tuple(data) if isinstance(data, list) else data
|
|
568
|
+
|
|
569
|
+
# Handle Dict types
|
|
570
|
+
if origin is dict:
|
|
571
|
+
if not isinstance(data, dict):
|
|
572
|
+
return data
|
|
573
|
+
if len(args) == 2:
|
|
574
|
+
return {_parse_with_type_annotation(k, args[0]): _parse_with_type_annotation(v, args[1]) for k, v in data.items()}
|
|
575
|
+
return data
|
|
576
|
+
|
|
577
|
+
# Handle Enum types
|
|
578
|
+
if inspect.isclass(annotation) and issubclass(annotation, enum.Enum):
|
|
579
|
+
if isinstance(data, str):
|
|
580
|
+
try:
|
|
581
|
+
return annotation[data] # By name
|
|
582
|
+
except KeyError:
|
|
583
|
+
return annotation(data) # By value
|
|
584
|
+
return annotation(data) # By value
|
|
585
|
+
|
|
586
|
+
# Handle Pydantic v2 - use model_construct to skip validation and recursively parse nested fields
|
|
587
|
+
if hasattr(annotation, 'model_construct'):
|
|
588
|
+
if not isinstance(data, dict):
|
|
589
|
+
return data
|
|
590
|
+
# Recursively parse each field according to its type annotation
|
|
591
|
+
if hasattr(annotation, 'model_fields'):
|
|
592
|
+
parsed_fields = {}
|
|
593
|
+
for field_name, field_info in annotation.model_fields.items():
|
|
594
|
+
if field_name in data:
|
|
595
|
+
field_annotation = field_info.annotation
|
|
596
|
+
parsed_fields[field_name] = _parse_with_type_annotation(data[field_name], field_annotation)
|
|
597
|
+
return annotation.model_construct(**parsed_fields)
|
|
598
|
+
# Fallback if model_fields not available
|
|
599
|
+
return annotation.model_construct(**data)
|
|
600
|
+
|
|
601
|
+
# Handle Pydantic v1 - use construct to skip validation and recursively parse nested fields
|
|
602
|
+
if hasattr(annotation, 'construct'):
|
|
603
|
+
if not isinstance(data, dict):
|
|
604
|
+
return data
|
|
605
|
+
# Recursively parse each field if __fields__ is available
|
|
606
|
+
if hasattr(annotation, '__fields__'):
|
|
607
|
+
parsed_fields = {}
|
|
608
|
+
for field_name, field_obj in annotation.__fields__.items():
|
|
609
|
+
if field_name in data:
|
|
610
|
+
field_annotation = field_obj.outer_type_
|
|
611
|
+
parsed_fields[field_name] = _parse_with_type_annotation(data[field_name], field_annotation)
|
|
612
|
+
return annotation.construct(**parsed_fields)
|
|
613
|
+
# Fallback if __fields__ not available
|
|
614
|
+
return annotation.construct(**data)
|
|
615
|
+
|
|
616
|
+
# Handle dataclasses
|
|
617
|
+
if dataclasses.is_dataclass(annotation) and isinstance(data, dict):
|
|
618
|
+
# Get field type annotations
|
|
619
|
+
field_types = {f.name: f.type for f in dataclasses.fields(annotation)}
|
|
620
|
+
# Recursively parse each field
|
|
621
|
+
parsed_fields = {}
|
|
622
|
+
for field_name, field_type in field_types.items():
|
|
623
|
+
if field_name in data:
|
|
624
|
+
parsed_fields[field_name] = _parse_with_type_annotation(data[field_name], field_type)
|
|
625
|
+
return cast(type[Any], annotation)(**parsed_fields)
|
|
626
|
+
|
|
627
|
+
# Handle regular classes
|
|
628
|
+
if inspect.isclass(annotation) and isinstance(data, dict):
|
|
629
|
+
try:
|
|
630
|
+
return annotation(**data)
|
|
631
|
+
except Exception:
|
|
632
|
+
pass
|
|
633
|
+
|
|
634
|
+
return data
|
|
635
|
+
|
|
636
|
+
except Exception:
|
|
637
|
+
return data
|