Skip to content

Commit 709c0d1

Browse files
Merge branch 'main' into miguel/bb-928-python-sdk-ci
2 parents 2aa6e58 + bd85a29 commit 709c0d1

File tree

16 files changed

+1029
-116
lines changed

16 files changed

+1029
-116
lines changed

MANIFEST.in

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
include README.md
2+
include requirements.txt
3+
global-exclude *.pyc
4+
global-exclude __pycache__
5+
global-exclude .DS_Store
6+
global-exclude */node_modules/*

README.md

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,56 @@ export STAGEHAND_SERVER_URL="url-of-stagehand-server"
116116

117117
## Quickstart
118118

119-
Below is a minimal example to get started with Stagehand using the new schema-based options:
119+
Stagehand supports both synchronous and asynchronous usage. Here are examples for both approaches:
120+
121+
### Synchronous Usage
122+
123+
```python
124+
import os
125+
from stagehand.sync.client import Stagehand
126+
from stagehand.schemas import ActOptions, ExtractOptions
127+
from pydantic import BaseModel
128+
from dotenv import load_dotenv
129+
130+
load_dotenv()
131+
132+
class DescriptionSchema(BaseModel):
133+
description: str
134+
135+
def main():
136+
# Create a Stagehand client - it will automatically create a new session if needed
137+
stagehand = Stagehand(
138+
model_name="gpt-4", # Optional: defaults are available from the server
139+
)
140+
141+
# Initialize Stagehand and create a new session
142+
stagehand.init()
143+
print(f"Created new session: {stagehand.session_id}")
144+
145+
# Navigate to a webpage using local Playwright controls
146+
stagehand.page.goto("https://www.example.com")
147+
print("Navigation complete.")
148+
149+
# Perform an action using the AI (e.g. simulate a button click)
150+
stagehand.page.act("click on the 'Quickstart' button")
151+
152+
# Extract data from the page with schema validation
153+
data = stagehand.page.extract(
154+
ExtractOptions(
155+
instruction="extract the description of the page",
156+
schemaDefinition=DescriptionSchema.model_json_schema()
157+
)
158+
)
159+
description = data.get("description") if isinstance(data, dict) else data.description
160+
print("Extracted description:", description)
161+
162+
stagehand.close()
163+
164+
if __name__ == "__main__":
165+
main()
166+
```
167+
168+
### Asynchronous Usage
120169

121170
```python
122171
import asyncio
@@ -146,7 +195,7 @@ async def main():
146195
print("Navigation complete.")
147196

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

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

167-
168-
## Running Evaluations
216+
## Evals
169217

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

172-
173-
```bash
174-
python evals/run_all_evals.py
175-
```
220+
`python evals/run_all_evals.py`
176221

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

179-
180224
## More Examples
181225

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

201247
Example using a unified configuration:
202248

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

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

226274
## Requirements
227275

228276
- Python 3.7+
229-
- httpx
230-
- asyncio
277+
- httpx (for async client)
278+
- requests (for sync client)
279+
- asyncio (for async client)
231280
- pydantic
232281
- python-dotenv (optional, for .env support)
233282
- playwright
234283

284+
## Contributing
285+
286+
### Running Tests
287+
288+
The project uses pytest for testing. To run the tests:
289+
290+
```bash
291+
# Install development dependencies
292+
pip install -r requirements-dev.txt
293+
294+
chmod +x run_tests.sh && ./run_tests.sh
295+
```
296+
235297
## License
236298

237299
MIT License (c) 2025 Browserbase, Inc.

examples/example.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ async def main():
7979

8080
console.print("\n▶️ [highlight] Performing action:[/] search for openai")
8181
await page.act("search for openai")
82+
await page.keyboard.press("Enter")
8283
console.print("✅ [success]Performing Action:[/] Action completed successfully")
8384

8485
console.print("\n▶️ [highlight] Observing page[/] for news button")

examples/example_sync.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import logging
2+
import os
3+
4+
from dotenv import load_dotenv
5+
from rich.console import Console
6+
from rich.panel import Panel
7+
from rich.theme import Theme
8+
9+
from stagehand.sync import Stagehand
10+
from stagehand.config import StagehandConfig
11+
12+
# Create a custom theme for consistent styling
13+
custom_theme = Theme(
14+
{
15+
"info": "cyan",
16+
"success": "green",
17+
"warning": "yellow",
18+
"error": "red bold",
19+
"highlight": "magenta",
20+
"url": "blue underline",
21+
}
22+
)
23+
24+
# Create a Rich console instance with our theme
25+
console = Console(theme=custom_theme)
26+
27+
load_dotenv()
28+
29+
# Configure logging with Rich handler
30+
logging.basicConfig(
31+
level=logging.WARNING, # Feel free to change this to INFO or DEBUG to see more logs
32+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
33+
datefmt="%Y-%m-%d %H:%M:%S",
34+
)
35+
36+
37+
def main():
38+
# Build a unified configuration object for Stagehand
39+
config = StagehandConfig(
40+
env="BROWSERBASE",
41+
api_key=os.getenv("BROWSERBASE_API_KEY"),
42+
project_id=os.getenv("BROWSERBASE_PROJECT_ID"),
43+
headless=False,
44+
dom_settle_timeout_ms=3000,
45+
model_name="gpt-4o",
46+
model_client_options={"apiKey": os.getenv("MODEL_API_KEY")},
47+
)
48+
49+
# Create a Stagehand client using the configuration object.
50+
stagehand = Stagehand(
51+
config=config, server_url=os.getenv("STAGEHAND_SERVER_URL"), verbose=2
52+
)
53+
54+
# Initialize - this creates a new session automatically.
55+
console.print("\n🚀 [info]Initializing Stagehand...[/]")
56+
stagehand.init()
57+
console.print(f"\n[yellow]Created new session:[/] {stagehand.session_id}")
58+
console.print(
59+
f"🌐 [white]View your live browser:[/] [url]https://www.browserbase.com/sessions/{stagehand.session_id}[/]"
60+
)
61+
62+
import time
63+
time.sleep(2)
64+
65+
console.print("\n▶️ [highlight] Navigating[/] to Google")
66+
stagehand.page.goto("https://google.com/")
67+
console.print("✅ [success]Navigated to Google[/]")
68+
69+
console.print("\n▶️ [highlight] Clicking[/] on About link")
70+
# Click on the "About" link using Playwright
71+
stagehand.page.get_by_role("link", name="About", exact=True).click()
72+
console.print("✅ [success]Clicked on About link[/]")
73+
74+
time.sleep(2)
75+
console.print("\n▶️ [highlight] Navigating[/] back to Google")
76+
stagehand.page.goto("https://google.com/")
77+
console.print("✅ [success]Navigated back to Google[/]")
78+
79+
console.print("\n▶️ [highlight] Performing action:[/] search for openai")
80+
stagehand.page.act("search for openai")
81+
stagehand.page.keyboard.press("Enter")
82+
console.print("✅ [success]Performing Action:[/] Action completed successfully")
83+
84+
console.print("\n▶️ [highlight] Observing page[/] for news button")
85+
observed = stagehand.page.observe("find the news button on the page")
86+
if len(observed) > 0:
87+
element = observed[0]
88+
console.print("✅ [success]Found element:[/] News button")
89+
stagehand.page.act(element)
90+
else:
91+
console.print("❌ [error]No element found[/]")
92+
93+
console.print("\n▶️ [highlight] Extracting[/] first search result")
94+
data = stagehand.page.extract("extract the first result from the search")
95+
console.print("📊 [info]Extracted data:[/]")
96+
console.print_json(f"{data.model_dump_json()}")
97+
98+
# Close the session
99+
console.print("\n⏹️ [warning]Closing session...[/]")
100+
stagehand.close()
101+
console.print("✅ [success]Session closed successfully![/]")
102+
console.rule("[bold]End of Example[/]")
103+
104+
105+
if __name__ == "__main__":
106+
# Add a fancy header
107+
console.print(
108+
"\n",
109+
Panel.fit(
110+
"[light_gray]Stagehand 🤘 Python Sync Example[/]",
111+
border_style="green",
112+
padding=(1, 10),
113+
),
114+
)
115+
main()

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ asyncio>=3.4.3
33
python-dotenv>=1.0.0
44
pydantic>=1.10.0
55
playwright>=1.42.1
6+
requests>=2.31.0
67
rich

stagehand/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
from .client import Stagehand
2+
from .config import StagehandConfig
3+
from .page import StagehandPage
4+
5+
6+
__version__ = "0.2.2"
7+
8+
__all__ = ["Stagehand", "StagehandConfig", "StagehandPage"]
29

3-
__version__ = "0.3.0"
4-
__all__ = ["Stagehand"]

stagehand/base.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Any, Callable, Dict, Optional, Union
3+
from playwright.async_api import Page
4+
5+
from .config import StagehandConfig
6+
from .page import StagehandPage
7+
from .utils import default_log_handler
8+
import os
9+
import time
10+
import logging
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class StagehandBase(ABC):
16+
"""
17+
Base class for Stagehand client implementations.
18+
Defines the common interface and functionality for both sync and async versions.
19+
"""
20+
def __init__(
21+
self,
22+
config: Optional[StagehandConfig] = None,
23+
server_url: Optional[str] = None,
24+
session_id: Optional[str] = None,
25+
browserbase_api_key: Optional[str] = None,
26+
browserbase_project_id: Optional[str] = None,
27+
model_api_key: Optional[str] = None,
28+
on_log: Optional[Callable[[Dict[str, Any]], Any]] = default_log_handler,
29+
verbose: int = 1,
30+
model_name: Optional[str] = None,
31+
dom_settle_timeout_ms: Optional[int] = None,
32+
debug_dom: Optional[bool] = None,
33+
timeout_settings: Optional[float] = None,
34+
stream_response: Optional[bool] = None,
35+
model_client_options: Optional[Dict[str, Any]] = None,
36+
):
37+
"""
38+
Initialize the Stagehand client with common configuration.
39+
"""
40+
self.server_url = server_url or os.getenv("STAGEHAND_SERVER_URL")
41+
42+
if config:
43+
self.browserbase_api_key = config.api_key or browserbase_api_key or os.getenv("BROWSERBASE_API_KEY")
44+
self.browserbase_project_id = config.project_id or browserbase_project_id or os.getenv("BROWSERBASE_PROJECT_ID")
45+
self.session_id = config.browserbase_session_id or session_id
46+
self.model_name = config.model_name or model_name
47+
self.dom_settle_timeout_ms = config.dom_settle_timeout_ms or dom_settle_timeout_ms
48+
self.debug_dom = config.debug_dom if config.debug_dom is not None else debug_dom
49+
else:
50+
self.browserbase_api_key = browserbase_api_key or os.getenv("BROWSERBASE_API_KEY")
51+
self.browserbase_project_id = browserbase_project_id or os.getenv("BROWSERBASE_PROJECT_ID")
52+
self.session_id = session_id
53+
self.model_name = model_name
54+
self.dom_settle_timeout_ms = dom_settle_timeout_ms
55+
self.debug_dom = debug_dom
56+
57+
# Handle model-related settings directly
58+
self.model_api_key = model_api_key or os.getenv("MODEL_API_KEY")
59+
self.model_client_options = model_client_options or {}
60+
if self.model_api_key and "apiKey" not in self.model_client_options:
61+
self.model_client_options["apiKey"] = self.model_api_key
62+
63+
# Handle streaming response setting directly
64+
self.streamed_response = stream_response if stream_response is not None else True
65+
66+
self.on_log = on_log
67+
self.verbose = verbose
68+
self.timeout_settings = timeout_settings or 180.0
69+
70+
self._initialized = False
71+
self._closed = False
72+
self.page: Optional[StagehandPage] = None
73+
74+
# Validate essential fields if session_id was provided
75+
if self.session_id:
76+
if not self.browserbase_api_key:
77+
raise ValueError("browserbase_api_key is required (or set BROWSERBASE_API_KEY in env).")
78+
if not self.browserbase_project_id:
79+
raise ValueError("browserbase_project_id is required (or set BROWSERBASE_PROJECT_ID in env).")
80+
81+
@abstractmethod
82+
def init(self):
83+
"""
84+
Initialize the Stagehand client.
85+
Must be implemented by subclasses.
86+
"""
87+
pass
88+
89+
@abstractmethod
90+
def close(self):
91+
"""
92+
Clean up resources.
93+
Must be implemented by subclasses.
94+
"""
95+
pass
96+
97+
def _log(self, message: str, level: int = 1):
98+
"""
99+
Internal logging helper that maps verbosity to logging levels.
100+
"""
101+
if self.verbose >= level:
102+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
103+
formatted_msg = f"{timestamp}::[stagehand] {message}"
104+
if level == 1:
105+
logger.info(formatted_msg)
106+
elif level == 2:
107+
logger.warning(formatted_msg)
108+
else:
109+
logger.debug(formatted_msg)

0 commit comments

Comments
 (0)