Skip to content

Commit a3070bd

Browse files
Update types (#8)
* update examples * update header from server * add type safetu via Pydantic * update examples with pydantic for act/extract/observe * add stagehand config * fix schemas * update model for example
1 parent 4f97163 commit a3070bd

File tree

8 files changed

+267
-169
lines changed

8 files changed

+267
-169
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ Before running your script, make sure you have exported the necessary environmen
9898
```bash
9999
export BROWSERBASE_API_KEY="your-api-key"
100100
export BROWSERBASE_PROJECT_ID="your-project-id"
101-
export OPENAI_API_KEY="your-openai-api-key"
101+
export OPENAI_API_KEY="your-openai-api-key" # or other model
102102
export STAGEHAND_SERVER_URL="url-of-stagehand-server"
103103
```
104104

@@ -158,7 +158,7 @@ For further examples, you can check out the scripts in the “examples/” direc
158158
- `stagehand_server_url`: The Stagehand API server URL
159159
- `browserbase_api_key`: Your BrowserBase API key (can also be set via BROWSERBASE_API_KEY environment variable)
160160
- `browserbase_project_id`: Your BrowserBase project ID (can also be set via BROWSERBASE_PROJECT_ID environment variable)
161-
- `openai_api_key`: Your OpenAI API key (can also be set via OPENAI_API_KEY environment variable)
161+
- `model_api_key`: Your model API key (e.g. OpenAI, Anthropic, etc) (can also be set via MODEL_API_KEY environment variable)
162162
- `verbose`: Verbosity level (default: 1)
163163
- `model_name`: (optional) Model name to use for the conversation
164164
- `dom_settle_timeout_ms`: (optional) Additional time for the DOM to settle

examples/example.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import logging
44
from dotenv import load_dotenv
55
from stagehand.client import Stagehand
6+
from stagehand.config import StagehandConfig
7+
from stagehand.schemas import ActOptions, ObserveOptions
68

79
load_dotenv()
810

@@ -14,21 +16,23 @@
1416
)
1517

