optexity-browser-use 0.9.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- browser_use/__init__.py +157 -0
- browser_use/actor/__init__.py +11 -0
- browser_use/actor/element.py +1175 -0
- browser_use/actor/mouse.py +134 -0
- browser_use/actor/page.py +561 -0
- browser_use/actor/playground/flights.py +41 -0
- browser_use/actor/playground/mixed_automation.py +54 -0
- browser_use/actor/playground/playground.py +236 -0
- browser_use/actor/utils.py +176 -0
- browser_use/agent/cloud_events.py +282 -0
- browser_use/agent/gif.py +424 -0
- browser_use/agent/judge.py +170 -0
- browser_use/agent/message_manager/service.py +473 -0
- browser_use/agent/message_manager/utils.py +52 -0
- browser_use/agent/message_manager/views.py +98 -0
- browser_use/agent/prompts.py +413 -0
- browser_use/agent/service.py +2316 -0
- browser_use/agent/system_prompt.md +185 -0
- browser_use/agent/system_prompt_flash.md +10 -0
- browser_use/agent/system_prompt_no_thinking.md +183 -0
- browser_use/agent/views.py +743 -0
- browser_use/browser/__init__.py +41 -0
- browser_use/browser/cloud/cloud.py +203 -0
- browser_use/browser/cloud/views.py +89 -0
- browser_use/browser/events.py +578 -0
- browser_use/browser/profile.py +1158 -0
- browser_use/browser/python_highlights.py +548 -0
- browser_use/browser/session.py +3225 -0
- browser_use/browser/session_manager.py +399 -0
- browser_use/browser/video_recorder.py +162 -0
- browser_use/browser/views.py +200 -0
- browser_use/browser/watchdog_base.py +260 -0
- browser_use/browser/watchdogs/__init__.py +0 -0
- browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
- browser_use/browser/watchdogs/crash_watchdog.py +335 -0
- browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
- browser_use/browser/watchdogs/dom_watchdog.py +817 -0
- browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
- browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
- browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
- browser_use/browser/watchdogs/popups_watchdog.py +143 -0
- browser_use/browser/watchdogs/recording_watchdog.py +126 -0
- browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
- browser_use/browser/watchdogs/security_watchdog.py +280 -0
- browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
- browser_use/cli.py +2359 -0
- browser_use/code_use/__init__.py +16 -0
- browser_use/code_use/formatting.py +192 -0
- browser_use/code_use/namespace.py +665 -0
- browser_use/code_use/notebook_export.py +276 -0
- browser_use/code_use/service.py +1340 -0
- browser_use/code_use/system_prompt.md +574 -0
- browser_use/code_use/utils.py +150 -0
- browser_use/code_use/views.py +171 -0
- browser_use/config.py +505 -0
- browser_use/controller/__init__.py +3 -0
- browser_use/dom/enhanced_snapshot.py +161 -0
- browser_use/dom/markdown_extractor.py +169 -0
- browser_use/dom/playground/extraction.py +312 -0
- browser_use/dom/playground/multi_act.py +32 -0
- browser_use/dom/serializer/clickable_elements.py +200 -0
- browser_use/dom/serializer/code_use_serializer.py +287 -0
- browser_use/dom/serializer/eval_serializer.py +478 -0
- browser_use/dom/serializer/html_serializer.py +212 -0
- browser_use/dom/serializer/paint_order.py +197 -0
- browser_use/dom/serializer/serializer.py +1170 -0
- browser_use/dom/service.py +825 -0
- browser_use/dom/utils.py +129 -0
- browser_use/dom/views.py +906 -0
- browser_use/exceptions.py +5 -0
- browser_use/filesystem/__init__.py +0 -0
- browser_use/filesystem/file_system.py +619 -0
- browser_use/init_cmd.py +376 -0
- browser_use/integrations/gmail/__init__.py +24 -0
- browser_use/integrations/gmail/actions.py +115 -0
- browser_use/integrations/gmail/service.py +225 -0
- browser_use/llm/__init__.py +155 -0
- browser_use/llm/anthropic/chat.py +242 -0
- browser_use/llm/anthropic/serializer.py +312 -0
- browser_use/llm/aws/__init__.py +36 -0
- browser_use/llm/aws/chat_anthropic.py +242 -0
- browser_use/llm/aws/chat_bedrock.py +289 -0
- browser_use/llm/aws/serializer.py +257 -0
- browser_use/llm/azure/chat.py +91 -0
- browser_use/llm/base.py +57 -0
- browser_use/llm/browser_use/__init__.py +3 -0
- browser_use/llm/browser_use/chat.py +201 -0
- browser_use/llm/cerebras/chat.py +193 -0
- browser_use/llm/cerebras/serializer.py +109 -0
- browser_use/llm/deepseek/chat.py +212 -0
- browser_use/llm/deepseek/serializer.py +109 -0
- browser_use/llm/exceptions.py +29 -0
- browser_use/llm/google/__init__.py +3 -0
- browser_use/llm/google/chat.py +542 -0
- browser_use/llm/google/serializer.py +120 -0
- browser_use/llm/groq/chat.py +229 -0
- browser_use/llm/groq/parser.py +158 -0
- browser_use/llm/groq/serializer.py +159 -0
- browser_use/llm/messages.py +238 -0
- browser_use/llm/models.py +271 -0
- browser_use/llm/oci_raw/__init__.py +10 -0
- browser_use/llm/oci_raw/chat.py +443 -0
- browser_use/llm/oci_raw/serializer.py +229 -0
- browser_use/llm/ollama/chat.py +97 -0
- browser_use/llm/ollama/serializer.py +143 -0
- browser_use/llm/openai/chat.py +264 -0
- browser_use/llm/openai/like.py +15 -0
- browser_use/llm/openai/serializer.py +165 -0
- browser_use/llm/openrouter/chat.py +211 -0
- browser_use/llm/openrouter/serializer.py +26 -0
- browser_use/llm/schema.py +176 -0
- browser_use/llm/views.py +48 -0
- browser_use/logging_config.py +330 -0
- browser_use/mcp/__init__.py +18 -0
- browser_use/mcp/__main__.py +12 -0
- browser_use/mcp/client.py +544 -0
- browser_use/mcp/controller.py +264 -0
- browser_use/mcp/server.py +1114 -0
- browser_use/observability.py +204 -0
- browser_use/py.typed +0 -0
- browser_use/sandbox/__init__.py +41 -0
- browser_use/sandbox/sandbox.py +637 -0
- browser_use/sandbox/views.py +132 -0
- browser_use/screenshots/__init__.py +1 -0
- browser_use/screenshots/service.py +52 -0
- browser_use/sync/__init__.py +6 -0
- browser_use/sync/auth.py +357 -0
- browser_use/sync/service.py +161 -0
- browser_use/telemetry/__init__.py +51 -0
- browser_use/telemetry/service.py +112 -0
- browser_use/telemetry/views.py +101 -0
- browser_use/tokens/__init__.py +0 -0
- browser_use/tokens/custom_pricing.py +24 -0
- browser_use/tokens/mappings.py +4 -0
- browser_use/tokens/service.py +580 -0
- browser_use/tokens/views.py +108 -0
- browser_use/tools/registry/service.py +572 -0
- browser_use/tools/registry/views.py +174 -0
- browser_use/tools/service.py +1675 -0
- browser_use/tools/utils.py +82 -0
- browser_use/tools/views.py +100 -0
- browser_use/utils.py +670 -0
- optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
- optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
- optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
- optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
- optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
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,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
|