Skip to content

Commit 9eb9428

Browse files
authored
Merge pull request #48 from stainless-sdks/STG-1306
STG-1306: README + examples updates
2 parents 1399f06 + f3cf476 commit 9eb9428

11 files changed

Lines changed: 122 additions & 162 deletions

README.md

Lines changed: 108 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<!-- x-stagehand-custom-start -->
12
<div id="toc" align="center" style="margin-bottom: 0;">
23
<ul style="list-style: none; margin: 0; padding: 0;">
34
<a href="https://stagehand.dev">
@@ -50,6 +51,7 @@ If you're looking for other languages, you can find them
5051
<img alt="Director" src="https://raw.githubusercontent.com/browserbase/stagehand/main/media/director_icon.svg" width="25" />
5152
</picture>
5253
</div>
54+
<!-- x-stagehand-custom-end -->
5355

5456
> [!TIP]
5557
> Migrating from the old v2 Python SDK? See our [migration guide here](https://docs.stagehand.dev/v3/migrations/python).
@@ -83,9 +85,12 @@ Python 3.9 or higher.
8385

8486
## Running the Example
8587

86-
A complete working example is available at [`examples/full_example.py`](examples/full_example.py).
88+
Set your environment variables (from `examples/.env.example`):
8789

88-
Before running examples, set up the example environment file:
90+
- `STAGEHAND_API_URL`
91+
- `MODEL_API_KEY`
92+
- `BROWSERBASE_API_KEY`
93+
- `BROWSERBASE_PROJECT_ID`
8994

9095
```bash
9196
cp examples/.env.example examples/.env
@@ -94,165 +99,120 @@ cp examples/.env.example examples/.env
9499

95100
The examples load `examples/.env` automatically.
96101

97-
To run it, first export the required environment variables, then use Python:
102+
Examples and dependencies:
98103

99-
```bash
100-
export BROWSERBASE_API_KEY="your-bb-api-key"
101-
export BROWSERBASE_PROJECT_ID="your-bb-project-uuid"
102-
export MODEL_API_KEY="sk-proj-your-llm-api-key"
103-
104-
uv run python examples/full_example.py
105-
```
106-
107-
## Local mode example
108-
109-
If you want to run Stagehand locally, use the local example (`examples/local_example.py`). It shows how to configure the client for `server="local"`.
110-
111-
Local mode runs Stagehand’s embedded server and launches a **local Chrome/Chromium** browser (it is **not bundled** with the Python wheel), so you must have Chrome installed on the machine running the example.
112-
113-
If Chrome is installed but Stagehand can’t find it, set `CHROME_PATH` to your browser executable (or pass `browser.launchOptions.executablePath` when starting the session).
104+
- `examples/full_example.py`: stagehand only
105+
- `examples/act_example.py`: stagehand only
106+
- `examples/agent_execute.py`: stagehand only
107+
- `examples/local_example.py`: stagehand only
108+
- `examples/logging_example.py`: stagehand only
109+
- `examples/remote_browser_playwright_example.py`: Playwright + Playwright browsers
110+
- `examples/local_browser_playwright_example.py`: Playwright + Playwright browsers
111+
- `examples/playwright_page_example.py`: Playwright + Playwright browsers
112+
- `examples/byob_example.py`: Playwright + Playwright browsers
113+
- `examples/pydoll_tab_example.py`: `pydoll-python` (Python 3.10+)
114114

115-
Common Windows paths:
116-
- `C:\Program Files\Google\Chrome\Application\chrome.exe`
117-
- `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
118-
119-
PowerShell:
120-
121-
```powershell
122-
# optional if you don't already have Chrome installed
123-
winget install -e --id Google.Chrome
124-
125-
# optional if Stagehand can't auto-detect Chrome
126-
$env:CHROME_PATH="C:\Program Files\Google\Chrome\Application\chrome.exe"
127-
128-
uv run python examples/local_example.py
129-
```
130-
131-
```bash
132-
pip install stagehand
133-
uv run python examples/local_example.py
134-
```
135-
136-
## Streaming logging example
137-
138-
See [`examples/logging_example.py`](examples/logging_example.py) for a remote-only flow that streams `StreamEvent`s with `verbose=2`, `stream_response=True`, and `x_stream_response="true"` so you can watch the SDK’s logs as they arrive.
139-
140-
```bash
141-
uv run python examples/logging_example.py
142-
```
143-
144-
## Remote Playwright (SSE) example
145-
146-
See [`examples/remote_browser_playwright_example.py`](examples/remote_browser_playwright_example.py) for a remote Browserbase flow that attaches Playwright via CDP and streams SSE events for observe/act/extract/execute.
115+
Run any example:
147116

148117
```bash
149118
uv run python examples/remote_browser_playwright_example.py
150119
```
151120

152-
## Local Playwright (SSE) example
153-
154-
See [`examples/local_browser_playwright_example.py`](examples/local_browser_playwright_example.py) for a local Stagehand flow that launches Playwright locally, shares its CDP URL with Stagehand, and streams SSE events for observe/act/extract/execute.
155-
156-
```bash
157-
uv run python examples/local_browser_playwright_example.py
158-
```
159-
160-
<details>
161-
<summary><strong>Local development</strong></summary>
162-
163-
This repository relies on `uv` to install the sanctioned Python version and dependencies. After cloning, bootstrap the environment with:
164-
165-
```sh
166-
./scripts/bootstrap
167-
```
168-
Once the environment is ready, execute repo scripts with `uv run`:
169-
170-
```sh
171-
uv run python examples/full_example.py
172-
```
173-
</details>
174-
175121
## Usage
176122

177-
This example demonstrates the full Stagehand workflow: starting a session, navigating to a page, observing possible actions, acting on elements, extracting data, and running an autonomous agent.
123+
This mirrors `examples/remote_browser_playwright_example.py`.
178124

179125
```python
180-
import asyncio
181-
182-
from stagehand import AsyncStagehand
183-
126+
import os
184127

185-
async def main() -> None:
186-
# Create client using environment variables:
187-
# BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, MODEL_API_KEY
188-
client = AsyncStagehand()
128+
from playwright.sync_api import sync_playwright
189129

190-
# Start a new browser session (returns a session helper bound to a session_id)
191-
session = await client.sessions.start(model_name="openai/gpt-5-nano")
130+
from env import load_example_env
131+
from stagehand import Stagehand
192132

193-
print(f"Session started: {session.id}")
194133

195-
try:
196-
# Navigate to a webpage
197-
await session.navigate(
198-
url="https://news.ycombinator.com",
199-
)
200-
print("Navigated to Hacker News")
134+
def main() -> None:
135+
load_example_env()
201136

202-
# Observe to find possible actions on the page
203-
observe_response = await session.observe(
204-
instruction="find the link to view comments for the top post",
137+
with Stagehand(
138+
server="remote",
139+
browserbase_api_key=os.environ.get("BROWSERBASE_API_KEY"),
140+
browserbase_project_id=os.environ.get("BROWSERBASE_PROJECT_ID"),
141+
model_api_key=os.environ.get("MODEL_API_KEY"),
142+
) as client:
143+
session = client.sessions.start(
144+
model_name="anthropic/claude-sonnet-4-6",
145+
browser={"type": "browserbase"},
205146
)
206147

207-
results = observe_response.data.result
208-
print(f"Found {len(results)} possible actions")
209-
if not results:
210-
return
211-
212-
# Take the first action returned by Observe and pass it to Act
213-
action = results[0].to_dict(exclude_none=True)
214-
print("Acting on:", action.get("description"))
215-
216-
act_response = await session.act(input=action)
217-
print("Act completed:", act_response.data.result.message)
218-
219-
# Extract structured data from the page using a JSON schema
220-
extract_response = await session.extract(
221-
instruction="extract the text of the top comment on this page",
222-
schema={
223-
"type": "object",
224-
"properties": {
225-
"commentText": {"type": "string"},
226-
"author": {"type": "string"},
148+
cdp_url = session.data.cdp_url
149+
if not cdp_url:
150+
raise RuntimeError("No cdp_url returned from the API for this session.")
151+
152+
with sync_playwright() as p:
153+
browser = p.chromium.connect_over_cdp(cdp_url)
154+
context = browser.contexts[0] if browser.contexts else browser.new_context()
155+
page = context.pages[0] if context.pages else context.new_page()
156+
157+
client.sessions.navigate(session.id, url="https://news.ycombinator.com")
158+
page.wait_for_load_state("domcontentloaded")
159+
160+
observe_stream = client.sessions.observe(
161+
session.id,
162+
instruction="find the link to view comments for the top post",
163+
stream_response=True,
164+
x_stream_response="true",
165+
)
166+
for _ in observe_stream:
167+
pass
168+
169+
act_stream = client.sessions.act(
170+
session.id,
171+
input="Click the comments link for the top post",
172+
stream_response=True,
173+
x_stream_response="true",
174+
)
175+
for _ in act_stream:
176+
pass
177+
178+
extract_stream = client.sessions.extract(
179+
session.id,
180+
instruction="extract the text of the top comment on this page",
181+
schema={
182+
"type": "object",
183+
"properties": {
184+
"commentText": {"type": "string"},
185+
"author": {"type": "string"},
186+
},
187+
"required": ["commentText"],
227188
},
228-
"required": ["commentText"],
229-
},
230-
)
231-
232-
extracted = extract_response.data.result
233-
author = extracted.get("author", "unknown") if isinstance(extracted, dict) else "unknown"
234-
print("Extracted author:", author)
235-
236-
# Run an autonomous agent to accomplish a complex task
237-
execute_response = await session.execute(
238-
execute_options={
239-
"instruction": f"Find any personal website, GitHub, or LinkedIn profile for the Hacker News user '{author}'.",
240-
"max_steps": 10,
241-
},
242-
agent_config={"model": "openai/gpt-5-nano"},
243-
timeout=300.0,
244-
)
189+
stream_response=True,
190+
x_stream_response="true",
191+
)
192+
for _ in extract_stream:
193+
pass
194+
195+
execute_stream = client.sessions.execute(
196+
session.id,
197+
execute_options={
198+
"instruction": "Click the 'Learn more' link if available",
199+
"max_steps": 3,
200+
},
201+
agent_config={
202+
"model": {"model_name": "anthropic/claude-opus-4-6"},
203+
"cua": False,
204+
},
205+
stream_response=True,
206+
x_stream_response="true",
207+
)
208+
for _ in execute_stream:
209+
pass
245210

246-
print("Agent completed:", execute_response.data.result.message)
247-
print("Agent success:", execute_response.data.result.success)
248-
finally:
249-
# End the browser session to clean up resources
250-
await session.end()
251-
print("Session ended")
211+
client.sessions.end(session.id)
252212

253213

254214
if __name__ == "__main__":
255-
asyncio.run(main())
215+
main()
256216
```
257217

258218
## Client configuration
@@ -336,7 +296,7 @@ from stagehand import AsyncStagehand
336296

337297
async def main() -> None:
338298
client = AsyncStagehand()
339-
session = await client.sessions.start(model_name="openai/gpt-5-nano")
299+
session = await client.sessions.start(model_name="anthropic/claude-sonnet-4-6")
340300
response = await session.act(input="click the first link on the page")
341301
print(response.data)
342302

@@ -364,7 +324,7 @@ from stagehand import AsyncStagehand, DefaultAioHttpClient
364324

365325
async def main() -> None:
366326
async with AsyncStagehand(http_client=DefaultAioHttpClient()) as client:
367-
session = await client.sessions.start(model_name="openai/gpt-5-nano")
327+
session = await client.sessions.start(model_name="anthropic/claude-sonnet-4-6")
368328
response = await session.act(input="click the first link on the page")
369329
print(response.data)
370330

@@ -389,7 +349,7 @@ from stagehand import AsyncStagehand
389349

390350
async def main() -> None:
391351
async with AsyncStagehand() as client:
392-
session = await client.sessions.start(model_name="openai/gpt-5-nano")
352+
session = await client.sessions.start(model_name="anthropic/claude-sonnet-4-6")
393353

394354
stream = await client.sessions.act(
395355
id=session.id,
@@ -419,7 +379,7 @@ from stagehand import AsyncStagehand
419379

420380
async def main() -> None:
421381
async with AsyncStagehand() as client:
422-
response = await client.sessions.with_raw_response.start(model_name="openai/gpt-5-nano")
382+
response = await client.sessions.with_raw_response.start(model_name="anthropic/claude-sonnet-4-6")
423383
print(response.headers.get("X-My-Header"))
424384

425385
session = response.parse() # get the object that `sessions.start()` would have returned
@@ -443,7 +403,7 @@ from stagehand import AsyncStagehand
443403

444404
async def main() -> None:
445405
async with AsyncStagehand() as client:
446-
async with client.sessions.with_streaming_response.start(model_name="openai/gpt-5-nano") as response:
406+
async with client.sessions.with_streaming_response.start(model_name="anthropic/claude-sonnet-4-6") as response:
447407
print(response.headers.get("X-My-Header"))
448408
async for line in response.iter_lines():
449409
print(line)
@@ -470,7 +430,7 @@ from stagehand import AsyncStagehand
470430
async def main() -> None:
471431
async with AsyncStagehand() as client:
472432
try:
473-
await client.sessions.start(model_name="openai/gpt-5-nano")
433+
await client.sessions.start(model_name="anthropic/claude-sonnet-4-6")
474434
except stagehand.APIConnectionError as e:
475435
print("The server could not be reached")
476436
print(e.__cause__) # an underlying Exception, likely raised within httpx.
@@ -513,7 +473,7 @@ from stagehand import AsyncStagehand
513473
async def main() -> None:
514474
async with AsyncStagehand(max_retries=0) as client:
515475
# Or, configure per-request:
516-
await client.with_options(max_retries=5).sessions.start(model_name="openai/gpt-5-nano")
476+
await client.with_options(max_retries=5).sessions.start(model_name="anthropic/claude-sonnet-4-6")
517477

518478

519479
asyncio.run(main())
@@ -589,7 +549,7 @@ from stagehand import APIResponseValidationError, AsyncStagehand
589549
try:
590550
async def main() -> None:
591551
async with AsyncStagehand(_strict_response_validation=True) as client:
592-
await client.sessions.start(model_name="openai/gpt-5-nano")
552+
await client.sessions.start(model_name="anthropic/claude-sonnet-4-6")
593553

594554
asyncio.run(main())
595555
except APIResponseValidationError as e:
@@ -623,7 +583,7 @@ from stagehand import Stagehand
623583

624584
client = Stagehand()
625585
response = client.sessions.with_raw_response.start(
626-
model_name="openai/gpt-5-nano",
586+
model_name="anthropic/claude-sonnet-4-6",
627587
)
628588
print(response.headers.get('X-My-Header'))
629589

@@ -643,7 +603,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi
643603

644604
```python
645605
with client.sessions.with_streaming_response.start(
646-
model_name="openai/gpt-5-nano",
606+
model_name="anthropic/claude-sonnet-4-6",
647607
) as response:
648608
print(response.headers.get("X-My-Header"))
649609

examples/act_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ async def main() -> None:
5353
) as client:
5454
# Start a new browser session
5555
session = await client.sessions.start(
56-
model_name="openai/gpt-5-nano",
56+
model_name="anthropic/claude-sonnet-4-6",
5757
)
5858

5959
print(f"Session started: {session.id}")

0 commit comments

Comments
 (0)