Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 74 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,56 @@ export STAGEHAND_SERVER_URL="url-of-stagehand-server"

## Quickstart

Below is a minimal example to get started with Stagehand using the new schema-based options:
Stagehand supports both synchronous and asynchronous usage. Here are examples for both approaches:

### Synchronous Usage

```python
import os
from stagehand.sync.client import Stagehand
from stagehand.schemas import ActOptions, ExtractOptions
from pydantic import BaseModel
from dotenv import load_dotenv

load_dotenv()

class DescriptionSchema(BaseModel):
description: str

def main():
# Create a Stagehand client - it will automatically create a new session if needed
stagehand = Stagehand(
model_name="gpt-4", # Optional: defaults are available from the server
)

# Initialize Stagehand and create a new session
stagehand.init()
print(f"Created new session: {stagehand.session_id}")

# Navigate to a webpage using local Playwright controls
stagehand.page.goto("https://www.example.com")
print("Navigation complete.")

# Perform an action using the AI (e.g. simulate a button click)
stagehand.page.act("click on the 'Quickstart' button")

# Extract data from the page with schema validation
data = stagehand.page.extract(
ExtractOptions(
instruction="extract the description of the page",
schemaDefinition=DescriptionSchema.model_json_schema()
)
)
description = data.get("description") if isinstance(data, dict) else data.description
print("Extracted description:", description)

stagehand.close()

if __name__ == "__main__":
main()
```

### Asynchronous Usage

```python
import asyncio
Expand Down Expand Up @@ -146,7 +195,7 @@ async def main():
print("Navigation complete.")

# Perform an action using the AI (e.g. simulate a button click)
await stagehand.page.act(ActOptions(action="click on the 'Quickstart' button"))
await stagehand.page.act("click on the 'Quickstart' button")

# Extract data from the page with schema validation
data = await stagehand.page.extract(
Expand All @@ -164,19 +213,14 @@ if __name__ == "__main__":
asyncio.run(main())
```


## Running Evaluations
## Evals

To test all evaluations, run the following command in your terminal:


```bash
python evals/run_all_evals.py
```
`python evals/run_all_evals.py`

This script will dynamically discover and execute every evaluation module within the `evals` directory and print the results for each.


## More Examples

For further examples, check out the scripts in the `examples/` directory:
Expand All @@ -197,6 +241,8 @@ Stagehand can be configured via environment variables or through a `StagehandCon
- `model_name`: Optional model name for the AI.
- `dom_settle_timeout_ms`: Additional time (in ms) to have the DOM settle.
- `debug_dom`: Enable debug mode for DOM operations.
- `stream_response`: Whether to stream responses from the server (default: True).
- `timeout_settings`: Custom timeout settings for HTTP requests.

Example using a unified configuration:

Expand All @@ -220,18 +266,34 @@ config = StagehandConfig(

- **AI-powered Browser Control**: Execute natural language instructions over a running browser.
- **Validated Data Extraction**: Use JSON schemas (or Pydantic models) to extract and validate information from pages.
- **Async/Await Support**: Built using Python's asyncio, making it easy to build scalable web automation workflows.
- **Async/Sync Support**: Choose between asynchronous and synchronous APIs based on your needs.
- **Context Manager Support**: Automatic resource cleanup with async and sync context managers.
- **Extensible**: Seamlessly extend Playwright functionality with AI enrichments.
- **Streaming Support**: Sreaming responses for better performance with long-running operations. Default True.

## Requirements

- Python 3.7+
- httpx
- asyncio
- httpx (for async client)
- requests (for sync client)
- asyncio (for async client)
- pydantic
- python-dotenv (optional, for .env support)
- playwright

## Contributing

### Running Tests

The project uses pytest for testing. To run the tests:

```bash
# Install development dependencies
pip install -r requirements-dev.txt

