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
browser_use/config.py ADDED
@@ -0,0 +1,505 @@
1
+ """Configuration system for browser-use with automatic migration support."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ from datetime import datetime
7
+ from functools import cache
8
+ from pathlib import Path
9
+ from typing import Any
10
+ from uuid import uuid4
11
+
12
+ import psutil
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+ from pydantic_settings import BaseSettings, SettingsConfigDict
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @cache
20
+ def is_running_in_docker() -> bool:
21
+ """Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)"""
22
+ try:
23
+ if Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():
24
+ return True
25
+ except Exception:
26
+ pass
27
+
28
+ try:
29
+ # if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker
30
+ # if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker
31
+ init_cmd = ' '.join(psutil.Process(1).cmdline())
32
+ if ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):
33
+ return True
34
+ except Exception:
35
+ pass
36
+
37
+ try:
38
+ # if less than 10 total running procs, then we're almost certainly in a container
39
+ if len(psutil.pids()) < 10:
40
+ return True
41
+ except Exception:
42
+ pass
43
+
44
+ return False
45
+
46
+
47
+ class OldConfig:
48
+ """Original lazy-loading configuration class for environment variables."""
49
+
50
+ # Cache for directory creation tracking
51
+ _dirs_created = False
52
+
53
+ @property
54
+ def BROWSER_USE_LOGGING_LEVEL(self) -> str:
55
+ return os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()
56
+
57
+ @property
58
+ def ANONYMIZED_TELEMETRY(self) -> bool:
59
+ return os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'
60
+
61
+ @property
62
+ def BROWSER_USE_CLOUD_SYNC(self) -> bool:
63
+ return os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'
64
+
65
+ @property
66
+ def BROWSER_USE_CLOUD_API_URL(self) -> str:
67
+ url = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')
68
+ assert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'
69
+ return url
70
+
71
+ @property
72
+ def BROWSER_USE_CLOUD_UI_URL(self) -> str:
73
+ url = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')
74
+ # Allow empty string as default, only validate if set
75
+ if url and '://' not in url:
76
+ raise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')
77
+ return url
78
+
79
+ # Path configuration
80
+ @property
81
+ def XDG_CACHE_HOME(self) -> Path:
82
+ return Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()
83
+
84
+ @property
85
+ def XDG_CONFIG_HOME(self) -> Path:
86
+ return Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()
87
+
88
+ @property
89
+ def BROWSER_USE_CONFIG_DIR(self) -> Path:
90
+ path = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()
91
+ self._ensure_dirs()
92
+ return path
93
+
94
+ @property
95
+ def BROWSER_USE_CONFIG_FILE(self) -> Path:
96
+ return self.BROWSER_USE_CONFIG_DIR / 'config.json'
97
+
98
+ @property
99
+ def BROWSER_USE_PROFILES_DIR(self) -> Path:
100
+ path = self.BROWSER_USE_CONFIG_DIR / 'profiles'
101
+ self._ensure_dirs()
102
+ return path
103
+
104
+ @property
105
+ def BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:
106
+ return self.BROWSER_USE_PROFILES_DIR / 'default'
107
+
108
+ @property
109
+ def BROWSER_USE_EXTENSIONS_DIR(self) -> Path:
110
+ path = self.BROWSER_USE_CONFIG_DIR / 'extensions'
111
+ self._ensure_dirs()
112
+ return path
113
+
114
+ def _ensure_dirs(self) -> None:
115
+ """Create directories if they don't exist (only once)"""
116
+ if not self._dirs_created:
117
+ config_dir = (
118
+ Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()
119
+ )
120
+ config_dir.mkdir(parents=True, exist_ok=True)
121
+ (config_dir / 'profiles').mkdir(parents=True, exist_ok=True)
122
+ (config_dir / 'extensions').mkdir(parents=True, exist_ok=True)
123
+ self._dirs_created = True
124
+
125
+ # LLM API key configuration
126
+ @property
127
+ def OPENAI_API_KEY(self) -> str:
128
+ return os.getenv('OPENAI_API_KEY', '')
129
+
130
+ @property
131
+ def ANTHROPIC_API_KEY(self) -> str:
132
+ return os.getenv('ANTHROPIC_API_KEY', '')
133
+
134
+ @property
135
+ def GOOGLE_API_KEY(self) -> str:
136
+ return os.getenv('GOOGLE_API_KEY', '')
137
+
138
+ @property
139
+ def DEEPSEEK_API_KEY(self) -> str:
140
+ return os.getenv('DEEPSEEK_API_KEY', '')
141
+
142
+ @property
143
+ def GROK_API_KEY(self) -> str:
144
+ return os.getenv('GROK_API_KEY', '')
145
+
146
+ @property
147
+ def NOVITA_API_KEY(self) -> str:
148
+ return os.getenv('NOVITA_API_KEY', '')
149
+
150
+ @property
151
+ def AZURE_OPENAI_ENDPOINT(self) -> str:
152
+ return os.getenv('AZURE_OPENAI_ENDPOINT', '')
153
+
154
+ @property
155
+ def AZURE_OPENAI_KEY(self) -> str:
156
+ return os.getenv('AZURE_OPENAI_KEY', '')
157
+
158
+ @property
159
+ def SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:
160
+ return os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'
161
+
162
+ @property
163
+ def DEFAULT_LLM(self) -> str:
164
+ return os.getenv('DEFAULT_LLM', '')
165
+
166
+ # Runtime hints
167
+ @property
168
+ def IN_DOCKER(self) -> bool:
169
+ return os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()
170
+
171
+ @property
172
+ def IS_IN_EVALS(self) -> bool:
173
+ return os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'
174
+
175
+ @property
176
+ def WIN_FONT_DIR(self) -> str:
177
+ return os.getenv('WIN_FONT_DIR', 'C:\\Windows\\Fonts')
178
+
179
+
180
+ class FlatEnvConfig(BaseSettings):
181
+ """All environment variables in a flat namespace."""
182
+
183
+ model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')
184
+
185
+ # Logging and telemetry
186
+ BROWSER_USE_LOGGING_LEVEL: str = Field(default='info')
187
+ CDP_LOGGING_LEVEL: str = Field(default='WARNING')
188
+ BROWSER_USE_DEBUG_LOG_FILE: str | None = Field(default=None)
189
+ BROWSER_USE_INFO_LOG_FILE: str | None = Field(default=None)
190
+ ANONYMIZED_TELEMETRY: bool = Field(default=True)
191
+ BROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)
192
+ BROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')
193
+ BROWSER_USE_CLOUD_UI_URL: str = Field(default='')
194
+
195
+ # Path configuration
196
+ XDG_CACHE_HOME: str = Field(default='~/.cache')
197
+ XDG_CONFIG_HOME: str = Field(default='~/.config')
198
+ BROWSER_USE_CONFIG_DIR: str | None = Field(default=None)
199
+
200
+ # LLM API keys
201
+ OPENAI_API_KEY: str = Field(default='')
202
+ ANTHROPIC_API_KEY: str = Field(default='')
203
+ GOOGLE_API_KEY: str = Field(default='')
204
+ DEEPSEEK_API_KEY: str = Field(default='')
205
+ GROK_API_KEY: str = Field(default='')
206
+ NOVITA_API_KEY: str = Field(default='')
207
+ AZURE_OPENAI_ENDPOINT: str = Field(default='')
208
+ AZURE_OPENAI_KEY: str = Field(default='')
209
+ SKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)
210
+ DEFAULT_LLM: str = Field(default='')
211
+
212
+ # Runtime hints
213
+ IN_DOCKER: bool | None = Field(default=None)
214
+ IS_IN_EVALS: bool = Field(default=False)
215
+ WIN_FONT_DIR: str = Field(default='C:\\Windows\\Fonts')
216
+
217
+ # MCP-specific env vars
218
+ BROWSER_USE_CONFIG_PATH: str | None = Field(default=None)
219
+ BROWSER_USE_HEADLESS: bool | None = Field(default=None)
220
+ BROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)
221
+ BROWSER_USE_LLM_MODEL: str | None = Field(default=None)
222
+
223
+ # Proxy env vars
224
+ BROWSER_USE_PROXY_URL: str | None = Field(default=None)
225
+ BROWSER_USE_NO_PROXY: str | None = Field(default=None)
226
+ BROWSER_USE_PROXY_USERNAME: str | None = Field(default=None)
227
+ BROWSER_USE_PROXY_PASSWORD: str | None = Field(default=None)
228
+
229
+
230
+ class DBStyleEntry(BaseModel):
231
+ """Database-style entry with UUID and metadata."""
232
+
233
+ id: str = Field(default_factory=lambda: str(uuid4()))
234
+ default: bool = Field(default=False)
235
+ created_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
236
+
237
+
238
+ class BrowserProfileEntry(DBStyleEntry):
239
+ """Browser profile configuration entry - accepts any BrowserProfile fields."""
240
+
241
+ model_config = ConfigDict(extra='allow')
242
+
243
+ # Common browser profile fields for reference
244
+ headless: bool | None = None
245
+ user_data_dir: str | None = None
246
+ allowed_domains: list[str] | None = None
247
+ downloads_path: str | None = None
248
+
249
+
250
+ class LLMEntry(DBStyleEntry):
251
+ """LLM configuration entry."""
252
+
253
+ api_key: str | None = None
254
+ model: str | None = None
255
+ temperature: float | None = None
256
+ max_tokens: int | None = None
257
+
258
+
259
+ class AgentEntry(DBStyleEntry):
260
+ """Agent configuration entry."""
261
+
262
+ max_steps: int | None = None
263
+ use_vision: bool | None = None
264
+ system_prompt: str | None = None
265
+
266
+
267
+ class DBStyleConfigJSON(BaseModel):
268
+ """New database-style configuration format."""
269
+
270
+ browser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)
271
+ llm: dict[str, LLMEntry] = Field(default_factory=dict)
272
+ agent: dict[str, AgentEntry] = Field(default_factory=dict)
273
+
274
+
275
+ def create_default_config() -> DBStyleConfigJSON:
276
+ """Create a fresh default configuration."""
277
+ logger.debug('Creating fresh default config.json')
278
+
279
+ new_config = DBStyleConfigJSON()
280
+
281
+ # Generate default IDs
282
+ profile_id = str(uuid4())
283
+ llm_id = str(uuid4())
284
+ agent_id = str(uuid4())
285
+
286
+ # Create default browser profile entry
287
+ new_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)
288
+
289
+ # Create default LLM entry
290
+ new_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4.1-mini', api_key='your-openai-api-key-here')
291
+
292
+ # Create default agent entry
293
+ new_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)
294
+
295
+ return new_config
296
+
297
+
298
+ def load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:
299
+ """Load config.json or create fresh one if old format detected."""
300
+ if not config_path.exists():
301
+ # Create fresh config with defaults
302
+ config_path.parent.mkdir(parents=True, exist_ok=True)
303
+ new_config = create_default_config()
304
+ with open(config_path, 'w') as f:
305
+ json.dump(new_config.model_dump(), f, indent=2)
306
+ return new_config
307
+
308
+ try:
309
+ with open(config_path) as f:
310
+ data = json.load(f)
311
+
312
+ # Check if it's already in DB-style format
313
+ if all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(
314
+ isinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']
315
+ ):
316
+ # Check if the values are DB-style entries (have UUIDs as keys)
317
+ if data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):
318
+ # Already in new format
319
+ return DBStyleConfigJSON(**data)
320
+
321
+ # Old format detected - delete it and create fresh config
322
+ logger.debug(f'Old config format detected at {config_path}, creating fresh config')
323
+ new_config = create_default_config()
324
+
325
+ # Overwrite with new config
326
+ with open(config_path, 'w') as f:
327
+ json.dump(new_config.model_dump(), f, indent=2)
328
+
329
+ logger.debug(f'Created fresh config.json at {config_path}')
330
+ return new_config
331
+
332
+ except Exception as e:
333
+ logger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')
334
+ # On any error, create fresh config
335
+ new_config = create_default_config()
336
+ try:
337
+ with open(config_path, 'w') as f:
338
+ json.dump(new_config.model_dump(), f, indent=2)
339
+ except Exception as write_error:
340
+ logger.error(f'Failed to write fresh config: {write_error}')
341
+ return new_config
342
+
343
+
344
+ class Config:
345
+ """Backward-compatible configuration class that merges all config sources.
346
+
347
+ Re-reads environment variables on every access to maintain compatibility.
348
+ """
349
+
350
+ def __init__(self):
351
+ # Cache for directory creation tracking only
352
+ self._dirs_created = False
353
+
354
+ def __getattr__(self, name: str) -> Any:
355
+ """Dynamically proxy all attributes to fresh instances.
356
+
357
+ This ensures env vars are re-read on every access.
358
+ """
359
+ # Special handling for internal attributes
360
+ if name.startswith('_'):
361
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
362
+
363
+ # Create fresh instances on every access
364
+ old_config = OldConfig()
365
+
366
+ # Always use old config for all attributes (it handles env vars with proper transformations)
367
+ if hasattr(old_config, name):
368
+ return getattr(old_config, name)
369
+
370
+ # For new MCP-specific attributes not in old config
371
+ env_config = FlatEnvConfig()
372
+ if hasattr(env_config, name):
373
+ return getattr(env_config, name)
374
+
375
+ # Handle special methods
376
+ if name == 'get_default_profile':
377
+ return lambda: self._get_default_profile()
378
+ elif name == 'get_default_llm':
379
+ return lambda: self._get_default_llm()
380
+ elif name == 'get_default_agent':
381
+ return lambda: self._get_default_agent()
382
+ elif name == 'load_config':
383
+ return lambda: self._load_config()
384
+ elif name == '_ensure_dirs':
385
+ return lambda: old_config._ensure_dirs()
386
+
387
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
388
+
389
+ def _get_config_path(self) -> Path:
390
+ """Get config path from fresh env config."""
391
+ env_config = FlatEnvConfig()
392
+ if env_config.BROWSER_USE_CONFIG_PATH:
393
+ return Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()
394
+ elif env_config.BROWSER_USE_CONFIG_DIR:
395
+ return Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'
396
+ else:
397
+ xdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()
398
+ return xdg_config / 'browseruse' / 'config.json'
399
+
400
+ def _get_db_config(self) -> DBStyleConfigJSON:
401
+ """Load and migrate config.json."""
402
+ config_path = self._get_config_path()
403
+ return load_and_migrate_config(config_path)
404
+
405
+ def _get_default_profile(self) -> dict[str, Any]:
406
+ """Get the default browser profile configuration."""
407
+ db_config = self._get_db_config()
408
+ for profile in db_config.browser_profile.values():
409
+ if profile.default:
410
+ return profile.model_dump(exclude_none=True)
411
+
412
+ # Return first profile if no default
413
+ if db_config.browser_profile:
414
+ return next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)
415
+
416
+ return {}
417
+
418
+ def _get_default_llm(self) -> dict[str, Any]:
419
+ """Get the default LLM configuration."""
420
+ db_config = self._get_db_config()
421
+ for llm in db_config.llm.values():
422
+ if llm.default:
423
+ return llm.model_dump(exclude_none=True)
424
+
425
+ # Return first LLM if no default
426
+ if db_config.llm:
427
+ return next(iter(db_config.llm.values())).model_dump(exclude_none=True)
428
+
429
+ return {}
430
+
431
+ def _get_default_agent(self) -> dict[str, Any]:
432
+ """Get the default agent configuration."""
433
+ db_config = self._get_db_config()
434
+ for agent in db_config.agent.values():
435
+ if agent.default:
436
+ return agent.model_dump(exclude_none=True)
437
+
438
+ # Return first agent if no default
439
+ if db_config.agent:
440
+ return next(iter(db_config.agent.values())).model_dump(exclude_none=True)
441
+
442
+ return {}
443
+
444
+ def _load_config(self) -> dict[str, Any]:
445
+ """Load configuration with env var overrides for MCP components."""
446
+ config = {
447
+ 'browser_profile': self._get_default_profile(),
448
+ 'llm': self._get_default_llm(),
449
+ 'agent': self._get_default_agent(),
450
+ }
451
+
452
+ # Fresh env config for overrides
453
+ env_config = FlatEnvConfig()
454
+
455
+ # Apply MCP-specific env var overrides
456
+ if env_config.BROWSER_USE_HEADLESS is not None:
457
+ config['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS
458
+
459
+ if env_config.BROWSER_USE_ALLOWED_DOMAINS:
460
+ domains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]
461
+ config['browser_profile']['allowed_domains'] = domains
462
+
463
+ # Proxy settings (Chromium) -> consolidated `proxy` dict
464
+ proxy_dict: dict[str, Any] = {}
465
+ if env_config.BROWSER_USE_PROXY_URL:
466
+ proxy_dict['server'] = env_config.BROWSER_USE_PROXY_URL
467
+ if env_config.BROWSER_USE_NO_PROXY:
468
+ # store bypass as comma-separated string to match Chrome flag
469
+ proxy_dict['bypass'] = ','.join([d.strip() for d in env_config.BROWSER_USE_NO_PROXY.split(',') if d.strip()])
470
+ if env_config.BROWSER_USE_PROXY_USERNAME:
471
+ proxy_dict['username'] = env_config.BROWSER_USE_PROXY_USERNAME
472
+ if env_config.BROWSER_USE_PROXY_PASSWORD:
473
+ proxy_dict['password'] = env_config.BROWSER_USE_PROXY_PASSWORD
474
+ if proxy_dict:
475
+ # ensure section exists
476
+ config.setdefault('browser_profile', {})
477
+ config['browser_profile']['proxy'] = proxy_dict
478
+
479
+ if env_config.OPENAI_API_KEY:
480
+ config['llm']['api_key'] = env_config.OPENAI_API_KEY
481
+
482
+ if env_config.BROWSER_USE_LLM_MODEL:
483
+ config['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL
484
+
485
+ return config
486
+
487
+
488
+ # Create singleton instance
489
+ CONFIG = Config()
490
+
491
+
492
+ # Helper functions for MCP components
493
+ def load_browser_use_config() -> dict[str, Any]:
494
+ """Load browser-use configuration for MCP components."""
495
+ return CONFIG.load_config()
496
+
497
+
498
+ def get_default_profile(config: dict[str, Any]) -> dict[str, Any]:
499
+ """Get default browser profile from config dict."""
500
+ return config.get('browser_profile', {})
501
+
502
+
503
+ def get_default_llm(config: dict[str, Any]) -> dict[str, Any]:
504
+ """Get default LLM config from config dict."""
505
+ return config.get('llm', {})
@@ -0,0 +1,3 @@
1
+ from browser_use.tools.service import Controller
2
+
3
+ __all__ = ['Controller']
@@ -0,0 +1,161 @@
1
+ """
2
+ Enhanced snapshot processing for browser-use DOM tree extraction.
3
+
4
+ This module provides stateless functions for parsing Chrome DevTools Protocol (CDP) DOMSnapshot data
5
+ to extract visibility, clickability, cursor styles, and other layout information.
6
+ """
7
+
8
+ from cdp_use.cdp.domsnapshot.commands import CaptureSnapshotReturns
9
+ from cdp_use.cdp.domsnapshot.types import (
10
+ LayoutTreeSnapshot,
11
+ NodeTreeSnapshot,
12
+ RareBooleanData,
13
+ )
14
+
15
+ from browser_use.dom.views import DOMRect, EnhancedSnapshotNode
16
+
17
+ # Only the ESSENTIAL computed styles for interactivity and visibility detection
18
+ REQUIRED_COMPUTED_STYLES = [
19
+ # Only styles actually accessed in the codebase (prevents Chrome crashes on heavy sites)
20
+ 'display', # Used in service.py visibility detection
21
+ 'visibility', # Used in service.py visibility detection
22
+ 'opacity', # Used in service.py visibility detection
23
+ 'overflow', # Used in views.py scrollability detection
24
+ 'overflow-x', # Used in views.py scrollability detection
25
+ 'overflow-y', # Used in views.py scrollability detection
26
+ 'cursor', # Used in enhanced_snapshot.py cursor extraction
27
+ 'pointer-events', # Used for clickability logic
28
+ 'position', # Used for visibility logic
29
+ 'background-color', # Used for visibility logic
30
+ ]
31
+
32
+
33
+ def _parse_rare_boolean_data(rare_data: RareBooleanData, index: int) -> bool | None:
34
+ """Parse rare boolean data from snapshot - returns True if index is in the rare data."""
35
+ return index in rare_data['index']
36
+
37
+
38
+ def _parse_computed_styles(strings: list[str], style_indices: list[int]) -> dict[str, str]:
39
+ """Parse computed styles from layout tree using string indices."""
40
+ styles = {}
41
+ for i, style_index in enumerate(style_indices):
42
+ if i < len(REQUIRED_COMPUTED_STYLES) and 0 <= style_index < len(strings):
43
+ styles[REQUIRED_COMPUTED_STYLES[i]] = strings[style_index]
44
+ return styles
45
+
46
+
47
+ def build_snapshot_lookup(
48
+ snapshot: CaptureSnapshotReturns,
49
+ device_pixel_ratio: float = 1.0,
50
+ ) -> dict[int, EnhancedSnapshotNode]:
51
+ """Build a lookup table of backend node ID to enhanced snapshot data with everything calculated upfront."""
52
+ snapshot_lookup: dict[int, EnhancedSnapshotNode] = {}
53
+
54
+ if not snapshot['documents']:
55
+ return snapshot_lookup
56
+
57
+ strings = snapshot['strings']
58
+
59
+ for document in snapshot['documents']:
60
+ nodes: NodeTreeSnapshot = document['nodes']
61
+ layout: LayoutTreeSnapshot = document['layout']
62
+
63
+ # Build backend node id to snapshot index lookup
64
+ backend_node_to_snapshot_index = {}
65
+ if 'backendNodeId' in nodes:
66
+ for i, backend_node_id in enumerate(nodes['backendNodeId']):
67
+ backend_node_to_snapshot_index[backend_node_id] = i
68
+
69
+ # PERFORMANCE: Pre-build layout index map to eliminate O(n²) double lookups
70
+ # Preserve original behavior: use FIRST occurrence for duplicates
71
+ layout_index_map = {}
72
+ if layout and 'nodeIndex' in layout:
73
+ for layout_idx, node_index in enumerate(layout['nodeIndex']):
74
+ if node_index not in layout_index_map: # Only store first occurrence
75
+ layout_index_map[node_index] = layout_idx
76
+
77
+ # Build snapshot lookup for each backend node id
78
+ for backend_node_id, snapshot_index in backend_node_to_snapshot_index.items():
79
+ is_clickable = None
80
+ if 'isClickable' in nodes:
81
+ is_clickable = _parse_rare_boolean_data(nodes['isClickable'], snapshot_index)
82
+
83
+ # Find corresponding layout node
84
+ cursor_style = None
85
+ is_visible = None
86
+ bounding_box = None
87
+ computed_styles = {}
88
+
89
+ # Look for layout tree node that corresponds to this snapshot node
90
+ paint_order = None
91
+ client_rects = None
92
+ scroll_rects = None
93
+ stacking_contexts = None
94
+ if snapshot_index in layout_index_map:
95
+ layout_idx = layout_index_map[snapshot_index]
96
+ if layout_idx < len(layout.get('bounds', [])):
97
+ # Parse bounding box
98
+ bounds = layout['bounds'][layout_idx]
99
+ if len(bounds) >= 4:
100
+ # IMPORTANT: CDP coordinates are in device pixels, convert to CSS pixels
101
+ # by dividing by the device pixel ratio
102
+ raw_x, raw_y, raw_width, raw_height = bounds[0], bounds[1], bounds[2], bounds[3]
103
+
104
+ # Apply device pixel ratio scaling to convert device pixels to CSS pixels
105
+ bounding_box = DOMRect(
106
+ x=raw_x / device_pixel_ratio,
107
+ y=raw_y / device_pixel_ratio,
108
+ width=raw_width / device_pixel_ratio,
109
+ height=raw_height / device_pixel_ratio,
110
+ )
111
+
112
+ # Parse computed styles for this layout node
113
+ if layout_idx < len(layout.get('styles', [])):
114
+ style_indices = layout['styles'][layout_idx]
115
+ computed_styles = _parse_computed_styles(strings, style_indices)
116
+ cursor_style = computed_styles.get('cursor')
117
+
118
+ # Extract paint order if available
119
+ if layout_idx < len(layout.get('paintOrders', [])):
120
+ paint_order = layout.get('paintOrders', [])[layout_idx]
121
+
122
+ # Extract client rects if available
123
+ client_rects_data = layout.get('clientRects', [])
124
+ if layout_idx < len(client_rects_data):
125
+ client_rect_data = client_rects_data[layout_idx]
126
+ if client_rect_data and len(client_rect_data) >= 4:
127
+ client_rects = DOMRect(
128
+ x=client_rect_data[0],
129
+ y=client_rect_data[1],
130
+ width=client_rect_data[2],
131
+ height=client_rect_data[3],
132
+ )
133
+
134
+ # Extract scroll rects if available
135
+ scroll_rects_data = layout.get('scrollRects', [])
136
+ if layout_idx < len(scroll_rects_data):
137
+ scroll_rect_data = scroll_rects_data[layout_idx]
138
+ if scroll_rect_data and len(scroll_rect_data) >= 4:
139
+ scroll_rects = DOMRect(
140
+ x=scroll_rect_data[0],
141
+ y=scroll_rect_data[1],
142
+ width=scroll_rect_data[2],
143
+ height=scroll_rect_data[3],
144
+ )
145
+
146
+ # Extract stacking contexts if available
147
+ if layout_idx < len(layout.get('stackingContexts', [])):
148
+ stacking_contexts = layout.get('stackingContexts', {}).get('index', [])[layout_idx]
149
+
150
+ snapshot_lookup[backend_node_id] = EnhancedSnapshotNode(
151
+ is_clickable=is_clickable,
152
+ cursor_style=cursor_style,
153
+ bounds=bounding_box,
154
+ clientRects=client_rects,
155
+ scrollRects=scroll_rects,
156
+ computed_styles=computed_styles if computed_styles else None,
157
+ paint_order=paint_order,
158
+ stacking_contexts=stacking_contexts,
159
+ )
160
+
161
+ return snapshot_lookup