mirror of
https://github.com/browser-use/browser-use
synced 2026-05-06 17:52:15 +02:00
Merge branch 'main' into fix-extraction-tasks
This commit is contained in:
8
.github/ISSUE_TEMPLATE/2_bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/2_bug_report.yml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
---
|
||||
> [!IMPORTANT]
|
||||
> 🙏 Please **go check *right now before filling this out* that you are *actually* on the [⬆️ LATEST VERSION](https://github.com/browser-use/browser-use/releases)**.
|
||||
> 🚀 We ship changes every hour and we might've already fixed your issue today!
|
||||
> 🚀 We ship changes every hour and we might've already fixed your issue today!
|
||||
> <a href="https://github.com/browser-use/browser-use/releases"><img src="https://github.com/user-attachments/assets/4cd34ee6-bafb-4f24-87e2-27a31dc5b9a4" width="500px"/></a>
|
||||
> If you are running an old version, the **first thing we will ask you to do is *upgrade to the latest version* and try again**:
|
||||
> - 🆕 [`beta`](https://docs.browser-use.com/development/local-setup): `uv pip install --upgrade git+https://github.com/browser-use/browser-use.git@main`
|
||||
@@ -25,7 +25,7 @@ body:
|
||||
attributes:
|
||||
label: Browser Use Version
|
||||
description: |
|
||||
What exact version of `browser-use` are you using? (Run `uv pip show browser-use` or `git log -n 1`)
|
||||
What exact version of `browser-use` are you using? (Run `uv pip show browser-use` or `git log -n 1`)
|
||||
**DO NOT WRITE `latest release` or `main` or a very old version or we will close your issue!**
|
||||
placeholder: "e.g. 0.4.45 or 62760baaefd"
|
||||
validations:
|
||||
@@ -58,7 +58,7 @@ body:
|
||||
|
||||
agent = Agent(
|
||||
task='...',
|
||||
llm=ChatOpenAI(model="gpt-4o"),
|
||||
llm=ChatOpenAI(model="gpt-4.1-mini"),
|
||||
browser_session=BrowserSession(headless=False),
|
||||
)
|
||||
...
|
||||
@@ -70,8 +70,6 @@ body:
|
||||
description: Which LLM model(s) are you using?
|
||||
multiple: true
|
||||
options:
|
||||
- gpt-4o
|
||||
- gpt-4o-mini
|
||||
- gpt-4
|
||||
- gpt-4.1
|
||||
- gpt-4.1-mini
|
||||
|
||||
40
README.md
40
README.md
@@ -15,20 +15,20 @@
|
||||
[](https://app.workweave.ai/reports/repository/org_T5Pvn3UBswTHIsN1dWS3voPg/881458615)
|
||||
|
||||
<!-- Keep these links. Translations will automatically update with the README. -->
|
||||
[Deutsch](https://www.readme-i18n.com/browser-use/browser-use?lang=de) |
|
||||
[Español](https://www.readme-i18n.com/browser-use/browser-use?lang=es) |
|
||||
[français](https://www.readme-i18n.com/browser-use/browser-use?lang=fr) |
|
||||
[日本語](https://www.readme-i18n.com/browser-use/browser-use?lang=ja) |
|
||||
[한국어](https://www.readme-i18n.com/browser-use/browser-use?lang=ko) |
|
||||
[Português](https://www.readme-i18n.com/browser-use/browser-use?lang=pt) |
|
||||
[Русский](https://www.readme-i18n.com/browser-use/browser-use?lang=ru) |
|
||||
[Deutsch](https://www.readme-i18n.com/browser-use/browser-use?lang=de) |
|
||||
[Español](https://www.readme-i18n.com/browser-use/browser-use?lang=es) |
|
||||
[français](https://www.readme-i18n.com/browser-use/browser-use?lang=fr) |
|
||||
[日本語](https://www.readme-i18n.com/browser-use/browser-use?lang=ja) |
|
||||
[한국어](https://www.readme-i18n.com/browser-use/browser-use?lang=ko) |
|
||||
[Português](https://www.readme-i18n.com/browser-use/browser-use?lang=pt) |
|
||||
[Русский](https://www.readme-i18n.com/browser-use/browser-use?lang=ru) |
|
||||
[中文](https://www.readme-i18n.com/browser-use/browser-use?lang=zh)
|
||||
|
||||
🌤️ Want to skip the setup? Use our <b>[cloud](https://cloud.browser-use.com)</b> for faster, scalable, stealth-enabled browser automation!
|
||||
|
||||
**🚀 Use the latest version!**
|
||||
**🚀 Use the latest version!**
|
||||
|
||||
> We ship every day improvements for **speed**, **accuracy**, and **UX**.
|
||||
> We ship every day improvements for **speed**, **accuracy**, and **UX**.
|
||||
> ```bash
|
||||
> uv pip install --upgrade browser-use
|
||||
> ```
|
||||
@@ -74,6 +74,22 @@ OPENAI_API_KEY=
|
||||
|
||||
For other settings, models, and more, check out the [documentation 📕](https://docs.browser-use.com).
|
||||
|
||||
**🌤️ Want to use cloud browsers?** Simply add `cloud_browser=True` to your Browser config:
|
||||
|
||||
```python
|
||||
from browser_use import Agent, Browser, ChatOpenAI
|
||||
|
||||
agent = Agent(
|
||||
task="Find the number of stars of the browser-use repo",
|
||||
llm=ChatOpenAI(model="gpt-4.1-mini"),
|
||||
browser=Browser(cloud_browser=True), # Uses Browser-Use cloud service
|
||||
)
|
||||
```
|
||||
|
||||
First Set BROWSER_USE_API_KEY environment variable. You can get your API key from [here](https://cloud.browser-use.com).
|
||||
|
||||
For other settings, models, and more, check out the [Cloud documentation 📕](https://docs.cloud.browser-use.com).
|
||||
|
||||
# Demos
|
||||
|
||||
<br/><br/>
|
||||
@@ -254,11 +270,11 @@ If you use Browser Use in your research or project, please cite:
|
||||
}
|
||||
```
|
||||
|
||||
<div align="center"> <img src="https://github.com/user-attachments/assets/06fa3078-8461-4560-b434-445510c1766f" width="400"/>
|
||||
|
||||
<div align="center"> <img src="https://github.com/user-attachments/assets/06fa3078-8461-4560-b434-445510c1766f" width="400"/>
|
||||
|
||||
[](https://x.com/intent/user?screen_name=gregpr07)
|
||||
[](https://x.com/intent/user?screen_name=mamagnus00)
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
286
browser_use/browser/cloud.py
Normal file
286
browser_use/browser/cloud.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""Cloud browser service integration for browser-use.
|
||||
|
||||
This module provides integration with the browser-use cloud browser service.
|
||||
When cloud_browser=True, it automatically creates a cloud browser instance
|
||||
and returns the CDP URL for connection.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import httpx
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from browser_use.sync.auth import CloudAuthConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudBrowserResponse(BaseModel):
|
||||
"""Response from cloud browser API."""
|
||||
|
||||
id: str
|
||||
status: str
|
||||
liveUrl: str = Field(alias='liveUrl')
|
||||
cdpUrl: str = Field(alias='cdpUrl')
|
||||
timeoutAt: str = Field(alias='timeoutAt')
|
||||
startedAt: str = Field(alias='startedAt')
|
||||
finishedAt: str | None = Field(alias='finishedAt', default=None)
|
||||
|
||||
|
||||
class CloudBrowserError(Exception):
|
||||
"""Exception raised when cloud browser operations fail."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CloudBrowserAuthError(CloudBrowserError):
|
||||
"""Exception raised when cloud browser authentication fails."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CloudBrowserClient:
|
||||
"""Client for browser-use cloud browser service."""
|
||||
|
||||
def __init__(self, api_base_url: str = 'https://api.browser-use.com'):
|
||||
self.api_base_url = api_base_url
|
||||
self.client = httpx.AsyncClient(timeout=30.0)
|
||||
self.current_session_id: str | None = None
|
||||
|
||||
async def create_browser(self) -> CloudBrowserResponse:
|
||||
"""Create a new cloud browser instance.
|
||||
|
||||
Returns:
|
||||
CloudBrowserResponse: Contains CDP URL and other browser info
|
||||
|
||||
Raises:
|
||||
CloudBrowserAuthError: If authentication fails
|
||||
CloudBrowserError: If browser creation fails
|
||||
"""
|
||||
url = f'{self.api_base_url}/api/v2/browsers'
|
||||
|
||||
# Try to get API key from environment variable first, then auth config
|
||||
api_token = os.getenv('BROWSER_USE_API_KEY')
|
||||
|
||||
if not api_token:
|
||||
# Fallback to auth config file
|
||||
try:
|
||||
auth_config = CloudAuthConfig.load_from_file()
|
||||
api_token = auth_config.api_token
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not api_token:
|
||||
raise CloudBrowserAuthError(
|
||||
'No authentication token found. Please set BROWSER_USE_API_KEY environment variable to authenticate with the cloud service.'
|
||||
)
|
||||
|
||||
headers = {'X-Browser-Use-API-Key': api_token, 'Content-Type': 'application/json'}
|
||||
|
||||
# Empty request body as per API specification
|
||||
request_body = {}
|
||||
|
||||
try:
|
||||
logger.info('🌤️ Creating cloud browser instance...')
|
||||
|
||||
response = await self.client.post(url, headers=headers, json=request_body)
|
||||
|
||||
if response.status_code == 401:
|
||||
raise CloudBrowserAuthError(
|
||||
'Authentication failed. Please make sure you have set BROWSER_USE_API_KEY environment variable to authenticate with the cloud service.'
|
||||
)
|
||||
elif response.status_code == 403:
|
||||
raise CloudBrowserAuthError('Access forbidden. Please check your browser-use cloud subscription status.')
|
||||
elif not response.is_success:
|
||||
error_msg = f'Failed to create cloud browser: HTTP {response.status_code}'
|
||||
try:
|
||||
error_data = response.json()
|
||||
if 'detail' in error_data:
|
||||
error_msg += f' - {error_data["detail"]}'
|
||||
except Exception:
|
||||
pass
|
||||
raise CloudBrowserError(error_msg)
|
||||
|
||||
browser_data = response.json()
|
||||
browser_response = CloudBrowserResponse(**browser_data)
|
||||
|
||||
# Store session ID for cleanup
|
||||
self.current_session_id = browser_response.id
|
||||
|
||||
logger.info(f'🌤️ Cloud browser created successfully: {browser_response.id}')
|
||||
logger.debug(f'🌤️ CDP URL: {browser_response.cdpUrl}')
|
||||
logger.debug(f'🌤️ Live URL: {browser_response.liveUrl}')
|
||||
|
||||
return browser_response
|
||||
|
||||
except httpx.TimeoutException:
|
||||
raise CloudBrowserError('Timeout while creating cloud browser. Please try again.')
|
||||
except httpx.ConnectError:
|
||||
raise CloudBrowserError('Failed to connect to cloud browser service. Please check your internet connection.')
|
||||
except Exception as e:
|
||||
if isinstance(e, (CloudBrowserError, CloudBrowserAuthError)):
|
||||
raise
|
||||
raise CloudBrowserError(f'Unexpected error creating cloud browser: {e}')
|
||||
|
||||
async def stop_browser(self, session_id: str | None = None) -> CloudBrowserResponse:
|
||||
"""Stop a cloud browser session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID to stop. If None, uses current session.
|
||||
|
||||
Returns:
|
||||
CloudBrowserResponse: Updated browser info with stopped status
|
||||
|
||||
Raises:
|
||||
CloudBrowserAuthError: If authentication fails
|
||||
CloudBrowserError: If stopping fails
|
||||
"""
|
||||
if session_id is None:
|
||||
session_id = self.current_session_id
|
||||
|
||||
if not session_id:
|
||||
raise CloudBrowserError('No session ID provided and no current session available')
|
||||
|
||||
url = f'{self.api_base_url}/api/v2/browsers/{session_id}'
|
||||
|
||||
# Try to get API key from environment variable first, then auth config
|
||||
api_token = os.getenv('BROWSER_USE_API_KEY')
|
||||
|
||||
if not api_token:
|
||||
# Fallback to auth config file
|
||||
try:
|
||||
auth_config = CloudAuthConfig.load_from_file()
|
||||
api_token = auth_config.api_token
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not api_token:
|
||||
raise CloudBrowserAuthError(
|
||||
'No authentication token found. Please set BROWSER_USE_API_KEY environment variable to authenticate with the cloud service.'
|
||||
)
|
||||
|
||||
headers = {'X-Browser-Use-API-Key': api_token, 'Content-Type': 'application/json'}
|
||||
|
||||
request_body = {'action': 'stop'}
|
||||
|
||||
try:
|
||||
logger.info(f'🌤️ Stopping cloud browser session: {session_id}')
|
||||
|
||||
response = await self.client.patch(url, headers=headers, json=request_body)
|
||||
|
||||
if response.status_code == 401:
|
||||
raise CloudBrowserAuthError(
|
||||
'Authentication failed. Please make sure you have set the BROWSER_USE_API_KEY environment variable to authenticate with the cloud service.'
|
||||
)
|
||||
elif response.status_code == 404:
|
||||
# Session already stopped or doesn't exist - treating as error and clearing session
|
||||
logger.debug(f'🌤️ Cloud browser session {session_id} not found (already stopped)')
|
||||
# Clear current session if it was this one
|
||||
if session_id == self.current_session_id:
|
||||
self.current_session_id = None
|
||||
raise CloudBrowserError(f'Cloud browser session {session_id} not found')
|
||||
elif not response.is_success:
|
||||
error_msg = f'Failed to stop cloud browser: HTTP {response.status_code}'
|
||||
try:
|
||||
error_data = response.json()
|
||||
if 'detail' in error_data:
|
||||
error_msg += f' - {error_data["detail"]}'
|
||||
except Exception:
|
||||
pass
|
||||
raise CloudBrowserError(error_msg)
|
||||
|
||||
browser_data = response.json()
|
||||
browser_response = CloudBrowserResponse(**browser_data)
|
||||
|
||||
# Clear current session if it was this one
|
||||
if session_id == self.current_session_id:
|
||||
self.current_session_id = None
|
||||
|
||||
logger.info(f'🌤️ Cloud browser session stopped: {browser_response.id}')
|
||||
logger.debug(f'🌤️ Status: {browser_response.status}')
|
||||
|
||||
return browser_response
|
||||
|
||||
except httpx.TimeoutException:
|
||||
raise CloudBrowserError('Timeout while stopping cloud browser. Please try again.')
|
||||
except httpx.ConnectError:
|
||||
raise CloudBrowserError('Failed to connect to cloud browser service. Please check your internet connection.')
|
||||
except Exception as e:
|
||||
if isinstance(e, (CloudBrowserError, CloudBrowserAuthError)):
|
||||
raise
|
||||
raise CloudBrowserError(f'Unexpected error stopping cloud browser: {e}')
|
||||
|
||||
async def close(self):
|
||||
"""Close the HTTP client and cleanup any active sessions."""
|
||||
# Try to stop current session if active
|
||||
if self.current_session_id:
|
||||
try:
|
||||
await self.stop_browser()
|
||||
except Exception as e:
|
||||
logger.debug(f'Failed to stop cloud browser session during cleanup: {e}')
|
||||
|
||||
await self.client.aclose()
|
||||
|
||||
|
||||
# Global client instance
|
||||
_cloud_client: CloudBrowserClient | None = None
|
||||
|
||||
|
||||
async def get_cloud_browser_cdp_url() -> str:
|
||||
"""Get a CDP URL for a new cloud browser instance.
|
||||
|
||||
Returns:
|
||||
str: CDP URL for connecting to the cloud browser
|
||||
|
||||
Raises:
|
||||
CloudBrowserAuthError: If authentication fails
|
||||
CloudBrowserError: If browser creation fails
|
||||
"""
|
||||
global _cloud_client
|
||||
|
||||
if _cloud_client is None:
|
||||
_cloud_client = CloudBrowserClient()
|
||||
|
||||
try:
|
||||
browser_response = await _cloud_client.create_browser()
|
||||
return browser_response.cdpUrl
|
||||
except Exception:
|
||||
# Clean up client on error
|
||||
if _cloud_client:
|
||||
await _cloud_client.close()
|
||||
_cloud_client = None
|
||||
raise
|
||||
|
||||
|
||||
async def stop_cloud_browser_session(session_id: str | None = None) -> CloudBrowserResponse:
|
||||
"""Stop a cloud browser session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID to stop. If None, uses current session from global client.
|
||||
|
||||
Returns:
|
||||
CloudBrowserResponse: Updated browser info with stopped status
|
||||
|
||||
Raises:
|
||||
CloudBrowserAuthError: If authentication fails
|
||||
CloudBrowserError: If stopping fails
|
||||
"""
|
||||
global _cloud_client
|
||||
|
||||
if _cloud_client is None:
|
||||
_cloud_client = CloudBrowserClient()
|
||||
|
||||
try:
|
||||
return await _cloud_client.stop_browser(session_id)
|
||||
except Exception:
|
||||
# Don't clean up client on stop errors - session might still be valid
|
||||
raise
|
||||
|
||||
|
||||
async def cleanup_cloud_client():
|
||||
"""Clean up the global cloud client."""
|
||||
global _cloud_client
|
||||
if _cloud_client:
|
||||
await _cloud_client.close()
|
||||
_cloud_client = None
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Iterable
|
||||
@@ -550,7 +551,15 @@ class BrowserProfile(BrowserConnectArgs, BrowserLaunchPersistentContextArgs, Bro
|
||||
# Session/connection configuration
|
||||
cdp_url: str | None = Field(default=None, description='CDP URL for connecting to existing browser instance')
|
||||
is_local: bool = Field(default=False, description='Whether this is a local browser instance')
|
||||
# label: str = 'default'
|
||||
use_cloud: bool = Field(
|
||||
default_factory=lambda: bool(os.getenv('BROWSER_USE_API_KEY')),
|
||||
description='Use browser-use cloud browser service instead of local browser',
|
||||
)
|
||||
|
||||
@property
|
||||
def cloud_browser(self) -> bool:
|
||||
"""Alias for use_cloud field for compatibility."""
|
||||
return self.use_cloud
|
||||
|
||||
# custom options we provide that aren't native playwright kwargs
|
||||
disable_security: bool = Field(default=False, description='Disable browser security features.')
|
||||
|
||||
@@ -15,6 +15,8 @@ from cdp_use.cdp.target import AttachedToTargetEvent, SessionID, TargetID
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
||||
from uuid_extensions import uuid7str
|
||||
|
||||
from browser_use.browser.cloud import CloudBrowserAuthError, CloudBrowserError, get_cloud_browser_cdp_url
|
||||
|
||||
# CDP logging is now handled by setup_logging() in logging_config.py
|
||||
# It automatically sets CDP logs to the same level as browser_use logs
|
||||
from browser_use.browser.events import (
|
||||
@@ -251,6 +253,8 @@ class BrowserSession(BaseModel):
|
||||
# From BrowserNewContextArgs
|
||||
storage_state: str | Path | dict[str, Any] | None = None,
|
||||
# BrowserProfile specific fields
|
||||
use_cloud: bool | None = None,
|
||||
cloud_browser: bool | None = None, # Backward compatibility alias
|
||||
disable_security: bool | None = None,
|
||||
deterministic_rendering: bool | None = None,
|
||||
allowed_domains: list[str] | None = None,
|
||||
@@ -278,6 +282,10 @@ class BrowserSession(BaseModel):
|
||||
# Only pass non-None values to avoid validation errors
|
||||
profile_kwargs = {k: v for k, v in locals().items() if k not in ['self', 'browser_profile', 'id'] and v is not None}
|
||||
|
||||
# Handle backward compatibility: map cloud_browser to use_cloud
|
||||
if 'cloud_browser' in profile_kwargs:
|
||||
profile_kwargs['use_cloud'] = profile_kwargs.pop('cloud_browser')
|
||||
|
||||
# if is_local is False but executable_path is provided, set is_local to True
|
||||
if is_local is False and executable_path is not None:
|
||||
profile_kwargs['is_local'] = True
|
||||
@@ -318,6 +326,11 @@ class BrowserSession(BaseModel):
|
||||
"""Whether this is a local browser instance from browser profile."""
|
||||
return self.browser_profile.is_local
|
||||
|
||||
@property
|
||||
def cloud_browser(self) -> bool:
|
||||
"""Whether to use cloud browser service from browser profile."""
|
||||
return self.browser_profile.use_cloud
|
||||
|
||||
# Main shared event bus for all browser session + all watchdogs
|
||||
event_bus: EventBus = Field(default_factory=EventBus)
|
||||
|
||||
@@ -496,9 +509,22 @@ class BrowserSession(BaseModel):
|
||||
await self.attach_all_watchdogs()
|
||||
|
||||
try:
|
||||
# If no CDP URL, launch local browser
|
||||
# If no CDP URL, launch local browser or cloud browser
|
||||
if not self.cdp_url:
|
||||
if self.is_local:
|
||||
if self.browser_profile.use_cloud:
|
||||
# Use cloud browser service
|
||||
try:
|
||||
cloud_cdp_url = await get_cloud_browser_cdp_url()
|
||||
self.browser_profile.cdp_url = cloud_cdp_url
|
||||
self.browser_profile.is_local = False
|
||||
self.logger.info('🌤️ Successfully connected to cloud browser service')
|
||||
except CloudBrowserAuthError:
|
||||
raise CloudBrowserAuthError(
|
||||
'Authentication failed for cloud browser service. Set BROWSER_USE_API_KEY environment variable'
|
||||
)
|
||||
except CloudBrowserError as e:
|
||||
raise CloudBrowserError(f'Failed to create cloud browser: {e}')
|
||||
elif self.is_local:
|
||||
# Launch local browser using event-driven approach
|
||||
launch_event = self.event_bus.dispatch(BrowserLaunchEvent())
|
||||
await launch_event
|
||||
@@ -813,6 +839,16 @@ class BrowserSession(BaseModel):
|
||||
self.event_bus.dispatch(BrowserStoppedEvent(reason='Kept alive due to keep_alive=True'))
|
||||
return
|
||||
|
||||
# Clean up cloud browser session if using cloud browser
|
||||
if self.browser_profile.use_cloud:
|
||||
try:
|
||||
from browser_use.browser.cloud import cleanup_cloud_client
|
||||
|
||||
await cleanup_cloud_client()
|
||||
self.logger.info('🌤️ Cloud browser session cleaned up')
|
||||
except Exception as e:
|
||||
self.logger.debug(f'Failed to cleanup cloud browser session: {e}')
|
||||
|
||||
# Clear CDP session cache before stopping
|
||||
await self.reset()
|
||||
|
||||
@@ -1616,18 +1652,18 @@ class BrowserSession(BaseModel):
|
||||
const highlights = document.querySelectorAll('[data-browser-use-highlight]');
|
||||
console.log('Removing', highlights.length, 'browser-use highlight elements');
|
||||
highlights.forEach(el => el.remove());
|
||||
|
||||
|
||||
// Also remove by ID in case selector missed anything
|
||||
const highlightContainer = document.getElementById('browser-use-debug-highlights');
|
||||
if (highlightContainer) {
|
||||
console.log('Removing highlight container by ID');
|
||||
highlightContainer.remove();
|
||||
}
|
||||
|
||||
|
||||
// Final cleanup - remove any orphaned tooltips
|
||||
const orphanedTooltips = document.querySelectorAll('[data-browser-use-highlight="tooltip"]');
|
||||
orphanedTooltips.forEach(el => el.remove());
|
||||
|
||||
|
||||
return { removed: highlights.length };
|
||||
})();
|
||||
"""
|
||||
|
||||
@@ -287,7 +287,7 @@ def create_default_config() -> DBStyleConfigJSON:
|
||||
new_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)
|
||||
|
||||
# Create default LLM entry
|
||||
new_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')
|
||||
new_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4.1-mini', api_key='your-openai-api-key-here')
|
||||
|
||||
# Create default agent entry
|
||||
new_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)
|
||||
|
||||
@@ -176,7 +176,7 @@ async def test_focus_vs_all_elements():
|
||||
# copy the user message to the clipboard
|
||||
# pyperclip.copy(text_to_save)
|
||||
|
||||
encoding = tiktoken.encoding_for_model('gpt-4o')
|
||||
encoding = tiktoken.encoding_for_model('gpt-4.1-mini')
|
||||
token_count = len(encoding.encode(text_to_save))
|
||||
print(f'Token count: {token_count}')
|
||||
|
||||
|
||||
@@ -472,7 +472,7 @@ class DOMTreeSerializer:
|
||||
if has_validation_attrs:
|
||||
is_visible = True # Force visibility for validation elements
|
||||
|
||||
# Include if visible, interactive, scrollable, has children, or is shadow host
|
||||
# Include if visible, scrollable, has children, or is shadow host
|
||||
if is_visible or is_scrollable or has_shadow_content or is_shadow_host:
|
||||
simplified = SimplifiedNode(original_node=node, children=[], is_shadow_host=is_shadow_host)
|
||||
|
||||
|
||||
@@ -23,8 +23,39 @@ agent = Agent(
|
||||
|
||||
|
||||
## Get a CDP URL
|
||||
### Cloud Browser
|
||||
Get a cdp url from your favorite browser provider like AnchorBorwser, HyperBrowser, BrowserBase, Steel.dev, etc.
|
||||
|
||||
### Browser-Use Cloud Browser
|
||||
|
||||
The easiest way to use a cloud browser is with the built-in Browser-Use cloud service:
|
||||
|
||||
```python
|
||||
from browser_use import Agent, Browser, ChatOpenAI
|
||||
|
||||
# Use Browser-Use cloud browser service
|
||||
browser = Browser(
|
||||
cloud_browser=True # Automatically provisions a cloud browser
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
task="Your task here",
|
||||
llm=ChatOpenAI(model='gpt-4.1-mini'),
|
||||
browser=browser,
|
||||
)
|
||||
```
|
||||
|
||||
**Prerequisites:**
|
||||
1. Get an API key from [cloud.browser-use.com](https://cloud.browser-use.com)
|
||||
2. Set BROWSER_USE_API_KEY environment variable
|
||||
|
||||
**Benefits:**
|
||||
- ✅ No local browser setup required
|
||||
- ✅ Scalable and fast cloud infrastructure
|
||||
- ✅ Automatic provisioning and teardown
|
||||
- ✅ Built-in authentication handling
|
||||
- ✅ Optimized for browser automation
|
||||
|
||||
### Third-Party Cloud Browsers
|
||||
Get a CDP URL from your favorite browser provider like AnchorBrowser, HyperBrowser, BrowserBase, Steel.dev, etc.
|
||||
|
||||
|
||||
|
||||
|
||||
41
examples/browser/cloud_browser.py
Normal file
41
examples/browser/cloud_browser.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Simple example of using Browser-Use cloud browser service.
|
||||
|
||||
Prerequisites:
|
||||
1. Set BROWSER_USE_API_KEY environment variable
|
||||
2. Active subscription at https://cloud.browser-use.com
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, Browser, ChatOpenAI
|
||||
|
||||
|
||||
async def main():
|
||||
"""Basic cloud browser example."""
|
||||
|
||||
print('🌤️ Using Browser-Use Cloud Browser')
|
||||
|
||||
# Create agent with cloud browser enabled
|
||||
agent = Agent(
|
||||
task='Go to https://github.com/browser-use/browser-use and find the number of stars',
|
||||
llm=ChatOpenAI(model='gpt-4.1-mini'),
|
||||
browser=Browser(use_cloud=True), # Enable cloud browser
|
||||
)
|
||||
|
||||
try:
|
||||
result = await agent.run()
|
||||
print(f'✅ Result: {result}')
|
||||
except Exception as e:
|
||||
print(f'❌ Error: {e}')
|
||||
if 'Authentication' in str(e):
|
||||
print('💡 Set BROWSER_USE_API_KEY environment variable')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
333
tests/ci/test_cloud_browser.py
Normal file
333
tests/ci/test_cloud_browser.py
Normal file
@@ -0,0 +1,333 @@
|
||||
"""Tests for cloud browser functionality."""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from browser_use.browser.cloud import (
|
||||
CloudBrowserAuthError,
|
||||
CloudBrowserClient,
|
||||
CloudBrowserError,
|
||||
get_cloud_browser_cdp_url,
|
||||
stop_cloud_browser_session,
|
||||
)
|
||||
from browser_use.browser.profile import BrowserProfile
|
||||
from browser_use.browser.session import BrowserSession
|
||||
from browser_use.sync.auth import CloudAuthConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_config_dir(monkeypatch):
|
||||
"""Create temporary config directory."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_dir = Path(tmpdir) / '.config' / 'browseruse'
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Use monkeypatch to set the environment variable
|
||||
monkeypatch.setenv('BROWSER_USE_CONFIG_DIR', str(temp_dir))
|
||||
|
||||
yield temp_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_auth_config(temp_config_dir):
|
||||
"""Create a mock auth config with valid token."""
|
||||
auth_config = CloudAuthConfig(api_token='test-token', user_id='test-user-id', authorized_at=None)
|
||||
auth_config.save_to_file()
|
||||
return auth_config
|
||||
|
||||
|
||||
class TestCloudBrowserClient:
|
||||
"""Test CloudBrowserClient class."""
|
||||
|
||||
async def test_create_browser_success(self, mock_auth_config):
|
||||
"""Test successful cloud browser creation."""
|
||||
|
||||
# Mock response data matching the API
|
||||
mock_response_data = {
|
||||
'id': 'test-browser-id',
|
||||
'status': 'active',
|
||||
'liveUrl': 'https://live.browser-use.com?wss=test',
|
||||
'cdpUrl': 'wss://test.proxy.daytona.works',
|
||||
'timeoutAt': '2025-09-17T04:35:36.049892',
|
||||
'startedAt': '2025-09-17T03:35:36.049974',
|
||||
'finishedAt': None,
|
||||
}
|
||||
|
||||
# Mock the httpx client
|
||||
with patch('httpx.AsyncClient') as mock_client_class:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 201
|
||||
mock_response.is_success = True
|
||||
mock_response.json = lambda: mock_response_data
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.post.return_value = mock_response
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
client = CloudBrowserClient()
|
||||
client.client = mock_client
|
||||
|
||||
result = await client.create_browser()
|
||||
|
||||
assert result.id == 'test-browser-id'
|
||||
assert result.status == 'active'
|
||||
assert result.cdpUrl == 'wss://test.proxy.daytona.works'
|
||||
|
||||
# Verify auth headers were included
|
||||
mock_client.post.assert_called_once()
|
||||
call_args = mock_client.post.call_args
|
||||
assert 'X-Browser-Use-API-Key' in call_args.kwargs['headers']
|
||||
assert call_args.kwargs['headers']['X-Browser-Use-API-Key'] == 'test-token'
|
||||
|
||||
async def test_create_browser_auth_error(self, temp_config_dir):
|
||||
"""Test cloud browser creation with auth error."""
|
||||
|
||||
# Don't create auth config - should trigger auth error
|
||||
|
||||
with patch('httpx.AsyncClient') as mock_client_class:
|
||||
mock_client = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
client = CloudBrowserClient()
|
||||
client.client = mock_client
|
||||
|
||||
with pytest.raises(CloudBrowserAuthError) as exc_info:
|
||||
await client.create_browser()
|
||||
|
||||
assert 'BROWSER_USE_API_KEY environment variable' in str(exc_info.value)
|
||||
|
||||
async def test_create_browser_http_401(self, mock_auth_config):
|
||||
"""Test cloud browser creation with HTTP 401 response."""
|
||||
|
||||
with patch('httpx.AsyncClient') as mock_client_class:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 401
|
||||
mock_response.is_success = False
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.post.return_value = mock_response
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
client = CloudBrowserClient()
|
||||
client.client = mock_client
|
||||
|
||||
with pytest.raises(CloudBrowserAuthError) as exc_info:
|
||||
await client.create_browser()
|
||||
|
||||
assert 'Authentication failed' in str(exc_info.value)
|
||||
|
||||
async def test_create_browser_with_env_var(self, temp_config_dir, monkeypatch):
|
||||
"""Test cloud browser creation using BROWSER_USE_API_KEY environment variable."""
|
||||
|
||||
# Set environment variable
|
||||
monkeypatch.setenv('BROWSER_USE_API_KEY', 'env-test-token')
|
||||
|
||||
# Mock response data matching the API
|
||||
mock_response_data = {
|
||||
'id': 'test-browser-id',
|
||||
'status': 'active',
|
||||
'liveUrl': 'https://live.browser-use.com?wss=test',
|
||||
'cdpUrl': 'wss://test.proxy.daytona.works',
|
||||
'timeoutAt': '2025-09-17T04:35:36.049892',
|
||||
'startedAt': '2025-09-17T03:35:36.049974',
|
||||
'finishedAt': None,
|
||||
}
|
||||
|
||||
with patch('httpx.AsyncClient') as mock_client_class:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 201
|
||||
mock_response.is_success = True
|
||||
mock_response.json = lambda: mock_response_data
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.post.return_value = mock_response
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
client = CloudBrowserClient()
|
||||
client.client = mock_client
|
||||
|
||||
result = await client.create_browser()
|
||||
|
||||
assert result.id == 'test-browser-id'
|
||||
assert result.status == 'active'
|
||||
assert result.cdpUrl == 'wss://test.proxy.daytona.works'
|
||||
|
||||
# Verify environment variable was used
|
||||
mock_client.post.assert_called_once()
|
||||
call_args = mock_client.post.call_args
|
||||
assert 'X-Browser-Use-API-Key' in call_args.kwargs['headers']
|
||||
assert call_args.kwargs['headers']['X-Browser-Use-API-Key'] == 'env-test-token'
|
||||
|
||||
async def test_stop_browser_success(self, mock_auth_config):
|
||||
"""Test successful cloud browser session stop."""
|
||||
|
||||
# Mock response data for stop
|
||||
mock_response_data = {
|
||||
'id': 'test-browser-id',
|
||||
'status': 'stopped',
|
||||
'liveUrl': 'https://live.browser-use.com?wss=test',
|
||||
'cdpUrl': 'wss://test.proxy.daytona.works',
|
||||
'timeoutAt': '2025-09-17T04:35:36.049892',
|
||||
'startedAt': '2025-09-17T03:35:36.049974',
|
||||
'finishedAt': '2025-09-17T04:35:36.049892',
|
||||
}
|
||||
|
||||
with patch('httpx.AsyncClient') as mock_client_class:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.is_success = True
|
||||
mock_response.json = lambda: mock_response_data
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.patch.return_value = mock_response
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
client = CloudBrowserClient()
|
||||
client.client = mock_client
|
||||
client.current_session_id = 'test-browser-id'
|
||||
|
||||
result = await client.stop_browser()
|
||||
|
||||
assert result.id == 'test-browser-id'
|
||||
assert result.status == 'stopped'
|
||||
assert result.finishedAt is not None
|
||||
|
||||
# Verify correct API call
|
||||
mock_client.patch.assert_called_once()
|
||||
call_args = mock_client.patch.call_args
|
||||
assert 'test-browser-id' in call_args.args[0] # URL contains session ID
|
||||
assert call_args.kwargs['json'] == {'action': 'stop'}
|
||||
assert 'X-Browser-Use-API-Key' in call_args.kwargs['headers']
|
||||
|
||||
async def test_stop_browser_session_not_found(self, mock_auth_config):
|
||||
"""Test stopping a browser session that doesn't exist."""
|
||||
|
||||
with patch('httpx.AsyncClient') as mock_client_class:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.is_success = False
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.patch.return_value = mock_response
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
client = CloudBrowserClient()
|
||||
client.client = mock_client
|
||||
|
||||
with pytest.raises(CloudBrowserError) as exc_info:
|
||||
await client.stop_browser('nonexistent-session')
|
||||
|
||||
assert 'not found' in str(exc_info.value)
|
||||
|
||||
|
||||
class TestBrowserSessionCloudIntegration:
|
||||
"""Test BrowserSession integration with cloud browsers."""
|
||||
|
||||
async def test_cloud_browser_profile_property(self):
|
||||
"""Test that cloud_browser property works correctly."""
|
||||
|
||||
profile = BrowserProfile(use_cloud=True)
|
||||
session = BrowserSession(browser_profile=profile)
|
||||
|
||||
assert session.cloud_browser is True
|
||||
assert session.browser_profile.use_cloud is True
|
||||
|
||||
async def test_browser_session_cloud_browser_logic(self, mock_auth_config):
|
||||
"""Test that cloud browser profile settings work correctly."""
|
||||
|
||||
# Test cloud browser profile creation
|
||||
profile = BrowserProfile(use_cloud=True)
|
||||
assert profile.use_cloud is True
|
||||
|
||||
# Test that BrowserSession respects cloud_browser setting
|
||||
session = BrowserSession(browser_profile=profile)
|
||||
assert session.cloud_browser is True
|
||||
|
||||
# Test that get_cloud_browser_cdp_url works with mocked API
|
||||
with patch('browser_use.browser.cloud.get_cloud_browser_cdp_url') as mock_get_cdp_url:
|
||||
mock_get_cdp_url.return_value = 'wss://test.proxy.daytona.works'
|
||||
|
||||
cdp_url = await mock_get_cdp_url()
|
||||
assert cdp_url == 'wss://test.proxy.daytona.works'
|
||||
mock_get_cdp_url.assert_called_once()
|
||||
|
||||
|
||||
async def test_get_cloud_browser_cdp_url_function(mock_auth_config):
|
||||
"""Test the get_cloud_browser_cdp_url convenience function."""
|
||||
|
||||
mock_response_data = {
|
||||
'id': 'test-browser-id',
|
||||
'status': 'active',
|
||||
'liveUrl': 'https://live.browser-use.com?wss=test',
|
||||
'cdpUrl': 'wss://test.proxy.daytona.works',
|
||||
'timeoutAt': '2025-09-17T04:35:36.049892',
|
||||
'startedAt': '2025-09-17T03:35:36.049974',
|
||||
'finishedAt': None,
|
||||
}
|
||||
|
||||
with patch('httpx.AsyncClient') as mock_client_class:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 201
|
||||
mock_response.is_success = True
|
||||
mock_response.json = lambda: mock_response_data
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.post.return_value = mock_response
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
cdp_url = await get_cloud_browser_cdp_url()
|
||||
|
||||
assert cdp_url == 'wss://test.proxy.daytona.works'
|
||||
|
||||
|
||||
async def test_cloud_browser_auth_error_no_fallback(temp_config_dir):
|
||||
"""Test that cloud browser throws error when auth fails (no fallback)."""
|
||||
|
||||
# Don't create auth config to trigger auth error
|
||||
profile = BrowserProfile(use_cloud=True)
|
||||
|
||||
# Test that cloud browser client raises error without fallback
|
||||
with patch('browser_use.browser.cloud.get_cloud_browser_cdp_url') as mock_cloud_cdp:
|
||||
mock_cloud_cdp.side_effect = CloudBrowserAuthError('No auth token')
|
||||
|
||||
# Verify that the cloud browser client raises the expected error
|
||||
with pytest.raises(CloudBrowserAuthError) as exc_info:
|
||||
await get_cloud_browser_cdp_url()
|
||||
|
||||
assert 'BROWSER_USE_API_KEY environment variable' in str(exc_info.value)
|
||||
|
||||
# Verify profile state unchanged (no fallback)
|
||||
assert profile.use_cloud is True
|
||||
assert profile.is_local is False
|
||||
|
||||
|
||||
async def test_stop_cloud_browser_session_function(mock_auth_config):
|
||||
"""Test the stop_cloud_browser_session convenience function."""
|
||||
|
||||
mock_response_data = {
|
||||
'id': 'test-browser-id',
|
||||
'status': 'stopped',
|
||||
'liveUrl': 'https://live.browser-use.com?wss=test',
|
||||
'cdpUrl': 'wss://test.proxy.daytona.works',
|
||||
'timeoutAt': '2025-09-17T04:35:36.049892',
|
||||
'startedAt': '2025-09-17T03:35:36.049974',
|
||||
'finishedAt': '2025-09-17T04:35:36.049892',
|
||||
}
|
||||
|
||||
with patch('httpx.AsyncClient') as mock_client_class:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.is_success = True
|
||||
mock_response.json = lambda: mock_response_data
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.patch.return_value = mock_response
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
result = await stop_cloud_browser_session('test-browser-id')
|
||||
|
||||
assert result.id == 'test-browser-id'
|
||||
assert result.status == 'stopped'
|
||||
Reference in New Issue
Block a user