chmod +x run_tests.sh && ./run_tests.sh
```

## License

MIT License (c) 2025 Browserbase, Inc.
115 changes: 115 additions & 0 deletions examples/example_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import logging
import os

from dotenv import load_dotenv
from rich.console import Console
from rich.panel import Panel
from rich.theme import Theme

from stagehand.sync import Stagehand
from stagehand.config import StagehandConfig

# Create a custom theme for consistent styling
custom_theme = Theme(
{
"info": "cyan",
"success": "green",
"warning": "yellow",
"error": "red bold",
"highlight": "magenta",
"url": "blue underline",
}
)

# Create a Rich console instance with our theme
console = Console(theme=custom_theme)

load_dotenv()

# Configure logging with Rich handler
logging.basicConfig(
level=logging.WARNING, # Feel free to change this to INFO or DEBUG to see more logs
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)


def main():
# Build a unified configuration object for Stagehand
config = StagehandConfig(
env="BROWSERBASE",
api_key=os.getenv("BROWSERBASE_API_KEY"),
project_id=os.getenv("BROWSERBASE_PROJECT_ID"),
headless=False,
dom_settle_timeout_ms=3000,
model_name="gpt-4o",
model_client_options={"apiKey": os.getenv("MODEL_API_KEY")},
)

# Create a Stagehand client using the configuration object.
stagehand = Stagehand(
config=config, server_url=os.getenv("STAGEHAND_SERVER_URL"), verbose=2
)

# Initialize - this creates a new session automatically.
console.print("\n🚀 [info]Initializing Stagehand...[/]")
stagehand.init()
console.print(f"\n[yellow]Created new session:[/] {stagehand.session_id}")
console.print(
f"🌐 [white]View your live browser:[/] [url]https://www.browserbase.com/sessions/{stagehand.session_id}[/]"
)

import time
time.sleep(2)

console.print("\n▶️ [highlight] Navigating[/] to Google")
stagehand.page.goto("https://google.com/")
console.print("✅ [success]Navigated to Google[/]")

console.print("\n▶️ [highlight] Clicking[/] on About link")
# Click on the "About" link using Playwright
stagehand.page.get_by_role("link", name="About", exact=True).click()
console.print("✅ [success]Clicked on About link[/]")

time.sleep(2)
console.print("\n▶️ [highlight] Navigating[/] back to Google")
stagehand.page.goto("https://google.com/")
console.print("✅ [success]Navigated back to Google[/]")

console.print("\n▶️ [highlight] Performing action:[/] search for openai")
stagehand.page.act("search for openai")
stagehand.page.keyboard.press("Enter")
console.print("✅ [success]Performing Action:[/] Action completed successfully")

console.print("\n▶️ [highlight] Observing page[/] for news button")
observed = stagehand.page.observe("find the news button on the page")
if len(observed) > 0:
element = observed[0]
console.print("✅ [success]Found element:[/] News button")
stagehand.page.act(element)
else:
console.print("❌ [error]No element found[/]")

console.print("\n▶️ [highlight] Extracting[/] first search result")
data = stagehand.page.extract("extract the first result from the search")
console.print("📊 [info]Extracted data:[/]")
console.print_json(f"{data.model_dump_json()}")

# Close the session
console.print("\n⏹️ [warning]Closing session...[/]")
stagehand.close()
console.print("✅ [success]Session closed successfully![/]")
console.rule("[bold]End of Example[/]")


if __name__ == "__main__":
# Add a fancy header
console.print(
"\n",
Panel.fit(
"[light_gray]Stagehand 🤘 Python Sync Example[/]",
border_style="green",
padding=(1, 10),
),
)
main()
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ asyncio>=3.4.3
python-dotenv>=1.0.0
pydantic>=1.10.0
playwright>=1.42.1
requests>=2.31.0
rich
7 changes: 5 additions & 2 deletions stagehand/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from .client import Stagehand
from .config import StagehandConfig
from .page import StagehandPage

__version__ = "0.1.0"
__all__ = ["Stagehand"]
__version__ = "0.2.2"

__all__ = ["Stagehand", "StagehandConfig", "StagehandPage"]
109 changes: 109 additions & 0 deletions stagehand/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from abc import ABC, abstractmethod
from typing import Any, Callable, Dict, Optional, Union
from playwright.async_api import Page

from .config import StagehandConfig
from .page import StagehandPage
from .utils import default_log_handler
import os
import time
import logging

logger = logging.getLogger(__name__)


class StagehandBase(ABC):
"""
Base class for Stagehand client implementations.
Defines the common interface and functionality for both sync and async versions.
"""
def __init__(
self,
config: Optional[StagehandConfig] = None,
server_url: Optional[str] = None,
session_id: Optional[str] = None,
browserbase_api_key: Optional[str] = None,
browserbase_project_id: Optional[str] = None,
model_api_key: Optional[str] = None,
on_log: Optional[Callable[[Dict[str, Any]], Any]] = default_log_handler,
verbose: int = 1,
model_name: Optional[str] = None,
dom_settle_timeout_ms: Optional[int] = None,
debug_dom: Optional[bool] = None,
timeout_settings: Optional[float] = None,
stream_response: Optional[bool] = None,
model_client_options: Optional[Dict[str, Any]] = None,
):
"""
Initialize the Stagehand client with common configuration.
"""
self.server_url = server_url or os.getenv("STAGEHAND_SERVER_URL")

if config:
self.browserbase_api_key = config.api_key or browserbase_api_key or os.getenv("BROWSERBASE_API_KEY")
self.browserbase_project_id = config.project_id or browserbase_project_id or os.getenv("BROWSERBASE_PROJECT_ID")
self.session_id = config.browserbase_session_id or session_id
self.model_name = config.model_name or model_name
self.dom_settle_timeout_ms = config.dom_settle_timeout_ms or dom_settle_timeout_ms
self.debug_dom = config.debug_dom if config.debug_dom is not None else debug_dom
else:
self.browserbase_api_key = browserbase_api_key or os.getenv("BROWSERBASE_API_KEY")
self.browserbase_project_id = browserbase_project_id or os.getenv("BROWSERBASE_PROJECT_ID")
self.session_id = session_id
self.model_name = model_name
self.dom_settle_timeout_ms = dom_settle_timeout_ms
self.debug_dom = debug_dom

# Handle model-related settings directly
self.model_api_key = model_api_key or os.getenv("MODEL_API_KEY")
self.model_client_options = model_client_options or {}
if self.model_api_key and "apiKey" not in self.model_client_options:
self.model_client_options["apiKey"] = self.model_api_key

# Handle streaming response setting directly
self.streamed_response = stream_response if stream_response is not None else True

self.on_log = on_log
self.verbose = verbose
self.timeout_settings = timeout_settings or 180.0

self._initialized = False
self._closed = False
self.page: Optional[StagehandPage] = None

# Validate essential fields if session_id was provided
if self.session_id:
if not self.browserbase_api_key:
raise ValueError("browserbase_api_key is required (or set BROWSERBASE_API_KEY in env).")
if not self.browserbase_project_id:
raise ValueError("browserbase_project_id is required (or set BROWSERBASE_PROJECT_ID in env).")

@abstractmethod
def init(self):
"""
Initialize the Stagehand client.
Must be implemented by subclasses.
"""
pass

@abstractmethod
def close(self):
"""
Clean up resources.
Must be implemented by subclasses.
"""
pass

def _log(self, message: str, level: int = 1):
"""
Internal logging helper that maps verbosity to logging levels.
"""
if self.verbose >= level:
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
formatted_msg = f"{timestamp}::[stagehand] {message}"
if level == 1:
logger.info(formatted_msg)
elif level == 2:
logger.warning(formatted_msg)
else:
logger.debug(formatted_msg)
Loading