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,572 @@
1
+ import asyncio
2
+ import functools
3
+ import inspect
4
+ import logging
5
+ import re
6
+ from collections.abc import Callable
7
+ from inspect import Parameter, iscoroutinefunction, signature
8
+ from types import UnionType
9
+ from typing import Any, Generic, Optional, TypeVar, Union, get_args, get_origin
10
+
11
+ import pyotp
12
+ from pydantic import BaseModel, Field, RootModel, create_model
13
+
14
+ from browser_use.browser import BrowserSession
15
+ from browser_use.filesystem.file_system import FileSystem
16
+ from browser_use.llm.base import BaseChatModel
17
+ from browser_use.observability import observe_debug
18
+ from browser_use.telemetry.service import ProductTelemetry
19
+ from browser_use.tools.registry.views import (
20
+ ActionModel,
21
+ ActionRegistry,
22
+ RegisteredAction,
23
+ SpecialActionParameters,
24
+ )
25
+ from browser_use.utils import is_new_tab_page, match_url_with_domain_pattern, time_execution_async
26
+
27
+ Context = TypeVar('Context')
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class Registry(Generic[Context]):
33
+ """Service for registering and managing actions"""
34
+
35
+ def __init__(self, exclude_actions: list[str] | None = None):
36
+ self.registry = ActionRegistry()
37
+ self.telemetry = ProductTelemetry()
38
+ self.exclude_actions = exclude_actions if exclude_actions is not None else []
39
+
40
+ def _get_special_param_types(self) -> dict[str, type | UnionType | None]:
41
+ """Get the expected types for special parameters from SpecialActionParameters"""
42
+ # Manually define the expected types to avoid issues with Optional handling.
43
+ # we should try to reduce this list to 0 if possible, give as few standardized objects to all the actions
44
+ # but each driver should decide what is relevant to expose the action methods,
45
+ # e.g. CDP client, 2fa code getters, sensitive_data wrappers, other context, etc.
46
+ return {
47
+ 'context': None, # Context is a TypeVar, so we can't validate type
48
+ 'browser_session': BrowserSession,
49
+ 'page_url': str,
50
+ 'cdp_client': None, # CDPClient type from cdp_use, but we don't import it here
51
+ 'page_extraction_llm': BaseChatModel,
52
+ 'available_file_paths': list,
53
+ 'has_sensitive_data': bool,
54
+ 'file_system': FileSystem,
55
+ }
56
+
57
+ def _normalize_action_function_signature(
58
+ self,
59
+ func: Callable,
60
+ description: str,
61
+ param_model: type[BaseModel] | None = None,
62
+ ) -> tuple[Callable, type[BaseModel]]:
63
+ """
64
+ Normalize action function to accept only kwargs.
65
+
66
+ Returns:
67
+ - Normalized function that accepts (*_, params: ParamModel, **special_params)
68
+ - The param model to use for registration
69
+ """
70
+ sig = signature(func)
71
+ parameters = list(sig.parameters.values())
72
+ special_param_types = self._get_special_param_types()
73
+ special_param_names = set(special_param_types.keys())
74
+
75
+ # Step 1: Validate no **kwargs in original function signature
76
+ # if it needs default values it must use a dedicated param_model: BaseModel instead
77
+ for param in parameters:
78
+ if param.kind == Parameter.VAR_KEYWORD:
79
+ raise ValueError(
80
+ f"Action '{func.__name__}' has **{param.name} which is not allowed. "
81
+ f'Actions must have explicit positional parameters only.'
82
+ )
83
+
84
+ # Step 2: Separate special and action parameters
85
+ action_params = []
86
+ special_params = []
87
+ param_model_provided = param_model is not None
88
+
89
+ for i, param in enumerate(parameters):
90
+ # Check if this is a Type 1 pattern (first param is BaseModel)
91
+ if i == 0 and param_model_provided and param.name not in special_param_names:
92
+ # This is Type 1 pattern - skip the params argument
93
+ continue
94
+
95
+ if param.name in special_param_names:
96
+ # Validate special parameter type
97
+ expected_type = special_param_types.get(param.name)
98
+ if param.annotation != Parameter.empty and expected_type is not None:
99
+ # Handle Optional types - normalize both sides
100
+ param_type = param.annotation
101
+ origin = get_origin(param_type)
102
+ if origin is Union:
103
+ args = get_args(param_type)
104
+ # Find non-None type
105
+ param_type = next((arg for arg in args if arg is not type(None)), param_type)
106
+
107
+ # Check if types are compatible (exact match, subclass, or generic list)
108
+ types_compatible = (
109
+ param_type == expected_type
110
+ or (
111
+ inspect.isclass(param_type)
112
+ and inspect.isclass(expected_type)
113
+ and issubclass(param_type, expected_type)
114
+ )
115
+ or
116
+ # Handle list[T] vs list comparison
117
+ (expected_type is list and (param_type is list or get_origin(param_type) is list))
118
+ )
119
+
120
+ if not types_compatible:
121
+ expected_type_name = getattr(expected_type, '__name__', str(expected_type))
122
+ param_type_name = getattr(param_type, '__name__', str(param_type))
123
+ raise ValueError(
124
+ f"Action '{func.__name__}' parameter '{param.name}: {param_type_name}' "
125
+ f"conflicts with special argument injected by tools: '{param.name}: {expected_type_name}'"
126
+ )
127
+ special_params.append(param)
128
+ else:
129
+ action_params.append(param)
130
+
131
+ # Step 3: Create or validate param model
132
+ if not param_model_provided:
133
+ # Type 2: Generate param model from action params
134
+ if action_params:
135
+ params_dict = {}
136
+ for param in action_params:
137
+ annotation = param.annotation if param.annotation != Parameter.empty else str
138
+ default = ... if param.default == Parameter.empty else param.default
139
+ params_dict[param.name] = (annotation, default)
140
+
141
+ param_model = create_model(f'{func.__name__}_Params', __base__=ActionModel, **params_dict)
142
+ else:
143
+ # No action params, create empty model
144
+ param_model = create_model(
145
+ f'{func.__name__}_Params',
146
+ __base__=ActionModel,
147
+ )
148
+ assert param_model is not None, f'param_model is None for {func.__name__}'
149
+
150
+ # Step 4: Create normalized wrapper function
151
+ @functools.wraps(func)
152
+ async def normalized_wrapper(*args, params: BaseModel | None = None, **kwargs):
153
+ """Normalized action that only accepts kwargs"""
154
+ # Validate no positional args
155
+ if args:
156
+ raise TypeError(f'{func.__name__}() does not accept positional arguments, only keyword arguments are allowed')
157
+
158
+ # Prepare arguments for original function
159
+ call_args = []
160
+ call_kwargs = {}
161
+
162
+ # Handle Type 1 pattern (first arg is the param model)
163
+ if param_model_provided and parameters and parameters[0].name not in special_param_names:
164
+ if params is None:
165
+ raise ValueError(f"{func.__name__}() missing required 'params' argument")
166
+ # For Type 1, we'll use the params object as first argument
167
+ pass
168
+ else:
169
+ # Type 2 pattern - need to unpack params
170
+ # If params is None, try to create it from kwargs
171
+ if params is None and action_params:
172
+ # Extract action params from kwargs
173
+ action_kwargs = {}
174
+ for param in action_params:
175
+ if param.name in kwargs:
176
+ action_kwargs[param.name] = kwargs[param.name]
177
+ if action_kwargs:
178
+ # Use the param_model which has the correct types defined
179
+ params = param_model(**action_kwargs)
180
+
181
+ # Build call_args by iterating through original function parameters in order
182
+ params_dict = params.model_dump() if params is not None else {}
183
+
184
+ for i, param in enumerate(parameters):
185
+ # Skip first param for Type 1 pattern (it's the model itself)
186
+ if param_model_provided and i == 0 and param.name not in special_param_names:
187
+ call_args.append(params)
188
+ elif param.name in special_param_names:
189
+ # This is a special parameter
190
+ if param.name in kwargs:
191
+ value = kwargs[param.name]
192
+ # Check if required special param is None
193
+ if value is None and param.default == Parameter.empty:
194
+ if param.name == 'browser_session':
195
+ raise ValueError(f'Action {func.__name__} requires browser_session but none provided.')
196
+ elif param.name == 'page_extraction_llm':
197
+ raise ValueError(f'Action {func.__name__} requires page_extraction_llm but none provided.')
198
+ elif param.name == 'file_system':
199
+ raise ValueError(f'Action {func.__name__} requires file_system but none provided.')
200
+ elif param.name == 'page':
201
+ raise ValueError(f'Action {func.__name__} requires page but none provided.')
202
+ elif param.name == 'available_file_paths':
203
+ raise ValueError(f'Action {func.__name__} requires available_file_paths but none provided.')
204
+ elif param.name == 'file_system':
205
+ raise ValueError(f'Action {func.__name__} requires file_system but none provided.')
206
+ else:
207
+ raise ValueError(f"{func.__name__}() missing required special parameter '{param.name}'")
208
+ call_args.append(value)
209
+ elif param.default != Parameter.empty:
210
+ call_args.append(param.default)
211
+ else:
212
+ # Special param is required but not provided
213
+ if param.name == 'browser_session':
214
+ raise ValueError(f'Action {func.__name__} requires browser_session but none provided.')
215
+ elif param.name == 'page_extraction_llm':
216
+ raise ValueError(f'Action {func.__name__} requires page_extraction_llm but none provided.')
217
+ elif param.name == 'file_system':
218
+ raise ValueError(f'Action {func.__name__} requires file_system but none provided.')
219
+ elif param.name == 'page':
220
+ raise ValueError(f'Action {func.__name__} requires page but none provided.')
221
+ elif param.name == 'available_file_paths':
222
+ raise ValueError(f'Action {func.__name__} requires available_file_paths but none provided.')
223
+ elif param.name == 'file_system':
224
+ raise ValueError(f'Action {func.__name__} requires file_system but none provided.')
225
+ else:
226
+ raise ValueError(f"{func.__name__}() missing required special parameter '{param.name}'")
227
+ else:
228
+ # This is an action parameter
229
+ if param.name in params_dict:
230
+ call_args.append(params_dict[param.name])
231
+ elif param.default != Parameter.empty:
232
+ call_args.append(param.default)
233
+ else:
234
+ raise ValueError(f"{func.__name__}() missing required parameter '{param.name}'")
235
+
236
+ # Call original function with positional args
237
+ if iscoroutinefunction(func):
238
+ return await func(*call_args)
239
+ else:
240
+ return await asyncio.to_thread(func, *call_args)
241
+
242
+ # Update wrapper signature to be kwargs-only
243
+ new_params = [Parameter('params', Parameter.KEYWORD_ONLY, default=None, annotation=Optional[param_model])]
244
+
245
+ # Add special params as keyword-only
246
+ for sp in special_params:
247
+ new_params.append(Parameter(sp.name, Parameter.KEYWORD_ONLY, default=sp.default, annotation=sp.annotation))
248
+
249
+ # Add **kwargs to accept and ignore extra params
250
+ new_params.append(Parameter('kwargs', Parameter.VAR_KEYWORD))
251
+
252
+ normalized_wrapper.__signature__ = sig.replace(parameters=new_params) # type: ignore[attr-defined]
253
+
254
+ return normalized_wrapper, param_model
255
+
256
+ # @time_execution_sync('--create_param_model')
257
+ def _create_param_model(self, function: Callable) -> type[BaseModel]:
258
+ """Creates a Pydantic model from function signature"""
259
+ sig = signature(function)
260
+ special_param_names = set(SpecialActionParameters.model_fields.keys())
261
+ params = {
262
+ name: (param.annotation, ... if param.default == param.empty else param.default)
263
+ for name, param in sig.parameters.items()
264
+ if name not in special_param_names
265
+ }
266
+ # TODO: make the types here work
267
+ return create_model(
268
+ f'{function.__name__}_parameters',
269
+ __base__=ActionModel,
270
+ **params, # type: ignore
271
+ )
272
+
273
+ def action(
274
+ self,
275
+ description: str,
276
+ param_model: type[BaseModel] | None = None,
277
+ domains: list[str] | None = None,
278
+ allowed_domains: list[str] | None = None,
279
+ ):
280
+ """Decorator for registering actions"""
281
+ # Handle aliases: domains and allowed_domains are the same parameter
282
+ if allowed_domains is not None and domains is not None:
283
+ raise ValueError("Cannot specify both 'domains' and 'allowed_domains' - they are aliases for the same parameter")
284
+
285
+ final_domains = allowed_domains if allowed_domains is not None else domains
286
+
287
+ def decorator(func: Callable):
288
+ # Skip registration if action is in exclude_actions
289
+ if func.__name__ in self.exclude_actions:
290
+ return func
291
+
292
+ # Normalize the function signature
293
+ normalized_func, actual_param_model = self._normalize_action_function_signature(func, description, param_model)
294
+
295
+ action = RegisteredAction(
296
+ name=func.__name__,
297
+ description=description,
298
+ function=normalized_func,
299
+ param_model=actual_param_model,
300
+ domains=final_domains,
301
+ )
302
+ self.registry.actions[func.__name__] = action
303
+
304
+ # Return the normalized function so it can be called with kwargs
305
+ return normalized_func
306
+
307
+ return decorator
308
+
309
+ @observe_debug(ignore_input=True, ignore_output=True, name='execute_action')
310
+ @time_execution_async('--execute_action')
311
+ async def execute_action(
312
+ self,
313
+ action_name: str,
314
+ params: dict,
315
+ browser_session: BrowserSession | None = None,
316
+ page_extraction_llm: BaseChatModel | None = None,
317
+ file_system: FileSystem | None = None,
318
+ sensitive_data: dict[str, str | dict[str, str]] | None = None,
319
+ available_file_paths: list[str] | None = None,
320
+ ) -> Any:
321
+ """Execute a registered action with simplified parameter handling"""
322
+ if action_name not in self.registry.actions:
323
+ raise ValueError(f'Action {action_name} not found')
324
+
325
+ action = self.registry.actions[action_name]
326
+ try:
327
+ # Create the validated Pydantic model
328
+ try:
329
+ validated_params = action.param_model(**params)
330
+ except Exception as e:
331
+ raise ValueError(f'Invalid parameters {params} for action {action_name}: {type(e)}: {e}') from e
332
+
333
+ if sensitive_data:
334
+ # Get current URL if browser_session is provided
335
+ current_url = None
336
+ if browser_session and browser_session.current_target_id:
337
+ try:
338
+ # Get current page info using CDP
339
+ targets = await browser_session.cdp_client.send.Target.getTargets()
340
+ for target in targets.get('targetInfos', []):
341
+ if target.get('targetId') == browser_session.current_target_id:
342
+ current_url = target.get('url')
343
+ break
344
+ except Exception:
345
+ pass
346
+ validated_params = self._replace_sensitive_data(validated_params, sensitive_data, current_url)
347
+
348
+ # Build special context dict
349
+ special_context = {
350
+ 'browser_session': browser_session,
351
+ 'page_extraction_llm': page_extraction_llm,
352
+ 'available_file_paths': available_file_paths,
353
+ 'has_sensitive_data': action_name == 'input' and bool(sensitive_data),
354
+ 'file_system': file_system,
355
+ }
356
+
357
+ # Only pass sensitive_data to actions that explicitly need it (input)
358
+ if action_name == 'input':
359
+ special_context['sensitive_data'] = sensitive_data
360
+
361
+ # Add CDP-related parameters if browser_session is available
362
+ if browser_session:
363
+ # Add page_url
364
+ try:
365
+ special_context['page_url'] = await browser_session.get_current_page_url()
366
+ except Exception:
367
+ special_context['page_url'] = None
368
+
369
+ # Add cdp_client
370
+ special_context['cdp_client'] = browser_session.cdp_client
371
+
372
+ # All functions are now normalized to accept kwargs only
373
+ # Call with params and unpacked special context
374
+ try:
375
+ return await action.function(params=validated_params, **special_context)
376
+ except Exception as e:
377
+ raise
378
+
379
+ except ValueError as e:
380
+ # Preserve ValueError messages from validation
381
+ if 'requires browser_session but none provided' in str(e) or 'requires page_extraction_llm but none provided' in str(
382
+ e
383
+ ):
384
+ raise RuntimeError(str(e)) from e
385
+ else:
386
+ raise RuntimeError(f'Error executing action {action_name}: {str(e)}') from e
387
+ except TimeoutError as e:
388
+ raise RuntimeError(f'Error executing action {action_name} due to timeout.') from e
389
+ except Exception as e:
390
+ raise RuntimeError(f'Error executing action {action_name}: {str(e)}') from e
391
+
392
+ def _log_sensitive_data_usage(self, placeholders_used: set[str], current_url: str | None) -> None:
393
+ """Log when sensitive data is being used on a page"""
394
+ if placeholders_used:
395
+ url_info = f' on {current_url}' if current_url and not is_new_tab_page(current_url) else ''
396
+ logger.info(f'🔒 Using sensitive data placeholders: {", ".join(sorted(placeholders_used))}{url_info}')
397
+
398
+ def _replace_sensitive_data(
399
+ self, params: BaseModel, sensitive_data: dict[str, Any], current_url: str | None = None
400
+ ) -> BaseModel:
401
+ """
402
+ Replaces sensitive data placeholders in params with actual values.
403
+
404
+ Args:
405
+ params: The parameter object containing <secret>placeholder</secret> tags
406
+ sensitive_data: Dictionary of sensitive data, either in old format {key: value}
407
+ or new format {domain_pattern: {key: value}}
408
+ current_url: Optional current URL for domain matching
409
+
410
+ Returns:
411
+ BaseModel: The parameter object with placeholders replaced by actual values
412
+ """
413
+ secret_pattern = re.compile(r'<secret>(.*?)</secret>')
414
+
415
+ # Set to track all missing placeholders across the full object
416
+ all_missing_placeholders = set()
417
+ # Set to track successfully replaced placeholders
418
+ replaced_placeholders = set()
419
+
420
+ # Process sensitive data based on format and current URL
421
+ applicable_secrets = {}
422
+
423
+ for domain_or_key, content in sensitive_data.items():
424
+ if isinstance(content, dict):
425
+ # New format: {domain_pattern: {key: value}}
426
+ # Only include secrets for domains that match the current URL
427
+ if current_url and not is_new_tab_page(current_url):
428
+ # it's a real url, check it using our custom allowed_domains scheme://*.example.com glob matching
429
+ if match_url_with_domain_pattern(current_url, domain_or_key):
430
+ applicable_secrets.update(content)
431
+ else:
432
+ # Old format: {key: value}, expose to all domains (only allowed for legacy reasons)
433
+ applicable_secrets[domain_or_key] = content
434
+
435
+ # Filter out empty values
436
+ applicable_secrets = {k: v for k, v in applicable_secrets.items() if v}
437
+
438
+ def recursively_replace_secrets(value: str | dict | list) -> str | dict | list:
439
+ if isinstance(value, str):
440
+ matches = secret_pattern.findall(value)
441
+ # check if the placeholder key, like x_password is in the output parameters of the LLM and replace it with the sensitive data
442
+ for placeholder in matches:
443
+ if placeholder in applicable_secrets:
444
+ # generate a totp code if secret is a 2fa secret
445
+ if 'bu_2fa_code' in placeholder:
446
+ totp = pyotp.TOTP(applicable_secrets[placeholder], digits=6)
447
+ replacement_value = totp.now()
448
+ else:
449
+ replacement_value = applicable_secrets[placeholder]
450
+
451
+ value = value.replace(f'<secret>{placeholder}</secret>', replacement_value)
452
+ replaced_placeholders.add(placeholder)
453
+ else:
454
+ # Keep track of missing placeholders
455
+ all_missing_placeholders.add(placeholder)
456
+ # Don't replace the tag, keep it as is
457
+
458
+ return value
459
+ elif isinstance(value, dict):
460
+ return {k: recursively_replace_secrets(v) for k, v in value.items()}
461
+ elif isinstance(value, list):
462
+ return [recursively_replace_secrets(v) for v in value]
463
+ return value
464
+
465
+ params_dump = params.model_dump()
466
+ processed_params = recursively_replace_secrets(params_dump)
467
+
468
+ # Log sensitive data usage
469
+ self._log_sensitive_data_usage(replaced_placeholders, current_url)
470
+
471
+ # Log a warning if any placeholders are missing
472
+ if all_missing_placeholders:
473
+ logger.warning(f'Missing or empty keys in sensitive_data dictionary: {", ".join(all_missing_placeholders)}')
474
+
475
+ return type(params).model_validate(processed_params)
476
+
477
+ # @time_execution_sync('--create_action_model')
478
+ def create_action_model(self, include_actions: list[str] | None = None, page_url: str | None = None) -> type[ActionModel]:
479
+ """Creates a Union of individual action models from registered actions,
480
+ used by LLM APIs that support tool calling & enforce a schema.
481
+
482
+ Each action model contains only the specific action being used,
483
+ rather than all actions with most set to None.
484
+ """
485
+ from typing import Union
486
+
487
+ # Filter actions based on page_url if provided:
488
+ # if page_url is None, only include actions with no filters
489
+ # if page_url is provided, only include actions that match the URL
490
+
491
+ available_actions: dict[str, RegisteredAction] = {}
492
+ for name, action in self.registry.actions.items():
493
+ if include_actions is not None and name not in include_actions:
494
+ continue
495
+
496
+ # If no page_url provided, only include actions with no filters
497
+ if page_url is None:
498
+ if action.domains is None:
499
+ available_actions[name] = action
500
+ continue
501
+
502
+ # Check domain filter if present
503
+ domain_is_allowed = self.registry._match_domains(action.domains, page_url)
504
+
505
+ # Include action if domain filter matches
506
+ if domain_is_allowed:
507
+ available_actions[name] = action
508
+
509
+ # Create individual action models for each action
510
+ individual_action_models: list[type[BaseModel]] = []
511
+
512
+ for name, action in available_actions.items():
513
+ # Create an individual model for each action that contains only one field
514
+ individual_model = create_model(
515
+ f'{name.title().replace("_", "")}ActionModel',
516
+ __base__=ActionModel,
517
+ **{
518
+ name: (
519
+ action.param_model,
520
+ Field(description=action.description),
521
+ ) # type: ignore
522
+ },
523
+ )
524
+ individual_action_models.append(individual_model)
525
+
526
+ # If no actions available, return empty ActionModel
527
+ if not individual_action_models:
528
+ return create_model('EmptyActionModel', __base__=ActionModel)
529
+
530
+ # Create proper Union type that maintains ActionModel interface
531
+ if len(individual_action_models) == 1:
532
+ # If only one action, return it directly (no Union needed)
533
+ result_model = individual_action_models[0]
534
+
535
+ # Meaning the length is more than 1
536
+ else:
537
+ # Create a Union type using RootModel that properly delegates ActionModel methods
538
+ union_type = Union[tuple(individual_action_models)] # type: ignore : Typing doesn't understand that the length is >= 2 (by design)
539
+
540
+ class ActionModelUnion(RootModel[union_type]): # type: ignore
541
+ def get_index(self) -> int | None:
542
+ """Delegate get_index to the underlying action model"""
543
+ if hasattr(self.root, 'get_index'):
544
+ return self.root.get_index() # type: ignore
545
+ return None
546
+
547
+ def set_index(self, index: int):
548
+ """Delegate set_index to the underlying action model"""
549
+ if hasattr(self.root, 'set_index'):
550
+ self.root.set_index(index) # type: ignore
551
+
552
+ def model_dump(self, **kwargs):
553
+ """Delegate model_dump to the underlying action model"""
554
+ if hasattr(self.root, 'model_dump'):
555
+ return self.root.model_dump(**kwargs) # type: ignore
556
+ return super().model_dump(**kwargs)
557
+
558
+ # Set the name for better debugging
559
+ ActionModelUnion.__name__ = 'ActionModel'
560
+ ActionModelUnion.__qualname__ = 'ActionModel'
561
+
562
+ result_model = ActionModelUnion
563
+
564
+ return result_model # type:ignore
565
+
566
+ def get_prompt_description(self, page_url: str | None = None) -> str:
567
+ """Get a description of all actions for the prompt
568
+
569
+ If page_url is provided, only include actions that are available for that URL
570
+ based on their domain filters
571
+ """
572
+ return self.registry.get_prompt_description(page_url=page_url)