Skip to content

Commit 3e242f7

Browse files
committed
updates for BB-820: first crack
1 parent 3ad5094 commit 3e242f7

File tree

8 files changed

+297
-118
lines changed

8 files changed

+297
-118
lines changed

examples/example.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from dotenv import load_dotenv
55
from stagehand.client import Stagehand
66
from stagehand.config import StagehandConfig
7-
from stagehand.schemas import ActOptions, ObserveOptions
7+
from pydantic import BaseModel
8+
from stagehand.schemas import ExtractOptions
89

910
load_dotenv()
1011

@@ -15,14 +16,16 @@
1516
datefmt='%Y-%m-%d %H:%M:%S'
1617
)
1718

19+
class ExtractSchema(BaseModel):
20+
stars: int
21+
1822
async def main():
1923
try:
2024
# Build a unified configuration object for Stagehand
2125
config = StagehandConfig(
22-
env="BROWSERBASE" if os.getenv("BROWSERBASE_API_KEY") and os.getenv("BROWSERBASE_PROJECT_ID") else "LOCAL",
26+
env="BROWSERBASE",
2327
api_key=os.getenv("BROWSERBASE_API_KEY"),
2428
project_id=os.getenv("BROWSERBASE_PROJECT_ID"),
25-
debug_dom=True,
2629
headless=False,
2730
dom_settle_timeout_ms=3000,
2831
model_name="gpt-4o-mini",
@@ -34,38 +37,67 @@ async def main():
3437

3538
# Initialize - this creates a new session automatically.
3639
await stagehand.init()
40+
page = stagehand.page
3741
print(f"Created new session with ID: {stagehand.session_id}")
3842

3943
print('EXAMPLE: You can navigate to any website using the local or remote Playwright.')
4044

41-
await stagehand.page.goto("https://news.ycombinator.com/")
45+
await page.goto("https://news.ycombinator.com/")
4246
print("Navigation complete with local Playwright.")
4347

44-
await stagehand.page.navigate("https://www.google.com")
48+
await page.navigate("https://www.google.com")
4549
print("Navigation complete with remote Playwright.")
4650

4751
print("EXAMPLE: Clicking on About link using local Playwright's get_by_role")
4852
# Click on the "About" link using Playwright
49-
await stagehand.page.get_by_role("link", name="About", exact=True).click()
53+
await page.get_by_role("link", name="About", exact=True).click()
5054
print("Clicked on About link")
5155

5256
await asyncio.sleep(2)
53-
await stagehand.page.navigate("https://www.google.com")
57+
await page.navigate("https://www.google.com")
5458

5559
# Hosted Stagehand API - ACT to do something like 'search for openai'
56-
await stagehand.page.act(ActOptions(action="search for openai"))
60+
print(f"EXAMPLE: Performing action")
61+
await page.act("search for openai")
5762

58-
print("EXAMPLE: Find the XPATH of the button 'News' using Stagehand API")
59-
xpaths = await stagehand.page.observe(ObserveOptions(instruction="find the button labeled 'News'", only_visible=True))
60-
if len(xpaths) > 0:
61-
element = xpaths[0]
62-
print("EXAMPLE: Click on the button 'News' using local Playwright.")
63-
await stagehand.page.click(element["selector"])
63+
# print("EXAMPLE: Find the XPATH of the button 'News' using Stagehand API")
64+
observed = await page.observe("find the news button on the page")
65+
if len(observed) > 0:
66+
element = observed[0]
67+
# print("EXAMPLE: Click on the button 'News' using local Playwright.")
68+
await page.act(element)
6469
else:
6570
print("No element found")
6671

6772
except Exception as e:
6873
print(f"An error occurred in the example: {e}")
74+
finally:
75+
await stagehand.close()
76+
77+
new_stagehand = Stagehand(config=config, server_url=os.getenv("STAGEHAND_SERVER_URL"), verbose=2)
78+
# page = new_stagehand.page
79+
await new_stagehand.init()
80+
page = new_stagehand.page
81+
print(f"Created new session with ID: {new_stagehand.session_id}")
82+
83+
try:
84+
await page.navigate("https://github.com/facebook/react")
85+
print("Navigation complete.")
86+
87+
# Use the ExtractOptions Pydantic model to pass instruction and schema definition
88+
data = await page.extract("Extract the number of stars for the project")
89+
data = await page.extract(
90+
ExtractOptions(
91+
instruction="Extract the number of stars for the project",
92+
schemaDefinition=ExtractSchema.model_json_schema()
93+
)
94+
)
95+
print("\nExtracted stars:", data)
96+
97+
except Exception as e:
98+
print(f"Error: {e}")
99+
finally:
100+
await new_stagehand.close()
69101

70102
if __name__ == "__main__":
71103
asyncio.run(main())

examples/extract-example.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@ async def main():
3636
print("Navigation complete.")
3737

3838
# Use the ExtractOptions Pydantic model to pass instruction and schema definition
39+
print(ExtractSchema.model_json_schema())
3940
data = await stagehand.page.extract(
4041
ExtractOptions(
4142
instruction="Extract the number of stars for the project",
4243
schemaDefinition=ExtractSchema.model_json_schema()
4344
)
4445
)
45-
print("\nExtracted stars:", data)
46+
print("\nExtracted stars:", data["stars"])
4647

4748
except Exception as e:
4849
print(f"Error: {e}")

examples/observe-example.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@
1111
async def main():
1212
# Build a unified Stagehand configuration object
1313
config = StagehandConfig(
14-
env="BROWSERBASE" if os.getenv("BROWSERBASE_API_KEY") and os.getenv("BROWSERBASE_PROJECT_ID") else "LOCAL",
14+
env="BROWSERBASE",
1515
api_key=os.getenv("BROWSERBASE_API_KEY"),
1616
project_id=os.getenv("BROWSERBASE_PROJECT_ID"),
1717
debug_dom=True,
1818
headless=True,
1919
model_name="gpt-4o-mini",
20-
model_client_options={"apiKey": os.getenv("MODEL_API_KEY")}
2120
)
2221

2322
# Create a Stagehand client using the configuration object.
@@ -35,7 +34,6 @@ async def main():
3534
# Use ObserveOptions for detailed instructions
3635
options = ObserveOptions(
3736
instruction="find all the links on the page regarding the city of el paso",
38-
only_visible=True
3937
)
4038
activity = await stagehand.page.observe(options)
4139
print("\nObservations:", activity)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name="stagehand-py",
8-
version="0.2.1",
8+
version="0.3.0",
99
author="Browserbase, Inc.",
1010
author_email="[email protected]",
1111
description="Python SDK for Stagehand",

stagehand/client.py

Lines changed: 98 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,7 @@ def __init__(
6565
if config:
6666
self.browserbase_api_key = config.api_key or browserbase_api_key or os.getenv("BROWSERBASE_API_KEY")
6767
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")
68+
self.model_api_key = os.getenv("MODEL_API_KEY")
7169
self.session_id = config.browserbase_session_id or session_id
7270
self.model_name = config.model_name or model_name
7371
self.dom_settle_timeout_ms = config.dom_settle_timeout_ms or dom_settle_timeout_ms
@@ -200,20 +198,38 @@ async def close(self):
200198
return
201199

202200
self._log("Closing resources...", level=1)
203-
if self._playwright_page:
204-
self._log("Closing the Playwright page...", level=1)
205-
await self._playwright_page.close()
206-
self._playwright_page = None
201+
202+
# End the session on the server if we have a session ID
203+
if self.session_id:
204+
try:
205+
self._log(f"Ending session {self.session_id} on the server...", level=1)
206+
client = self.httpx_client or httpx.AsyncClient(timeout=self.timeout_settings)
207+
headers = {
208+
"x-bb-api-key": self.browserbase_api_key,
209+
"x-bb-project-id": self.browserbase_project_id,
210+
"Content-Type": "application/json",
211+
}
212+
213+
async with client:
214+
await self._execute("end", {"sessionId": self.session_id})
215+
self._log(f"Session {self.session_id} ended successfully", level=1)
216+
except Exception as e:
217+
self._log(f"Error ending session: {str(e)}", level=2)
218+
219+
# if self._playwright_page:
220+
# self._log("Closing the Playwright page...", level=1)
221+
# await self._playwright_page.close()
222+
# self._playwright_page = None
207223

208-
if self._context:
209-
self._log("Closing the context...", level=1)
210-
await self._context.close()
211-
self._context = None
224+
# if self._context:
225+
# self._log("Closing the context...", level=1)
226+
# await self._context.close()
227+
# self._context = None
212228

213-
if self._browser:
214-
self._log("Closing the browser...", level=1)
215-
await self._browser.close()
216-
self._browser = None
229+
# if self._browser:
230+
# self._log("Closing the browser...", level=1)
231+
# await self._browser.close()
232+
# self._browser = None
217233

218234
if self._playwright:
219235
self._log("Stopping Playwright...", level=1)
@@ -315,56 +331,76 @@ async def _execute(self, method: str, payload: Dict[str, Any]) -> Any:
315331
headers["x-model-api-key"] = self.model_api_key
316332

317333
client = self.httpx_client or httpx.AsyncClient(timeout=self.timeout_settings)
318-
print(f"Executing {method} with payload: {payload} and headers: {headers}")
334+
print(f"\n==== EXECUTING {method.upper()} ====")
335+
print(f"URL: {self.server_url}/sessions/{self.session_id}/{method}")
336+
print(f"Payload: {payload}")
337+
print(f"Headers: {headers}")
338+
319339
async with client:
320-
async with client.stream(
321-
"POST",
322-
f"{self.server_url}/sessions/{self.session_id}/{method}",
323-
json=payload,
324-
headers=headers,
325-
) as response:
326-
if response.status_code != 200:
327-
error_text = await response.aread()
328-
self._log(f"Error: {error_text.decode('utf-8')}", level=2)
329-
return None
330-
331-
async for line in response.aiter_lines():
332-
# Skip empty lines
333-
if not line.strip():
334-
continue
335-
336-
try:
337-
# Handle SSE-style messages that start with "data: "
338-
if line.startswith("data: "):
339-
line = line[len("data: "):]
340-
341-
message = json.loads(line)
342-
logger.info(f"Message: {message}")
343-
344-
# Handle different message types
345-
msg_type = message.get("type")
346-
347-
if msg_type == "system":
348-
status = message.get("data", {}).get("status")
349-
if status == "finished":
350-
return message.get("data", {}).get("result")
351-
elif msg_type == "log":
352-
# Log message from data.message
353-
log_msg = message.get("data", {}).get("message", "")
354-
self._log(log_msg, level=1)
355-
if self.on_log:
356-
await self.on_log(message)
357-
else:
358-
# Log any other message types
359-
self._log(f"Unknown message type: {msg_type}", level=2)
360-
if self.on_log:
361-
await self.on_log(message)
362-
363-
except json.JSONDecodeError:
364-
self._log(f"Could not parse line as JSON: {line}", level=2)
365-
continue
340+
try:
341+
async with client.stream(
342+
"POST",
343+
f"{self.server_url}/sessions/{self.session_id}/{method}",
344+
json=payload,
345+
headers=headers,
346+
) as response:
347+
print(f"Response status: {response.status_code}")
348+
349+
if response.status_code != 200:
350+
error_text = await response.aread()
351+
error_message = error_text.decode('utf-8')
352+
print(f"ERROR RESPONSE: {error_message}")
353+
self._log(f"Error: {error_message}", level=2)
354+
return None
355+
356+
print("Starting to process streaming response...")
357+
async for line in response.aiter_lines():
358+
# Skip empty lines
359+
if not line.strip():
360+
continue
361+
362+
try:
363+
# Handle SSE-style messages that start with "data: "
364+
if line.startswith("data: "):
365+
line = line[len("data: "):]
366+
367+
message = json.loads(line)
368+
print(f"RAW MESSAGE: {message}")
369+
370+
# Handle different message types
371+
msg_type = message.get("type")
372+
373+
if msg_type == "system":
374+
status = message.get("data", {}).get("status")
375+
if status == "finished":
376+
result = message.get("data", {}).get("result")
377+
print(f"FINISHED WITH RESULT: {result}")
378+
print(f"==== {method.upper()} COMPLETE ====\n")
379+
return result
380+
elif msg_type == "log":
381+
# Log message from data.message
382+
log_msg = message.get("data", {}).get("message", "")
383+
print(f"LOG MESSAGE: {log_msg}")
384+
self._log(log_msg, level=1)
385+
if self.on_log:
386+
await self.on_log(message)
387+
else:
388+
# Log any other message types
389+
print(f"UNKNOWN MESSAGE TYPE: {msg_type}")
390+
self._log(f"Unknown message type: {msg_type}", level=2)
391+
if self.on_log:
392+
await self.on_log(message)
393+
394+
except json.JSONDecodeError:
395+
print(f"JSON DECODE ERROR on line: {line}")
396+
self._log(f"Could not parse line as JSON: {line}", level=2)
397+
continue
398+
except Exception as e:
399+
print(f"EXCEPTION IN _EXECUTE: {str(e)}")
400+
raise
366401

367402
# If we get here without seeing a "finished" message, something went wrong
403+
print("==== ERROR: No 'finished' message received ====")
368404
raise RuntimeError("Server connection closed without sending 'finished' message")
369405

370406
async def _handle_log(self, msg: Dict[str, Any]):

stagehand/config.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from pydantic import BaseModel, Field
2-
from typing import Optional, Dict, Callable, Any
2+
from typing import Optional, Callable, Any
3+
from stagehand.schemas import AvailableModel
34

45
class StagehandConfig(BaseModel):
56
"""
67
Configuration for the Stagehand client.
78
89
Attributes:
9-
env (str): Environment type. Use 'BROWSERBASE' for remote usage or 'LOCAL' otherwise.
10+
env (str): Environment type. 'BROWSERBASE' for remote usage
1011
api_key (Optional[str]): API key for authentication.
1112
project_id (Optional[str]): Project identifier.
1213
debug_dom (bool): Enable DOM debugging features.
@@ -16,19 +17,19 @@ class StagehandConfig(BaseModel):
1617
enable_caching (Optional[bool]): Enable caching functionality.
1718
browserbase_session_id (Optional[str]): Session ID for resuming Browserbase sessions.
1819
model_name (Optional[str]): Name of the model to use.
19-
model_client_options (Optional[Dict[str, Any]]): Configuration options for the model client.
20+
selfHeal (Optional[bool]): Enable self-healing functionality.
2021
"""
21-
env: str = Field("LOCAL", description="Environment type, e.g., 'BROWSERBASE' for remote or 'LOCAL' for local")
22-
api_key: Optional[str] = Field(None, alias="apiKey", description="API key for authentication")
23-
project_id: Optional[str] = Field(None, alias="projectId", description="Project identifier")
22+
env: str = "BROWSERBASE"
23+
api_key: Optional[str] = Field(None, alias="apiKey", description="Browserbase API key for authentication")
24+
project_id: Optional[str] = Field(None, alias="projectId", description="Browserbase project ID")
2425
debug_dom: bool = Field(False, alias="debugDom", description="Enable DOM debugging features")
2526
headless: bool = Field(True, description="Run browser in headless mode")
2627
logger: Optional[Callable[[Any], None]] = Field(None, description="Custom logging function")
2728
dom_settle_timeout_ms: Optional[int] = Field(3000, alias="domSettleTimeoutMs", description="Timeout for DOM to settle (in ms)")
2829
enable_caching: Optional[bool] = Field(False, alias="enableCaching", description="Enable caching functionality")
2930
browserbase_session_id: Optional[str] = Field(None, alias="browserbaseSessionID", description="Session ID for resuming Browserbase sessions")
30-
model_name: Optional[str] = Field(None, alias="modelName", description="Name of the model to use")
31-
model_client_options: Optional[Dict[str, Any]] = Field(default_factory=dict, alias="modelClientOptions", description="Options for the model client")
31+
model_name: Optional[str] = Field(AvailableModel.GPT_4O, alias="modelName", description="Name of the model to use")
32+
selfHeal: Optional[bool] = Field(True, description="Enable self-healing functionality")
3233

3334
class Config:
3435
populate_by_name = True

0 commit comments

Comments
 (0)