1618
async def main():
17-
1819
try:
19-
# Create a Stagehand client - it will create a new session automatically
20-
stagehand = Stagehand(
21-
server_url=os.getenv("SERVER_URL"),
22-
browserbase_api_key=os.getenv("BROWSERBASE_API_KEY"),
23-
browserbase_project_id=os.getenv("BROWSERBASE_PROJECT_ID"),
24-
model_api_key=os.getenv("OPENAI_API_KEY"),
25-
verbose=2,
26-
model_name="gpt-4o", # optional - defaults to server's default
27-
dom_settle_timeout_ms=3000, # optional - defaults to server's default
28-
debug_dom=True, # optional - defaults to server's default
20+
# Build a unified configuration object for Stagehand
21+
config = StagehandConfig(
22+
env="BROWSERBASE" if os.getenv("BROWSERBASE_API_KEY") and os.getenv("BROWSERBASE_PROJECT_ID") else "LOCAL",
23+
api_key=os.getenv("BROWSERBASE_API_KEY"),
24+
project_id=os.getenv("BROWSERBASE_PROJECT_ID"),
25+
debug_dom=True,
26+
headless=False,
27+
dom_settle_timeout_ms=3000,
28+
model_name="gpt-4o-mini",
29+
model_client_options={"apiKey": os.getenv("MODEL_API_KEY")}
2930
)
3031

31-
# Initialize - this will create a new session since we didn't provide session_id
32+
# Create a Stagehand client using the configuration object.
33+
stagehand = Stagehand(config=config, server_url=os.getenv("SERVER_URL"), verbose=2)
34+
35+
# Initialize - this creates a new session automatically.
3236
await stagehand.init()
3337
print(f"Created new session with ID: {stagehand.session_id}")
3438

@@ -40,32 +44,28 @@ async def main():
4044
await stagehand.page.navigate("https://www.google.com")
4145
print("Navigation complete with remote Playwright.")
4246

43-
4447
print("EXAMPLE: Clicking on About link using local Playwright's get_by_role")
4548
# Click on the "About" link using Playwright
4649
await stagehand.page.get_by_role("link", name="About", exact=True).click()
4750
print("Clicked on About link")
4851

4952
await asyncio.sleep(2)
50-
5153
await stagehand.page.navigate("https://www.google.com")
5254

5355
# Hosted Stagehand API - ACT to do something like 'search for openai'
54-
await stagehand.page.act("search for openai")
56+
await stagehand.page.act(ActOptions(action="search for openai"))
5557

5658
print("EXAMPLE: Find the XPATH of the button 'News' using Stagehand API")
57-
xpaths = await stagehand.page.observe("find the button 'News'", only_visible=True)
59+
xpaths = await stagehand.page.observe(ObserveOptions(instruction="find the button labeled 'News'", only_visible=True))
5860
if len(xpaths) > 0:
5961
element = xpaths[0]
6062
print("EXAMPLE: Click on the button 'News' using local Playwright.")
6163
await stagehand.page.click(element["selector"])
6264
else:
6365
print("No element found")
6466

65-
6667
except Exception as e:
6768
print(f"An error occurred in the example: {e}")
6869

69-
7070
if __name__ == "__main__":
7171
asyncio.run(main())

examples/extract-example.py

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import os
33
from dotenv import load_dotenv
44
from stagehand import Stagehand
5+
from stagehand.config import StagehandConfig
6+
from stagehand.schemas import ExtractOptions
57
from pydantic import BaseModel
68

79
class ExtractSchema(BaseModel):
@@ -11,45 +13,34 @@ class ExtractSchema(BaseModel):
1113
load_dotenv()
1214

1315
async def main():
14-
# Create a Stagehand instance with automatic session creation
15-
stagehand = Stagehand(
16-
server_url=os.getenv("STAGEHAND_SERVER_URL"),
17-
browserbase_api_key=os.getenv("BROWSERBASE_API_KEY"),
18-
browserbase_project_id=os.getenv("BROWSERBASE_PROJECT_ID"),
19-
model_api_key=os.getenv("OPENAI_API_KEY"),
20-
verbose=2,
21-
model_name="gpt-4o", # optional - defaults to server's default
22-
debug_dom=True, # optional - defaults to server's default
16+
# Build a unified Stagehand configuration object
17+
config = StagehandConfig(
18+
env="BROWSERBASE" if os.getenv("BROWSERBASE_API_KEY") and os.getenv("BROWSERBASE_PROJECT_ID") else "LOCAL",
19+
api_key=os.getenv("BROWSERBASE_API_KEY"),
20+
project_id=os.getenv("BROWSERBASE_PROJECT_ID"),
21+
debug_dom=True,
22+
headless=True,
23+
model_name="gpt-4o",
24+
model_client_options={"apiKey": os.getenv("MODEL_API_KEY")}
2325
)
2426

25-
# Initialize - this will create a new session
27+
# Create a Stagehand client using the configuration object.
28+
stagehand = Stagehand(config=config, server_url=os.getenv("STAGEHAND_SERVER_URL"), verbose=2)
29+
30+
# Initialize - this creates a new session.
2631
await stagehand.init()
2732
print(f"Created new session with ID: {stagehand.session_id}")
2833

2934
try:
30-
3135
await stagehand.page.navigate("https://github.com/facebook/react")
3236
print("Navigation complete.")
3337

34-
# Define schema for stars extraction
35-
# extract_schema = {
36-
# "type": "object",
37-
# "properties": {
38-
# "stars": {
39-
# "type": "number",
40-
# "description": "the number of stars for the project"
41-
# }
42-
# },
43-
# "required": ["stars"]
44-
# }
45-
46-
# we can either use a pydantic model or a json schema via dict
47-
extract_schema = ExtractSchema
48-
49-
# Extract data using the schema
38+
# Use the ExtractOptions Pydantic model to pass instruction and schema definition
5039
data = await stagehand.page.extract(
51-
instruction="Extract the number of stars for the project",
52-
schema=extract_schema
40+
ExtractOptions(
41+
instruction="Extract the number of stars for the project",
42+
schemaDefinition=ExtractSchema.model_json_schema()
43+
)
5344
)
5445
print("\nExtracted stars:", data)
5546

examples/observe-example.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,51 @@
22
import os
33
from dotenv import load_dotenv
44
from stagehand import Stagehand
5+
from stagehand.config import StagehandConfig
6+
from stagehand.schemas import ObserveOptions
57

68
# Load environment variables from .env file
79
load_dotenv()
810

911
async def main():
10-
# Create a Stagehand instance with automatic session creation
11-
stagehand = Stagehand(
12-
server_url=os.getenv("SERVER_URL"),
13-
browserbase_api_key=os.getenv("BROWSERBASE_API_KEY"),
14-
browserbase_project_id=os.getenv("BROWSERBASE_PROJECT_ID"),
15-
model_api_key=os.getenv("OPENAI_API_KEY"),
16-
verbose=2,
17-
model_name="gpt-4o-mini", # optional - defaults to server's default
18-
debug_dom=True, # optional - defaults to server's default
12+
# Build a unified Stagehand configuration object
13+
config = StagehandConfig(
14+
env="BROWSERBASE" if os.getenv("BROWSERBASE_API_KEY") and os.getenv("BROWSERBASE_PROJECT_ID") else "LOCAL",
15+
api_key=os.getenv("BROWSERBASE_API_KEY"),
16+
project_id=os.getenv("BROWSERBASE_PROJECT_ID"),
17+
debug_dom=True,
18+
headless=True,
19+
model_name="gpt-4o-mini",
20+
model_client_options={"apiKey": os.getenv("MODEL_API_KEY")}
1921
)
2022

21-
# Initialize - this will create a new session
23+
# Create a Stagehand client using the configuration object.
24+
stagehand = Stagehand(config=config, server_url=os.getenv("SERVER_URL"), verbose=2)
25+
26+
# Initialize - this creates a new session.
2227
await stagehand.init()
2328
print(f"Created new session with ID: {stagehand.session_id}")
2429

2530
try:
26-
# Navigate to GitHub repository
31+
# Navigate to the desired page
2732
await stagehand.page.navigate("https://elpasotexas.ionwave.net/Login.aspx")
2833
print("Navigation complete.")
2934

30-
# # Make observations about the site
31-
activity = await stagehand.page.observe(
35+
# Use ObserveOptions for detailed instructions
36+
options = ObserveOptions(
3237
instruction="find all the links on the page regarding the city of el paso",
33-
only_visible=True # Use accessibility tree faster DOM parsing
38+
only_visible=True
3439
)
40+
activity = await stagehand.page.observe(options)
3541
print("\nObservations:", activity)
3642
print("Length of observations:", len(activity))
3743

3844
print("Click on the first extracted element")
39-
print(activity[0])
40-
await stagehand.page.click(activity[0]["selector"])
45+
if activity:
46+
print(activity[0])
47+
await stagehand.page.click(activity[0]["selector"])
48+
else:
49+
print("No elements found")
4150

4251
except Exception as e:
4352
print(f"Error: {e}")

stagehand/client.py

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from playwright.async_api import async_playwright
1010
from .page import StagehandPage
1111
from .utils import default_log_handler
12+
from .config import StagehandConfig
1213

1314
load_dotenv()
1415

@@ -19,15 +20,15 @@ class Stagehand:
1920
Python client for interacting with a running Stagehand server and Browserbase remote headless browser.
2021
2122
Now supports automatically creating a new session if no session_id is provided.
22-
You can also optionally provide modelName, domSettleTimeoutMs, verbose, and debugDom,
23-
which will be sent to the server if a new session is created.
23+
You can also optionally provide a configuration via the 'config' parameter to centralize all parameters.
2424
"""
2525

2626
# Dictionary to store one lock per session_id
2727
_session_locks = {}
2828

2929
def __init__(
3030
self,
31+
config: Optional[StagehandConfig] = None,
3132
server_url: Optional[str] = None,
3233
session_id: Optional[str] = None,
3334
browserbase_api_key: Optional[str] = None,
@@ -42,36 +43,56 @@ def __init__(
4243
timeout_settings: Optional[httpx.Timeout] = None,
4344
):
4445
"""
45-
:param server_url: The running Stagehand server URL.
46-
:param session_id: An existing Browserbase session ID (if you already have one).
47-
:param browserbase_api_key: Your Browserbase API key.
48-
:param browserbase_project_id: Your Browserbase project ID.
49-
:param model_api_key: Your model API key (e.g. OpenAI, Anthropic, etc).
50-
:param on_log: Async callback for log messages streamed from the server.
51-
:param verbose: Verbosity level for console logs from this client.
52-
:param model_name: Model name to use when creating a new session (e.g., "gpt-4o").
53-
:param dom_settle_timeout_ms: Additional time for the DOM to settle.
54-
:param debug_dom: Whether or not to enable DOM debug mode.
55-
:param httpx_client: Optional custom httpx.AsyncClient instance.
56-
:param timeout_settings: Optional custom timeout settings for httpx.
46+
Initialize the Stagehand client.
47+
48+
Args:
49+
config (Optional[StagehandConfig]): Optional configuration object encapsulating common parameters.
50+
server_url (Optional[str]): The running Stagehand server URL.
51+
session_id (Optional[str]): An existing Browserbase session ID.
52+
browserbase_api_key (Optional[str]): Your Browserbase API key.
53+
browserbase_project_id (Optional[str]): Your Browserbase project ID.
54+
model_api_key (Optional[str]): Your model API key (e.g. OpenAI, Anthropic, etc.).
55+
on_log (Optional[Callable[[Dict[str, Any]], Awaitable[None]]]): Async callback for log messages from the server.
56+
verbose (int): Verbosity level for logs.
57+
model_name (Optional[str]): Model name to use when creating a new session.
58+
dom_settle_timeout_ms (Optional[int]): Additional time for the DOM to settle (in ms).
59+
debug_dom (Optional[bool]): Whether to enable DOM debugging mode.
60+
httpx_client (Optional[httpx.AsyncClient]): Optional custom httpx.AsyncClient instance.
61+
timeout_settings (Optional[httpx.Timeout]): Optional custom timeout settings for httpx.
5762
"""
58-
5963
self.server_url = server_url or os.getenv("STAGEHAND_SERVER_URL")
60-
self.session_id = session_id
61-
self.browserbase_api_key = browserbase_api_key or os.getenv("BROWSERBASE_API_KEY")
62-
self.browserbase_project_id = browserbase_project_id or os.getenv("BROWSERBASE_PROJECT_ID")
63-
self.model_api_key = model_api_key or os.getenv("OPENAI_API_KEY") # Fallback to OPENAI_API_KEY for backwards compatibility
64+
65+
if config:
66+
self.browserbase_api_key = config.api_key or browserbase_api_key or os.getenv("BROWSERBASE_API_KEY")
67+
self.browserbase_project_id = config.project_id or browserbase_project_id or os.getenv("BROWSERBASE_PROJECT_ID")
68+
self.model_api_key = model_api_key or (
69+
config.model_client_options.get("apiKey") if config.model_client_options else None
70+
) or os.getenv("MODEL_API_KEY")
71+
self.session_id = config.browserbase_session_id or session_id
72+
self.model_name = config.model_name or model_name
73+
self.dom_settle_timeout_ms = config.dom_settle_timeout_ms or dom_settle_timeout_ms
74+
self.debug_dom = config.debug_dom if config.debug_dom is not None else debug_dom
75+
self._custom_logger = config.logger # For future integration if needed
76+
# Additional config parameters available for future use:
77+
self.headless = config.headless
78+
self.enable_caching = config.enable_caching
79+
else:
80+
self.browserbase_api_key = browserbase_api_key or os.getenv("BROWSERBASE_API_KEY")
81+
self.browserbase_project_id = browserbase_project_id or os.getenv("BROWSERBASE_PROJECT_ID")
82+
self.model_api_key = model_api_key or os.getenv("MODEL_API_KEY")
83+
self.session_id = session_id
84+
self.model_name = model_name
85+
self.dom_settle_timeout_ms = dom_settle_timeout_ms
86+
self.debug_dom = debug_dom
87+
6488
self.on_log = on_log
6589
self.verbose = verbose
66-
self.model_name = model_name
67-
self.dom_settle_timeout_ms = dom_settle_timeout_ms
68-
self.debug_dom = debug_dom
6990
self.httpx_client = httpx_client
7091
self.timeout_settings = timeout_settings or httpx.Timeout(
71-
connect=90.0, # connection timeout
72-
read=90.0, # read timeout
73-
write=90.0, # write timeout
74-
pool=90.0, # pool timeout
92+
connect=90.0,
93+
read=90.0,
94+
write=90.0,
95+
pool=90.0,
7596
)
7697
self.streamed_response = True # Default to True for streamed responses
7798

@@ -82,16 +103,16 @@ def __init__(
82103
self._playwright_page = None
83104
self.page: Optional[StagehandPage] = None
84105

85-
self._initialized = False # Flag to track if we've already run init()
86-
self._closed = False # Flag to track if we've closed
106+
self._initialized = False # Flag to track if init() has run
107+
self._closed = False # Flag to track if resources have been closed
87108

88-
# Validate essential fields if session_id was given
109+
# Validate essential fields if session_id was provided
89110
if self.session_id:
90111
if not self.browserbase_api_key:
91112
raise ValueError("browserbase_api_key is required (or set BROWSERBASE_API_KEY in env).")
92113
if not self.browserbase_project_id:
93114
raise ValueError("browserbase_project_id is required (or set BROWSERBASE_PROJECT_ID in env).")
94-
115+
95116
def _get_lock_for_session(self) -> asyncio.Lock:
96117
"""
97118
Return an asyncio.Lock for this session. If one doesn't exist yet, create it.
@@ -288,7 +309,7 @@ async def _execute(self, method: str, payload: Dict[str, Any]) -> Any:
288309
"x-bb-project-id": self.browserbase_project_id,
289310
"Content-Type": "application/json",
290311
"Connection": "keep-alive",
291-
"x-streamed-response": str(self.streamed_response).lower()
312+
"x-stream-response": str(self.streamed_response).lower()
292313
}
293314
if self.model_api_key:
294315
headers["x-model-api-key"] = self.model_api_key

0 commit comments

Comments
 (0)