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