chore: import upstream snapshot with attribution
Test Browser Use CLI Install / uv pip install (ubuntu-latest) (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use from local wheel (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use[cli] from PyPI (push) Failing after 1s
package / pip-install-on-macos-latest-py-3.11 (push) Has been skipped
package / pip-install-on-macos-latest-py-3.13 (push) Has been skipped
package / pip-install-on-ubuntu-latest-py-3.11 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.13 (push) Has been skipped
cloud_evals / trigger_cloud_eval_image_build (push) Failing after 1s
docker / build_publish_image (push) Failing after 1s
Test Browser Use CLI Install / browser-use skill sync (push) Failing after 1s
lint / code-style (push) Failing after 0s
lint / type-checker (push) Failing after 1s
package / pip-build (push) Failing after 1s
lint / syntax-errors (push) Failing after 3s
package / pip-install-on-ubuntu-latest-py-3.13 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.11 (push) Has been skipped
test / ${{ matrix.test_filename }} (push) Has been skipped
test / evaluate-tasks (push) Has been skipped
test / setup-chromium (push) Failing after 2s
test / find_tests (push) Failing after 2s
Test Browser Use CLI Install / uv pip install (windows-latest) (push) Has been cancelled
Test Browser Use CLI Install / uv pip install (macos-latest) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:02:32 +08:00
commit 4cd2d4af2b
475 changed files with 121829 additions and 0 deletions
+251
View File
@@ -0,0 +1,251 @@
# Browser Actor
Browser Actor is a web automation library built on CDP (Chrome DevTools Protocol) that provides low-level browser automation capabilities within the browser-use ecosystem.
## Usage
### Integrated with Browser (Recommended)
```python
from browser_use import Browser # Alias for BrowserSession
# Create and start browser session
browser = Browser()
await browser.start()
# Create new tabs and navigate
page = await browser.new_page("https://example.com")
pages = await browser.get_pages()
current_page = await browser.get_current_page()
```
### Direct Page Access (Advanced)
```python
from browser_use.actor import Page, Element, Mouse
# Create page with existing browser session
page = Page(browser_session, target_id, session_id)
```
## Basic Operations
```python
# Tab Management
page = await browser.new_page() # Create blank tab
page = await browser.new_page("https://example.com") # Create tab with URL
pages = await browser.get_pages() # Get all existing tabs
await browser.close_page(page) # Close specific tab
# Navigation
await page.goto("https://example.com")
await page.go_back()
await page.go_forward()
await page.reload()
```
## Element Operations
```python
# Find elements by CSS selector
elements = await page.get_elements_by_css_selector("input[type='text']")
buttons = await page.get_elements_by_css_selector("button.submit")
# Get element by backend node ID
element = await page.get_element(backend_node_id=12345)
# AI-powered element finding (requires LLM)
element = await page.get_element_by_prompt("search button", llm=your_llm)
element = await page.must_get_element_by_prompt("login form", llm=your_llm)
```
> **Note**: `get_elements_by_css_selector` returns immediately without waiting for visibility.
## Element Interactions
```python
# Element actions
await element.click(button='left', click_count=1, modifiers=['Control'])
await element.fill("Hello World") # Clears first, then types
await element.hover()
await element.focus()
await element.check() # Toggle checkbox/radio
await element.select_option(["option1", "option2"]) # For dropdown/select
await element.drag_to(target_element) # Drag and drop
# Element properties
value = await element.get_attribute("value")
box = await element.get_bounding_box() # Returns BoundingBox or None
info = await element.get_basic_info() # Comprehensive element info
screenshot_b64 = await element.screenshot(format='png')
# Execute JavaScript on element (this context is the element)
text = await element.evaluate("() => this.textContent")
await element.evaluate("(color) => this.style.backgroundColor = color", "yellow")
classes = await element.evaluate("() => Array.from(this.classList)")
```
## Mouse Operations
```python
# Mouse operations
mouse = await page.mouse
await mouse.click(x=100, y=200, button='left', click_count=1)
await mouse.move(x=300, y=400, steps=1)
await mouse.down(button='left') # Press button
await mouse.up(button='left') # Release button
await mouse.scroll(x=0, y=100, delta_x=0, delta_y=-500) # Scroll at coordinates
```
## Page Operations
```python
# JavaScript evaluation
result = await page.evaluate('() => document.title') # Must use arrow function format
result = await page.evaluate('(x, y) => x + y', 10, 20) # With arguments
# Keyboard input
await page.press("Control+A") # Key combinations supported
await page.press("Escape") # Single keys
# Page controls
await page.set_viewport_size(width=1920, height=1080)
page_screenshot = await page.screenshot() # PNG by default
page_png = await page.screenshot(format="png", quality=90)
# Page information
url = await page.get_url()
title = await page.get_title()
```
## AI-Powered Features
```python
# Content extraction using LLM
from pydantic import BaseModel
class ProductInfo(BaseModel):
name: str
price: float
description: str
# Extract structured data from current page
products = await page.extract_content(
"Find all products with their names, prices and descriptions",
ProductInfo,
llm=your_llm
)
```
## Core Classes
- **BrowserSession** (aliased as **Browser**): Main browser session manager with tab operations
- **Page**: Represents a single browser tab or iframe for page-level operations
- **Element**: Individual DOM element for interactions and property access
- **Mouse**: Mouse operations within a page (click, move, scroll)
## API Reference
### BrowserSession Methods (Tab Management)
- `start()` - Initialize and start the browser session
- `stop()` - Stop the browser session (keeps browser alive)
- `kill()` - Kill the browser process and reset all state
- `new_page(url=None)``Page` - Create blank tab or navigate to URL
- `get_pages()``list[Page]` - Get all available pages
- `get_current_page()``Page | None` - Get the currently focused page
- `close_page(page: Page | str)` - Close page by object or ID
- Session management and CDP client operations
### Page Methods (Page Operations)
- `get_elements_by_css_selector(selector: str)``list[Element]` - Find elements by CSS selector
- `get_element(backend_node_id: int)``Element` - Get element by backend node ID
- `get_element_by_prompt(prompt: str, llm)``Element | None` - AI-powered element finding
- `must_get_element_by_prompt(prompt: str, llm)``Element` - AI element finding (raises if not found)
- `extract_content(prompt: str, structured_output: type[T], llm)``T` - Extract structured data using LLM
- `goto(url: str)` - Navigate this page to URL
- `go_back()`, `go_forward()` - Navigate history (with error handling)
- `reload()` - Reload the current page
- `evaluate(page_function: str, *args)``str` - Execute JavaScript (MUST use (...args) => format)
- `press(key: str)` - Press key on page (supports "Control+A" format)
- `set_viewport_size(width: int, height: int)` - Set viewport dimensions
- `screenshot(format='png', quality=None)``str` - Take page screenshot, return base64
- `get_url()``str`, `get_title()``str` - Get page information
- `mouse``Mouse` - Get mouse interface for this page
### Element Methods (DOM Interactions)
- `click(button='left', click_count=1, modifiers=None)` - Click element with advanced fallbacks
- `fill(text: str, clear=True)` - Fill input with text (clears first by default)
- `hover()` - Hover over element
- `focus()` - Focus the element
- `check()` - Toggle checkbox/radio button (clicks to change state)
- `select_option(values: str | list[str])` - Select dropdown options
- `drag_to(target_element: Element | Position, source_position=None, target_position=None)` - Drag to target element
- `evaluate(page_function: str, *args)``str` - Execute JavaScript on element (this = element)
- `get_attribute(name: str)``str | None` - Get attribute value
- `get_bounding_box()``BoundingBox | None` - Get element position/size
- `screenshot(format='png', quality=None)``str` - Take element screenshot, return base64
- `get_basic_info()``ElementInfo` - Get comprehensive element information
### Mouse Methods (Coordinate-Based Operations)
- `click(x: int, y: int, button='left', click_count=1)` - Click at coordinates
- `move(x: int, y: int, steps=1)` - Move to coordinates
- `down(button='left', click_count=1)`, `up(button='left', click_count=1)` - Press/release button
- `scroll(x=0, y=0, delta_x=None, delta_y=None)` - Scroll page at coordinates
## Type Definitions
### Position
```python
class Position(TypedDict):
x: float
y: float
```
### BoundingBox
```python
class BoundingBox(TypedDict):
x: float
y: float
width: float
height: float
```
### ElementInfo
```python
class ElementInfo(TypedDict):
backendNodeId: int # CDP backend node ID
nodeId: int | None # CDP node ID
nodeName: str # HTML tag name (e.g., "DIV", "INPUT")
nodeType: int # DOM node type
nodeValue: str | None # Text content for text nodes
attributes: dict[str, str] # HTML attributes
boundingBox: BoundingBox | None # Element position and size
error: str | None # Error message if info retrieval failed
```
## Important Usage Notes
**This is browser-use actor, NOT Playwright or Selenium.** Only use the methods documented above.
### Critical JavaScript Rules
- `page.evaluate()` and `element.evaluate()` MUST use `(...args) => {}` arrow function format
- Always returns string (objects are JSON-stringified automatically)
- Use single quotes around the function: `page.evaluate('() => document.title')`
- For complex selectors in JS: `'() => document.querySelector("input[name=\\"email\\"]")'`
- `element.evaluate()`: `this` context is bound to the element automatically
### Method Restrictions
- `get_elements_by_css_selector()` returns immediately (no automatic waiting)
- For dropdowns: use `element.select_option()`, NOT `element.fill()`
- Form submission: click submit button or use `page.press("Enter")`
- No methods like: `element.submit()`, `element.dispatch_event()`, `element.get_property()`
### Error Prevention
- Always verify page state changes with `page.get_url()`, `page.get_title()`
- Use `element.get_attribute()` to check element properties
- Validate CSS selectors before use
- Handle navigation timing with appropriate `asyncio.sleep()` calls
### AI Features
- `get_element_by_prompt()` and `extract_content()` require an LLM instance
- These methods use DOM analysis and structured output parsing
- Best for complex page understanding and data extraction tasks
+11
View File
@@ -0,0 +1,11 @@
"""CDP-Use High-Level Library
A Playwright-like library built on top of CDP (Chrome DevTools Protocol).
"""
from .element import Element
from .mouse import Mouse
from .page import Page
from .utils import Utils
__all__ = ['Page', 'Element', 'Mouse', 'Utils']
File diff suppressed because it is too large Load Diff
+134
View File
@@ -0,0 +1,134 @@
"""Mouse class for mouse operations."""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from cdp_use.cdp.input.commands import DispatchMouseEventParameters, SynthesizeScrollGestureParameters
from cdp_use.cdp.input.types import MouseButton
from browser_use.browser.session import BrowserSession
class Mouse:
"""Mouse operations for a target."""
def __init__(self, browser_session: 'BrowserSession', session_id: str | None = None, target_id: str | None = None):
self._browser_session = browser_session
self._client = browser_session.cdp_client
self._session_id = session_id
self._target_id = target_id
async def click(self, x: int, y: int, button: 'MouseButton' = 'left', click_count: int = 1) -> None:
"""Click at the specified coordinates."""
# Mouse press
press_params: 'DispatchMouseEventParameters' = {
'type': 'mousePressed',
'x': x,
'y': y,
'button': button,
'clickCount': click_count,
}
await self._client.send.Input.dispatchMouseEvent(
press_params,
session_id=self._session_id,
)
# Mouse release
release_params: 'DispatchMouseEventParameters' = {
'type': 'mouseReleased',
'x': x,
'y': y,
'button': button,
'clickCount': click_count,
}
await self._client.send.Input.dispatchMouseEvent(
release_params,
session_id=self._session_id,
)
async def down(self, button: 'MouseButton' = 'left', click_count: int = 1) -> None:
"""Press mouse button down."""
params: 'DispatchMouseEventParameters' = {
'type': 'mousePressed',
'x': 0, # Will use last mouse position
'y': 0,
'button': button,
'clickCount': click_count,
}
await self._client.send.Input.dispatchMouseEvent(
params,
session_id=self._session_id,
)
async def up(self, button: 'MouseButton' = 'left', click_count: int = 1) -> None:
"""Release mouse button."""
params: 'DispatchMouseEventParameters' = {
'type': 'mouseReleased',
'x': 0, # Will use last mouse position
'y': 0,
'button': button,
'clickCount': click_count,
}
await self._client.send.Input.dispatchMouseEvent(
params,
session_id=self._session_id,
)
async def move(self, x: int, y: int, steps: int = 1) -> None:
"""Move mouse to the specified coordinates."""
# TODO: Implement smooth movement with multiple steps if needed
_ = steps # Acknowledge parameter for future use
params: 'DispatchMouseEventParameters' = {'type': 'mouseMoved', 'x': x, 'y': y}
await self._client.send.Input.dispatchMouseEvent(params, session_id=self._session_id)
async def scroll(self, x: int = 0, y: int = 0, delta_x: int | None = None, delta_y: int | None = None) -> None:
"""Scroll the page using robust CDP methods."""
if not self._session_id:
raise RuntimeError('Session ID is required for scroll operations')
# Method 1: Try mouse wheel event (most reliable)
try:
# Get viewport dimensions
layout_metrics = await self._client.send.Page.getLayoutMetrics(session_id=self._session_id)
viewport_width = layout_metrics['layoutViewport']['clientWidth']
viewport_height = layout_metrics['layoutViewport']['clientHeight']
# Use provided coordinates or center of viewport
scroll_x = x if x > 0 else viewport_width / 2
scroll_y = y if y > 0 else viewport_height / 2
# Calculate scroll deltas (positive = down/right)
scroll_delta_x = delta_x or 0
scroll_delta_y = delta_y or 0
# Dispatch mouse wheel event
await self._client.send.Input.dispatchMouseEvent(
params={
'type': 'mouseWheel',
'x': scroll_x,
'y': scroll_y,
'deltaX': scroll_delta_x,
'deltaY': scroll_delta_y,
},
session_id=self._session_id,
)
return
except Exception:
pass
# Method 2: Fallback to synthesizeScrollGesture
try:
params: 'SynthesizeScrollGestureParameters' = {'x': x, 'y': y, 'xDistance': delta_x or 0, 'yDistance': delta_y or 0}
await self._client.send.Input.synthesizeScrollGesture(
params,
session_id=self._session_id,
)
except Exception:
# Method 3: JavaScript fallback
scroll_js = f'window.scrollBy({delta_x or 0}, {delta_y or 0})'
await self._client.send.Runtime.evaluate(
params={'expression': scroll_js, 'returnByValue': True},
session_id=self._session_id,
)
+564
View File
@@ -0,0 +1,564 @@
"""Page class for page-level operations."""
from typing import TYPE_CHECKING, TypeVar
from pydantic import BaseModel
from browser_use import logger
from browser_use.actor.utils import get_key_info
from browser_use.dom.serializer.serializer import DOMTreeSerializer
from browser_use.dom.service import DomService
from browser_use.llm.messages import SystemMessage, UserMessage
T = TypeVar('T', bound=BaseModel)
if TYPE_CHECKING:
from cdp_use.cdp.dom.commands import (
DescribeNodeParameters,
QuerySelectorAllParameters,
)
from cdp_use.cdp.emulation.commands import SetDeviceMetricsOverrideParameters
from cdp_use.cdp.input.commands import (
DispatchKeyEventParameters,
)
from cdp_use.cdp.page.commands import CaptureScreenshotParameters, NavigateParameters, NavigateToHistoryEntryParameters
from cdp_use.cdp.runtime.commands import EvaluateParameters
from cdp_use.cdp.target.commands import (
AttachToTargetParameters,
GetTargetInfoParameters,
)
from cdp_use.cdp.target.types import TargetInfo
from browser_use.browser.session import BrowserSession
from browser_use.llm.base import BaseChatModel
from .element import Element
from .mouse import Mouse
class Page:
"""Page operations (tab or iframe)."""
def __init__(
self, browser_session: 'BrowserSession', target_id: str, session_id: str | None = None, llm: 'BaseChatModel | None' = None
):
self._browser_session = browser_session
self._client = browser_session.cdp_client
self._target_id = target_id
self._session_id: str | None = session_id
self._mouse: 'Mouse | None' = None
self._llm = llm
async def _ensure_session(self) -> str:
"""Ensure we have a session ID for this target."""
if not self._session_id:
params: 'AttachToTargetParameters' = {'targetId': self._target_id, 'flatten': True}
result = await self._client.send.Target.attachToTarget(params)
self._session_id = result['sessionId']
# Enable necessary domains
import asyncio
await asyncio.gather(
self._client.send.Page.enable(session_id=self._session_id),
self._client.send.DOM.enable(session_id=self._session_id),
self._client.send.Runtime.enable(session_id=self._session_id),
self._client.send.Network.enable(session_id=self._session_id),
)
return self._session_id
@property
async def session_id(self) -> str:
"""Get the session ID for this target.
@dev Pass this to an arbitrary CDP call
"""
return await self._ensure_session()
@property
async def mouse(self) -> 'Mouse':
"""Get the mouse interface for this target."""
if not self._mouse:
session_id = await self._ensure_session()
from .mouse import Mouse
self._mouse = Mouse(self._browser_session, session_id, self._target_id)
return self._mouse
async def reload(self) -> None:
"""Reload the target."""
session_id = await self._ensure_session()
await self._client.send.Page.reload(session_id=session_id)
async def get_element(self, backend_node_id: int) -> 'Element':
"""Get an element by its backend node ID."""
session_id = await self._ensure_session()
from .element import Element as Element_
return Element_(self._browser_session, backend_node_id, session_id)
async def evaluate(self, page_function: str, *args) -> str:
"""Execute JavaScript in the target.
Args:
page_function: JavaScript code that MUST start with (...args) => format
*args: Arguments to pass to the function
Returns:
String representation of the JavaScript execution result.
Objects and arrays are JSON-stringified.
"""
session_id = await self._ensure_session()
# Clean and fix common JavaScript string parsing issues
page_function = self._fix_javascript_string(page_function)
# Enforce arrow function format
if not (page_function.startswith('(') and '=>' in page_function):
raise ValueError(f'JavaScript code must start with (...args) => format. Got: {page_function[:50]}...')
# Build the expression - call the arrow function with provided args
if args:
# Convert args to JSON representation for safe passing
import json
arg_strs = [json.dumps(arg) for arg in args]
expression = f'({page_function})({", ".join(arg_strs)})'
else:
expression = f'({page_function})()'
# Debug: log the actual expression being evaluated
logger.debug(f'Evaluating JavaScript: {repr(expression)}')
params: 'EvaluateParameters' = {'expression': expression, 'returnByValue': True, 'awaitPromise': True}
result = await self._client.send.Runtime.evaluate(
params,
session_id=session_id,
)
if 'exceptionDetails' in result:
raise RuntimeError(f'JavaScript evaluation failed: {result["exceptionDetails"]}')
value = result.get('result', {}).get('value')
# Always return string representation
if value is None:
return ''
elif isinstance(value, str):
return value
else:
# Convert objects, numbers, booleans to string
import json
try:
return json.dumps(value) if isinstance(value, (dict, list)) else str(value)
except (TypeError, ValueError):
return str(value)
def _fix_javascript_string(self, js_code: str) -> str:
"""Fix common JavaScript string parsing issues when written as Python string."""
# Just do minimal, safe cleaning
js_code = js_code.strip()
# Only fix the most common and safe issues:
# 1. Remove obvious Python string wrapper quotes if they exist
if (js_code.startswith('"') and js_code.endswith('"')) or (js_code.startswith("'") and js_code.endswith("'")):
# Check if it's a wrapped string (not part of JS syntax)
inner = js_code[1:-1]
if inner.count('"') + inner.count("'") == 0 or '() =>' in inner:
js_code = inner
# 2. Only fix clearly escaped quotes that shouldn't be
# But be very conservative - only if we're sure it's a Python string artifact
if '\\"' in js_code and js_code.count('\\"') > js_code.count('"'):
js_code = js_code.replace('\\"', '"')
if "\\'" in js_code and js_code.count("\\'") > js_code.count("'"):
js_code = js_code.replace("\\'", "'")
# 3. Basic whitespace normalization only
js_code = js_code.strip()
# Final validation - ensure it's not empty
if not js_code:
raise ValueError('JavaScript code is empty after cleaning')
return js_code
async def screenshot(self, format: str = 'png', quality: int | None = None) -> str:
"""Take a screenshot and return base64 encoded image.
Args:
format: Image format ('jpeg', 'png', 'webp')
quality: Quality 0-100 for JPEG format
Returns:
Base64-encoded image data
"""
session_id = await self._ensure_session()
params: 'CaptureScreenshotParameters' = {'format': format}
if quality is not None and format.lower() == 'jpeg':
params['quality'] = quality
result = await self._client.send.Page.captureScreenshot(params, session_id=session_id)
return result['data']
async def press(self, key: str) -> None:
"""Press a key on the page (sends keyboard input to the focused element or page)."""
session_id = await self._ensure_session()
# Handle key combinations like "Control+A"
if '+' in key:
parts = key.split('+')
modifiers = parts[:-1]
main_key = parts[-1]
# Calculate modifier bitmask
modifier_value = 0
modifier_map = {'Alt': 1, 'Control': 2, 'Meta': 4, 'Shift': 8}
for mod in modifiers:
modifier_value |= modifier_map.get(mod, 0)
# Press modifier keys
for mod in modifiers:
code, vk_code = get_key_info(mod)
params: 'DispatchKeyEventParameters' = {'type': 'keyDown', 'key': mod, 'code': code}
if vk_code is not None:
params['windowsVirtualKeyCode'] = vk_code
await self._client.send.Input.dispatchKeyEvent(params, session_id=session_id)
# Press main key with modifiers bitmask
main_code, main_vk_code = get_key_info(main_key)
main_down_params: 'DispatchKeyEventParameters' = {
'type': 'keyDown',
'key': main_key,
'code': main_code,
'modifiers': modifier_value,
}
if main_vk_code is not None:
main_down_params['windowsVirtualKeyCode'] = main_vk_code
await self._client.send.Input.dispatchKeyEvent(main_down_params, session_id=session_id)
main_up_params: 'DispatchKeyEventParameters' = {
'type': 'keyUp',
'key': main_key,
'code': main_code,
'modifiers': modifier_value,
}
if main_vk_code is not None:
main_up_params['windowsVirtualKeyCode'] = main_vk_code
await self._client.send.Input.dispatchKeyEvent(main_up_params, session_id=session_id)
# Release modifier keys
for mod in reversed(modifiers):
code, vk_code = get_key_info(mod)
release_params: 'DispatchKeyEventParameters' = {'type': 'keyUp', 'key': mod, 'code': code}
if vk_code is not None:
release_params['windowsVirtualKeyCode'] = vk_code
await self._client.send.Input.dispatchKeyEvent(release_params, session_id=session_id)
else:
# Simple key press
code, vk_code = get_key_info(key)
key_down_params: 'DispatchKeyEventParameters' = {'type': 'keyDown', 'key': key, 'code': code}
if vk_code is not None:
key_down_params['windowsVirtualKeyCode'] = vk_code
await self._client.send.Input.dispatchKeyEvent(key_down_params, session_id=session_id)
key_up_params: 'DispatchKeyEventParameters' = {'type': 'keyUp', 'key': key, 'code': code}
if vk_code is not None:
key_up_params['windowsVirtualKeyCode'] = vk_code
await self._client.send.Input.dispatchKeyEvent(key_up_params, session_id=session_id)
async def set_viewport_size(self, width: int, height: int) -> None:
"""Set the viewport size."""
session_id = await self._ensure_session()
params: 'SetDeviceMetricsOverrideParameters' = {
'width': width,
'height': height,
'deviceScaleFactor': 1.0,
'mobile': False,
}
await self._client.send.Emulation.setDeviceMetricsOverride(
params,
session_id=session_id,
)
# Target properties (from CDP getTargetInfo)
async def get_target_info(self) -> 'TargetInfo':
"""Get target information."""
params: 'GetTargetInfoParameters' = {'targetId': self._target_id}
result = await self._client.send.Target.getTargetInfo(params)
return result['targetInfo']
async def get_url(self) -> str:
"""Get the current URL."""
info = await self.get_target_info()
return info.get('url', '')
async def get_title(self) -> str:
"""Get the current title."""
info = await self.get_target_info()
return info.get('title', '')
async def goto(self, url: str) -> None:
"""Navigate this target to a URL."""
session_id = await self._ensure_session()
params: 'NavigateParameters' = {'url': url}
await self._client.send.Page.navigate(params, session_id=session_id)
async def navigate(self, url: str) -> None:
"""Alias for goto."""
await self.goto(url)
async def go_back(self) -> None:
"""Navigate back in history."""
session_id = await self._ensure_session()
try:
# Get navigation history
history = await self._client.send.Page.getNavigationHistory(session_id=session_id)
current_index = history['currentIndex']
entries = history['entries']
# Check if we can go back
if current_index <= 0:
raise RuntimeError('Cannot go back - no previous entry in history')
# Navigate to the previous entry
previous_entry_id = entries[current_index - 1]['id']
params: 'NavigateToHistoryEntryParameters' = {'entryId': previous_entry_id}
await self._client.send.Page.navigateToHistoryEntry(params, session_id=session_id)
except Exception as e:
raise RuntimeError(f'Failed to navigate back: {e}')
async def go_forward(self) -> None:
"""Navigate forward in history."""
session_id = await self._ensure_session()
try:
# Get navigation history
history = await self._client.send.Page.getNavigationHistory(session_id=session_id)
current_index = history['currentIndex']
entries = history['entries']
# Check if we can go forward
if current_index >= len(entries) - 1:
raise RuntimeError('Cannot go forward - no next entry in history')
# Navigate to the next entry
next_entry_id = entries[current_index + 1]['id']
params: 'NavigateToHistoryEntryParameters' = {'entryId': next_entry_id}
await self._client.send.Page.navigateToHistoryEntry(params, session_id=session_id)
except Exception as e:
raise RuntimeError(f'Failed to navigate forward: {e}')
# Element finding methods (these would need to be implemented based on DOM queries)
async def get_elements_by_css_selector(self, selector: str) -> list['Element']:
"""Get elements by CSS selector."""
session_id = await self._ensure_session()
# Get document first
doc_result = await self._client.send.DOM.getDocument(session_id=session_id)
document_node_id = doc_result['root']['nodeId']
# Query selector all
query_params: 'QuerySelectorAllParameters' = {'nodeId': document_node_id, 'selector': selector}
result = await self._client.send.DOM.querySelectorAll(query_params, session_id=session_id)
elements = []
from .element import Element as Element_
# Convert node IDs to backend node IDs
for node_id in result['nodeIds']:
# Get backend node ID
describe_params: 'DescribeNodeParameters' = {'nodeId': node_id}
node_result = await self._client.send.DOM.describeNode(describe_params, session_id=session_id)
backend_node_id = node_result['node']['backendNodeId']
elements.append(Element_(self._browser_session, backend_node_id, session_id))
return elements
# AI METHODS
@property
def dom_service(self) -> 'DomService':
"""Get the DOM service for this target."""
return DomService(self._browser_session)
async def get_element_by_prompt(self, prompt: str, llm: 'BaseChatModel | None' = None) -> 'Element | None':
"""Get an element by a prompt."""
await self._ensure_session()
llm = llm or self._llm
if not llm:
raise ValueError('LLM not provided')
dom_service = self.dom_service
# Lazy fetch all_frames inside get_dom_tree if needed (for cross-origin iframes)
enhanced_dom_tree, _ = await dom_service.get_dom_tree(target_id=self._target_id, all_frames=None)
session_id = self._browser_session.id
serialized_dom_state, _ = DOMTreeSerializer(
enhanced_dom_tree, None, paint_order_filtering=True, session_id=session_id
).serialize_accessible_elements()
llm_representation = serialized_dom_state.llm_representation()
system_message = SystemMessage(
content="""You are an AI created to find an element on a page by a prompt.
<browser_state>
Interactive Elements: All interactive elements will be provided in format as [index]<type>text</type> where
- index: Numeric identifier for interaction
- type: HTML element type (button, input, etc.)
- text: Element description
Examples:
[33]<div>User form</div>
[35]<button aria-label='Submit form'>Submit</button>
Note that:
- Only elements with numeric indexes in [] are interactive
- (stacked) indentation (with \t) is important and means that the element is a (html) child of the element above (with a lower index)
- Pure text elements without [] are not interactive.
</browser_state>
Your task is to find an element index (if any) that matches the prompt (written in <prompt> tag).
If non of the elements matches the, return None.
Before you return the element index, reason about the state and elements for a sentence or two."""
)
state_message = UserMessage(
content=f"""
<browser_state>
{llm_representation}
</browser_state>
<prompt>
{prompt}
</prompt>
"""
)
class ElementResponse(BaseModel):
# thinking: str
element_highlight_index: int | None
llm_response = await llm.ainvoke(
[
system_message,
state_message,
],
output_format=ElementResponse,
)
element_highlight_index = llm_response.completion.element_highlight_index
if element_highlight_index is None or element_highlight_index not in serialized_dom_state.selector_map:
return None
element = serialized_dom_state.selector_map[element_highlight_index]
from .element import Element as Element_
return Element_(self._browser_session, element.backend_node_id, self._session_id)
async def must_get_element_by_prompt(self, prompt: str, llm: 'BaseChatModel | None' = None) -> 'Element':
"""Get an element by a prompt.
@dev LLM can still return None, this just raises an error if the element is not found.
"""
element = await self.get_element_by_prompt(prompt, llm)
if element is None:
raise ValueError(f'No element found for prompt: {prompt}')
return element
async def extract_content(self, prompt: str, structured_output: type[T], llm: 'BaseChatModel | None' = None) -> T:
"""Extract structured content from the current page using LLM.
Extracts clean markdown from the page and sends it to LLM for structured data extraction.
Args:
prompt: Description of what content to extract
structured_output: Pydantic BaseModel class defining the expected output structure
llm: Language model to use for extraction
Returns:
The structured BaseModel instance with extracted content
"""
llm = llm or self._llm
if not llm:
raise ValueError('LLM not provided')
# Extract clean markdown using the same method as in tools/service.py
try:
content, content_stats = await self._extract_clean_markdown()
except Exception as e:
raise RuntimeError(f'Could not extract clean markdown: {type(e).__name__}')
# System prompt for structured extraction
system_prompt = """
You are an expert at extracting structured data from the markdown of a webpage.
<input>
You will be given a query and the markdown of a webpage that has been filtered to remove noise and advertising content.
</input>
<instructions>
- You are tasked to extract information from the webpage that is relevant to the query.
- You should ONLY use the information available in the webpage to answer the query. Do not make up information or provide guess from your own knowledge.
- If the information relevant to the query is not available in the page, your response should mention that.
- If the query asks for all items, products, etc., make sure to directly list all of them.
- Return the extracted content in the exact structured format specified.
</instructions>
<output>
- Your output should present ALL the information relevant to the query in the specified structured format.
- Do not answer in conversational format - directly output the relevant information in the structured format.
</output>
""".strip()
# Build prompt with just query and content
prompt_content = f'<query>\n{prompt}\n</query>\n\n<webpage_content>\n{content}\n</webpage_content>'
# Send to LLM with structured output
import asyncio
try:
response = await asyncio.wait_for(
llm.ainvoke(
[SystemMessage(content=system_prompt), UserMessage(content=prompt_content)], output_format=structured_output
),
timeout=120.0,
)
# Return the structured output BaseModel instance
return response.completion
except Exception as e:
raise RuntimeError(str(e))
async def _extract_clean_markdown(self, extract_links: bool = False) -> tuple[str, dict]:
"""Extract clean markdown from the current page using enhanced DOM tree.
Uses the shared markdown extractor for consistency with tools/service.py.
"""
from browser_use.dom.markdown_extractor import extract_clean_markdown
dom_service = self.dom_service
return await extract_clean_markdown(dom_service=dom_service, target_id=self._target_id, extract_links=extract_links)
+41
View File
@@ -0,0 +1,41 @@
import asyncio
from browser_use import Agent, Browser, ChatOpenAI
llm = ChatOpenAI('gpt-4.1-mini')
async def main():
"""
Main function demonstrating mixed automation with Browser-Use and Playwright.
"""
print('🚀 Mixed Automation with Browser-Use and Actor API')
browser = Browser(keep_alive=True)
await browser.start()
page = await browser.get_current_page() or await browser.new_page()
# Go to apple wikipedia page
await page.goto('https://www.google.com/travel/flights')
await asyncio.sleep(1)
round_trip_button = await page.must_get_element_by_prompt('round trip button', llm)
await round_trip_button.click()
one_way_button = await page.must_get_element_by_prompt('one way button', llm)
await one_way_button.click()
await asyncio.sleep(1)
agent = Agent(task='Find the cheapest flight from London to Paris on 2025-10-15', llm=llm, browser_session=browser)
await agent.run()
input('Press Enter to continue...')
await browser.stop()
if __name__ == '__main__':
asyncio.run(main())
@@ -0,0 +1,54 @@
import asyncio
from pydantic import BaseModel
from browser_use import Browser, ChatOpenAI
TASK = """
On the current wikipedia page, find the latest huge edit and tell me what is was about.
"""
class LatestEditFinder(BaseModel):
"""Find the latest huge edit on the current wikipedia page."""
latest_edit: str
edit_time: str
edit_author: str
edit_summary: str
edit_url: str
llm = ChatOpenAI('gpt-4.1-mini')
async def main():
"""
Main function demonstrating mixed automation with Browser-Use and Playwright.
"""
print('🚀 Mixed Automation with Browser-Use and Actor API')
browser = Browser(keep_alive=True)
await browser.start()
page = await browser.get_current_page() or await browser.new_page()
# Go to apple wikipedia page
await page.goto('https://browser-use.github.io/stress-tests/challenges/angularjs-form.html')
await asyncio.sleep(1)
element = await page.get_element_by_prompt('zip code input', llm)
print('Element found', element)
if element:
await element.click()
else:
print('No element found')
await browser.stop()
if __name__ == '__main__':
asyncio.run(main())
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""
Playground script to test the browser-use actor API.
This script demonstrates:
- Starting a browser session
- Using the actor API to navigate and interact
- Finding elements, clicking, scrolling, JavaScript evaluation
- Testing most of the available methods
"""
import asyncio
import json
import logging
from browser_use import Browser
# Configure logging to see what's happening
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def main():
"""Main playground function."""
logger.info('🚀 Starting browser actor playground')
# Create browser session
browser = Browser()
try:
# Start the browser
await browser.start()
logger.info('✅ Browser session started')
# Navigate to Wikipedia using integrated methods
logger.info('📖 Navigating to Wikipedia...')
page = await browser.new_page('https://en.wikipedia.org')
# Get basic page info
url = await page.get_url()
title = await page.get_title()
logger.info(f'📄 Page loaded: {title} ({url})')
# Take a screenshot
logger.info('📸 Taking initial screenshot...')
screenshot_b64 = await page.screenshot()
logger.info(f'📸 Screenshot captured: {len(screenshot_b64)} bytes')
# Set viewport size
logger.info('🖥️ Setting viewport to 1920x1080...')
await page.set_viewport_size(1920, 1080)
# Execute some JavaScript to count links
logger.info('🔍 Counting article links using JavaScript...')
js_code = """() => {
// Find all article links on the page
const links = Array.from(document.querySelectorAll('a[href*="/wiki/"]:not([href*=":"])'))
.filter(link => !link.href.includes('Main_Page') && !link.href.includes('Special:'));
return {
total: links.length,
sample: links.slice(0, 3).map(link => ({
href: link.href,
text: link.textContent.trim()
}))
};
}"""
link_info = json.loads(await page.evaluate(js_code))
logger.info(f'🔗 Found {link_info["total"]} article links')
# Try to find and interact with links using CSS selector
try:
# Find article links on the page
links = await page.get_elements_by_css_selector('a[href*="/wiki/"]:not([href*=":"])')
if links:
logger.info(f'📋 Found {len(links)} wiki links via CSS selector')
# Pick the first link
link_element = links[0]
# Get link info using available methods
basic_info = await link_element.get_basic_info()
link_href = await link_element.get_attribute('href')
logger.info(f'🎯 Selected element: <{basic_info["nodeName"]}>')
logger.info(f'🔗 Link href: {link_href}')
if basic_info['boundingBox']:
bbox = basic_info['boundingBox']
logger.info(f'📏 Position: ({bbox["x"]}, {bbox["y"]}) Size: {bbox["width"]}x{bbox["height"]}')
# Test element interactions with robust implementations
logger.info('👆 Hovering over the element...')
await link_element.hover()
await asyncio.sleep(1)
logger.info('🔍 Focusing the element...')
await link_element.focus()
await asyncio.sleep(0.5)
# Click the link using robust click method
logger.info('🖱️ Clicking the link with robust fallbacks...')
await link_element.click()
# Wait for navigation
await asyncio.sleep(3)
# Get new page info
new_url = await page.get_url()
new_title = await page.get_title()
logger.info(f'📄 Navigated to: {new_title}')
logger.info(f'🌐 New URL: {new_url}')
else:
logger.warning('❌ No links found to interact with')
except Exception as e:
logger.warning(f'⚠️ Link interaction failed: {e}')
# Scroll down the page
logger.info('📜 Scrolling down the page...')
mouse = await page.mouse
await mouse.scroll(x=0, y=100, delta_y=500)
await asyncio.sleep(1)
# Test mouse operations
logger.info('🖱️ Testing mouse operations...')
await mouse.move(x=100, y=200)
await mouse.click(x=150, y=250)
# Execute more JavaScript examples
logger.info('🧪 Testing JavaScript evaluation...')
# Simple expressions
page_height = await page.evaluate('() => document.body.scrollHeight')
current_scroll = await page.evaluate('() => window.pageYOffset')
logger.info(f'📏 Page height: {page_height}px, current scroll: {current_scroll}px')
# JavaScript with arguments
result = await page.evaluate('(x) => x * 2', 21)
logger.info(f'🧮 JavaScript with args: 21 * 2 = {result}')
# More complex JavaScript
page_stats = json.loads(
await page.evaluate("""() => {
return {
url: window.location.href,
title: document.title,
links: document.querySelectorAll('a').length,
images: document.querySelectorAll('img').length,
scrollTop: window.pageYOffset,
viewportHeight: window.innerHeight
};
}""")
)
logger.info(f'📊 Page stats: {page_stats}')
# Get page title using different methods
title_via_js = await page.evaluate('() => document.title')
title_via_api = await page.get_title()
logger.info(f'📝 Title via JS: "{title_via_js}"')
logger.info(f'📝 Title via API: "{title_via_api}"')
# Take a final screenshot
logger.info('📸 Taking final screenshot...')
final_screenshot = await page.screenshot()
logger.info(f'📸 Final screenshot: {len(final_screenshot)} bytes')
# Test browser navigation with error handling
logger.info('⬅️ Testing browser back navigation...')
try:
await page.go_back()
await asyncio.sleep(2)
back_url = await page.get_url()
back_title = await page.get_title()
logger.info(f'📄 After going back: {back_title}')
logger.info(f'🌐 Back URL: {back_url}')
except RuntimeError as e:
logger.info(f'️ Navigation back failed as expected: {e}')
# Test creating new page
logger.info('🆕 Creating new blank page...')
new_page = await browser.new_page()
new_page_url = await new_page.get_url()
logger.info(f'🆕 New page created with URL: {new_page_url}')
# Get all pages
all_pages = await browser.get_pages()
logger.info(f'📑 Total pages: {len(all_pages)}')
# Test form interaction if we can find a form
try:
# Look for search input on the page
search_inputs = await page.get_elements_by_css_selector('input[type="search"], input[name*="search"]')
if search_inputs:
search_input = search_inputs[0]
logger.info('🔍 Found search input, testing form interaction...')
await search_input.focus()
await search_input.fill('test search query')
await page.press('Enter')
logger.info('✅ Form interaction test completed')
else:
logger.info('️ No search inputs found for form testing')
except Exception as e:
logger.info(f'️ Form interaction test skipped: {e}')
# wait 2 seconds before closing the new page
logger.info('🕒 Waiting 2 seconds before closing the new page...')
await asyncio.sleep(2)
logger.info('🗑️ Closing new page...')
await browser.close_page(new_page)
logger.info('✅ Playground completed successfully!')
input('Press Enter to continue...')
except Exception as e:
logger.error(f'❌ Error in playground: {e}', exc_info=True)
finally:
# Clean up
logger.info('🧹 Cleaning up...')
try:
await browser.stop()
logger.info('✅ Browser session stopped')
except Exception as e:
logger.error(f'❌ Error stopping browser: {e}')
if __name__ == '__main__':
asyncio.run(main())
+176
View File
@@ -0,0 +1,176 @@
"""Utility functions for actor operations."""
class Utils:
"""Utility functions for actor operations."""
@staticmethod
def get_key_info(key: str) -> tuple[str, int | None]:
"""Get the code and windowsVirtualKeyCode for a key.
Args:
key: Key name (e.g., 'Enter', 'ArrowUp', 'a', 'A')
Returns:
Tuple of (code, windowsVirtualKeyCode)
Reference: Windows Virtual Key Codes
https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
"""
# Complete mapping of key names to (code, virtualKeyCode)
# Based on standard Windows Virtual Key Codes
key_map = {
# Navigation keys
'Backspace': ('Backspace', 8),
'Tab': ('Tab', 9),
'Enter': ('Enter', 13),
'Escape': ('Escape', 27),
'Space': ('Space', 32),
' ': ('Space', 32),
'PageUp': ('PageUp', 33),
'PageDown': ('PageDown', 34),
'End': ('End', 35),
'Home': ('Home', 36),
'ArrowLeft': ('ArrowLeft', 37),
'ArrowUp': ('ArrowUp', 38),
'ArrowRight': ('ArrowRight', 39),
'ArrowDown': ('ArrowDown', 40),
'Insert': ('Insert', 45),
'Delete': ('Delete', 46),
# Modifier keys
'Shift': ('ShiftLeft', 16),
'ShiftLeft': ('ShiftLeft', 16),
'ShiftRight': ('ShiftRight', 16),
'Control': ('ControlLeft', 17),
'ControlLeft': ('ControlLeft', 17),
'ControlRight': ('ControlRight', 17),
'Alt': ('AltLeft', 18),
'AltLeft': ('AltLeft', 18),
'AltRight': ('AltRight', 18),
'Meta': ('MetaLeft', 91),
'MetaLeft': ('MetaLeft', 91),
'MetaRight': ('MetaRight', 92),
# Function keys F1-F24
'F1': ('F1', 112),
'F2': ('F2', 113),
'F3': ('F3', 114),
'F4': ('F4', 115),
'F5': ('F5', 116),
'F6': ('F6', 117),
'F7': ('F7', 118),
'F8': ('F8', 119),
'F9': ('F9', 120),
'F10': ('F10', 121),
'F11': ('F11', 122),
'F12': ('F12', 123),
'F13': ('F13', 124),
'F14': ('F14', 125),
'F15': ('F15', 126),
'F16': ('F16', 127),
'F17': ('F17', 128),
'F18': ('F18', 129),
'F19': ('F19', 130),
'F20': ('F20', 131),
'F21': ('F21', 132),
'F22': ('F22', 133),
'F23': ('F23', 134),
'F24': ('F24', 135),
# Numpad keys
'NumLock': ('NumLock', 144),
'Numpad0': ('Numpad0', 96),
'Numpad1': ('Numpad1', 97),
'Numpad2': ('Numpad2', 98),
'Numpad3': ('Numpad3', 99),
'Numpad4': ('Numpad4', 100),
'Numpad5': ('Numpad5', 101),
'Numpad6': ('Numpad6', 102),
'Numpad7': ('Numpad7', 103),
'Numpad8': ('Numpad8', 104),
'Numpad9': ('Numpad9', 105),
'NumpadMultiply': ('NumpadMultiply', 106),
'NumpadAdd': ('NumpadAdd', 107),
'NumpadSubtract': ('NumpadSubtract', 109),
'NumpadDecimal': ('NumpadDecimal', 110),
'NumpadDivide': ('NumpadDivide', 111),
# Lock keys
'CapsLock': ('CapsLock', 20),
'ScrollLock': ('ScrollLock', 145),
# OEM/Punctuation keys (US keyboard layout)
'Semicolon': ('Semicolon', 186),
';': ('Semicolon', 186),
'Equal': ('Equal', 187),
'=': ('Equal', 187),
'Comma': ('Comma', 188),
',': ('Comma', 188),
'Minus': ('Minus', 189),
'-': ('Minus', 189),
'Period': ('Period', 190),
'.': ('Period', 190),
'Slash': ('Slash', 191),
'/': ('Slash', 191),
'Backquote': ('Backquote', 192),
'`': ('Backquote', 192),
'BracketLeft': ('BracketLeft', 219),
'[': ('BracketLeft', 219),
'Backslash': ('Backslash', 220),
'\\': ('Backslash', 220),
'BracketRight': ('BracketRight', 221),
']': ('BracketRight', 221),
'Quote': ('Quote', 222),
"'": ('Quote', 222),
# Media/Browser keys
'AudioVolumeMute': ('AudioVolumeMute', 173),
'AudioVolumeDown': ('AudioVolumeDown', 174),
'AudioVolumeUp': ('AudioVolumeUp', 175),
'MediaTrackNext': ('MediaTrackNext', 176),
'MediaTrackPrevious': ('MediaTrackPrevious', 177),
'MediaStop': ('MediaStop', 178),
'MediaPlayPause': ('MediaPlayPause', 179),
'BrowserBack': ('BrowserBack', 166),
'BrowserForward': ('BrowserForward', 167),
'BrowserRefresh': ('BrowserRefresh', 168),
'BrowserStop': ('BrowserStop', 169),
'BrowserSearch': ('BrowserSearch', 170),
'BrowserFavorites': ('BrowserFavorites', 171),
'BrowserHome': ('BrowserHome', 172),
# Additional common keys
'Clear': ('Clear', 12),
'Pause': ('Pause', 19),
'Select': ('Select', 41),
'Print': ('Print', 42),
'Execute': ('Execute', 43),
'PrintScreen': ('PrintScreen', 44),
'Help': ('Help', 47),
'ContextMenu': ('ContextMenu', 93),
}
if key in key_map:
return key_map[key]
# Handle alphanumeric keys dynamically
if len(key) == 1:
if key.isalpha():
# Letter keys: A-Z have VK codes 65-90
return (f'Key{key.upper()}', ord(key.upper()))
elif key.isdigit():
# Digit keys: 0-9 have VK codes 48-57 (same as ASCII)
return (f'Digit{key}', ord(key))
# Fallback: use the key name as code, no virtual key code
return (key, None)
# Backward compatibility: provide standalone function
def get_key_info(key: str) -> tuple[str, int | None]:
"""Get the code and windowsVirtualKeyCode for a key.
Args:
key: Key name (e.g., 'Enter', 'ArrowUp', 'a', 'A')
Returns:
Tuple of (code, windowsVirtualKeyCode)
Reference: Windows Virtual Key Codes
https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
"""
return Utils.get_key_info(key)