Skip to content

Commit d1c342a

Browse files
authored
Merge pull request #77 from browserbase/miguel/stg-463-consolidate-client-base
consolidate base-client
2 parents 7953669 + c1b9611 commit d1c342a

File tree

8 files changed

+98
-219
lines changed

8 files changed

+98
-219
lines changed

stagehand/api.py

Whitespace-only changes.

stagehand/base.py

Lines changed: 0 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,169 +0,0 @@
1-
import logging
2-
import os
3-
from abc import ABC, abstractmethod
4-
from collections.abc import Awaitable
5-
from typing import Any, Callable, Optional
6-
7-
from browserbase.types import SessionCreateParams as BrowserbaseSessionCreateParams
8-
9-
from .config import StagehandConfig
10-
from .page import StagehandPage
11-
from .utils import default_log_handler
12-
13-
logger = logging.getLogger(__name__)
14-
15-
16-
class StagehandBase(ABC):
17-
"""
18-
Base class for Stagehand client implementations.
19-
Defines the common interface and functionality.
20-
"""
21-
22-
def __init__(
23-
self,
24-
config: Optional["StagehandConfig"] = None,
25-
server_url: Optional[str] = None,
26-
session_id: Optional[str] = None,
27-
browserbase_api_key: Optional[str] = None,
28-
browserbase_project_id: Optional[str] = None,
29-
model_api_key: Optional[str] = None,
30-
on_log: Optional[
31-
Callable[[dict[str, Any]], Awaitable[None]]
32-
] = default_log_handler,
33-
verbose: int = 1,
34-
model_name: Optional[str] = None,
35-
dom_settle_timeout_ms: Optional[int] = None,
36-
timeout_settings: Optional[Any] = None,
37-
stream_response: Optional[bool] = None,
38-
model_client_options: Optional[dict[str, Any]] = None,
39-
self_heal: Optional[bool] = None,
40-
wait_for_captcha_solves: Optional[bool] = None,
41-
system_prompt: Optional[str] = None,
42-
browserbase_session_create_params: Optional[
43-
BrowserbaseSessionCreateParams
44-
] = None,
45-
enable_caching: Optional[bool] = None,
46-
):
47-
"""
48-
Initialize the Stagehand client with common configuration.
49-
50-
Args:
51-
config (Optional[StagehandConfig]): Configuration object that can provide all settings.
52-
server_url (Optional[str]): URL of the Stagehand server.
53-
session_id (Optional[str]): Existing session ID to resume.
54-
browserbase_api_key (Optional[str]): Browserbase API key.
55-
browserbase_project_id (Optional[str]): Browserbase project ID.
56-
model_api_key (Optional[str]): Model provider API key.
57-
on_log (Optional[Callable]): Callback for log events.
58-
verbose (int): Verbosity level.
59-
model_name (Optional[str]): Name of the model to use.
60-
dom_settle_timeout_ms (Optional[int]): Time for DOM to settle in ms.
61-
debug_dom (Optional[bool]): Whether to enable DOM debugging.
62-
timeout_settings (Optional[float]): Request timeout in seconds.
63-
stream_response (Optional[bool]): Whether to stream responses.
64-
model_client_options (Optional[dict]): Options for the model client.
65-
self_heal (Optional[bool]): Whether to enable self-healing.
66-
wait_for_captcha_solves (Optional[bool]): Whether to wait for CAPTCHA solves.
67-
act_timeout_ms (Optional[int]): Timeout for act commands in ms.
68-
system_prompt (Optional[str]): System prompt for LLM interactions.
69-
browserbase_session_create_params (Optional[BrowserbaseSessionCreateParams]): Params for Browserbase session creation.
70-
enable_caching (Optional[bool]): Whether to enable caching functionality.
71-
"""
72-
self.server_url = server_url or os.getenv("STAGEHAND_SERVER_URL")
73-
74-
if config:
75-
self.browserbase_api_key = (
76-
config.api_key
77-
or browserbase_api_key
78-
or os.getenv("BROWSERBASE_API_KEY")
79-
)
80-
self.browserbase_project_id = (
81-
config.project_id
82-
or browserbase_project_id
83-
or os.getenv("BROWSERBASE_PROJECT_ID")
84-
)
85-
self.session_id = config.browserbase_session_id or session_id
86-
self.model_name = config.model_name or model_name
87-
self.dom_settle_timeout_ms = (
88-
config.dom_settle_timeout_ms or dom_settle_timeout_ms
89-
)
90-
self.self_heal = (
91-
config.self_heal if config.self_heal is not None else self_heal
92-
)
93-
self.wait_for_captcha_solves = (
94-
config.wait_for_captcha_solves
95-
if config.wait_for_captcha_solves is not None
96-
else wait_for_captcha_solves
97-
)
98-
self.system_prompt = config.system_prompt or system_prompt
99-
self.browserbase_session_create_params = (
100-
config.browserbase_session_create_params
101-
or browserbase_session_create_params
102-
)
103-
self.enable_caching = (
104-
config.enable_caching
105-
if config.enable_caching is not None
106-
else enable_caching
107-
)
108-
self.verbose = config.verbose if config.verbose is not None else verbose
109-
else:
110-
self.browserbase_api_key = browserbase_api_key or os.getenv(
111-
"BROWSERBASE_API_KEY"
112-
)
113-
self.browserbase_project_id = browserbase_project_id or os.getenv(
114-
"BROWSERBASE_PROJECT_ID"
115-
)
116-
self.session_id = session_id
117-
self.model_name = model_name
118-
self.dom_settle_timeout_ms = dom_settle_timeout_ms
119-
self.self_heal = self_heal
120-
self.wait_for_captcha_solves = wait_for_captcha_solves
121-
self.system_prompt = system_prompt
122-
self.browserbase_session_create_params = browserbase_session_create_params
123-
self.enable_caching = enable_caching
124-
self.verbose = verbose
125-
126-
# Handle model-related settings directly
127-
self.model_api_key = model_api_key or os.getenv("MODEL_API_KEY")
128-
self.model_client_options = model_client_options or {}
129-
if self.model_api_key and "apiKey" not in self.model_client_options:
130-
self.model_client_options["apiKey"] = self.model_api_key
131-
132-
# Handle streaming response setting directly
133-
self.streamed_response = (
134-
stream_response if stream_response is not None else True
135-
)
136-
137-
self.on_log = on_log
138-
self.timeout_settings = timeout_settings or 180.0
139-
140-
self._initialized = False
141-
self._closed = False
142-
self.page: Optional[StagehandPage] = None
143-
144-
# Validate essential fields if session_id was provided
145-
if self.session_id:
146-
if not self.browserbase_api_key:
147-
raise ValueError(
148-
"browserbase_api_key is required (or set BROWSERBASE_API_KEY in env)."
149-
)
150-
if not self.browserbase_project_id:
151-
raise ValueError(
152-
"browserbase_project_id is required (or set BROWSERBASE_PROJECT_ID in env)."
153-
)
154-
155-
@abstractmethod
156-
def init(self):
157-
"""
158-
Initialize the Stagehand client.
159-
Must be implemented by subclasses.
160-
"""
161-
pass
162-
163-
@abstractmethod
164-
def close(self):
165-
"""
166-
Clean up resources.
167-
Must be implemented by subclasses.
168-
"""
169-
pass

stagehand/browser.py

Whitespace-only changes.

stagehand/client.py

Lines changed: 78 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import httpx
1212
from browserbase import Browserbase
13+
from browserbase.types import SessionCreateParams as BrowserbaseSessionCreateParams
1314
from dotenv import load_dotenv
1415
from playwright.async_api import (
1516
BrowserContext,
@@ -18,18 +19,17 @@
1819
)
1920
from playwright.async_api import Page as PlaywrightPage
2021

21-
from .base import StagehandBase
2222
from .config import StagehandConfig
2323
from .context import StagehandContext
2424
from .llm import LLMClient
2525
from .metrics import StagehandFunctionName, StagehandMetrics
2626
from .page import StagehandPage
27-
from .utils import StagehandLogger, convert_dict_keys_to_camel_case
27+
from .utils import StagehandLogger, convert_dict_keys_to_camel_case, default_log_handler
2828

2929
load_dotenv()
3030

3131

32-
class Stagehand(StagehandBase):
32+
class Stagehand:
3333
"""
3434
Python client for interacting with a running Stagehand server and Browserbase remote headless browser.
3535
@@ -42,6 +42,7 @@ class Stagehand(StagehandBase):
4242

4343
def __init__(
4444
self,
45+
*,
4546
config: Optional[StagehandConfig] = None,
4647
server_url: Optional[str] = None,
4748
session_id: Optional[str] = None,
@@ -62,6 +63,7 @@ def __init__(
6263
use_rich_logging: bool = True,
6364
env: Literal["BROWSERBASE", "LOCAL"] = None,
6465
local_browser_launch_options: Optional[dict[str, Any]] = None,
66+
browserbase_session_create_params: Optional[BrowserbaseSessionCreateParams] = None,
6567
):
6668
"""
6769
Initialize the Stagehand client.
@@ -89,31 +91,81 @@ def __init__(
8991
local_browser_launch_options (Optional[dict[str, Any]]): Options for launching the local browser context
9092
when env="LOCAL". See Playwright's launch_persistent_context documentation.
9193
Common keys: 'headless', 'user_data_dir', 'downloads_path', 'viewport', 'locale', 'proxy', 'args', 'cdp_url'.
94+
browserbase_session_create_params (Optional[BrowserbaseSessionCreateParams]): Params for Browserbase session creation.
9295
"""
93-
super().__init__(
94-
config=config,
95-
server_url=server_url,
96-
session_id=session_id,
97-
browserbase_api_key=browserbase_api_key,
98-
browserbase_project_id=browserbase_project_id,
99-
model_api_key=model_api_key,
100-
on_log=on_log,
101-
verbose=verbose,
102-
model_name=model_name,
103-
dom_settle_timeout_ms=dom_settle_timeout_ms,
104-
timeout_settings=timeout_settings,
105-
stream_response=stream_response,
106-
model_client_options=model_client_options,
107-
self_heal=self_heal,
108-
wait_for_captcha_solves=wait_for_captcha_solves,
109-
system_prompt=system_prompt,
96+
# Initialize configuration from config object or individual parameters
97+
self.server_url = server_url or os.getenv("STAGEHAND_SERVER_URL")
98+
99+
if config:
100+
self.browserbase_api_key = (
101+
config.api_key
102+
or browserbase_api_key
103+
or os.getenv("BROWSERBASE_API_KEY")
104+
)
105+
self.browserbase_project_id = (
106+
config.project_id
107+
or browserbase_project_id
108+
or os.getenv("BROWSERBASE_PROJECT_ID")
109+
)
110+
self.session_id = config.browserbase_session_id or session_id
111+
self.model_name = config.model_name or model_name
112+
self.dom_settle_timeout_ms = (
113+
config.dom_settle_timeout_ms or dom_settle_timeout_ms
114+
)
115+
self.self_heal = (
116+
config.self_heal if config.self_heal is not None else self_heal
117+
)
118+
self.wait_for_captcha_solves = (
119+
config.wait_for_captcha_solves
120+
if config.wait_for_captcha_solves is not None
121+
else wait_for_captcha_solves
122+
)
123+
self.system_prompt = config.system_prompt or system_prompt
124+
self.browserbase_session_create_params = (
125+
config.browserbase_session_create_params
126+
or browserbase_session_create_params
127+
)
128+
self.verbose = config.verbose if config.verbose is not None else verbose
129+
else:
130+
self.browserbase_api_key = browserbase_api_key or os.getenv(
131+
"BROWSERBASE_API_KEY"
132+
)
133+
self.browserbase_project_id = browserbase_project_id or os.getenv(
134+
"BROWSERBASE_PROJECT_ID"
135+
)
136+
self.session_id = session_id
137+
self.model_name = model_name
138+
self.dom_settle_timeout_ms = dom_settle_timeout_ms
139+
self.self_heal = self_heal
140+
self.wait_for_captcha_solves = wait_for_captcha_solves
141+
self.system_prompt = system_prompt
142+
self.browserbase_session_create_params = browserbase_session_create_params
143+
self.verbose = verbose
144+
145+
# Handle model-related settings directly
146+
self.model_api_key = model_api_key or os.getenv("MODEL_API_KEY")
147+
self.model_client_options = model_client_options or {}
148+
if self.model_api_key and "apiKey" not in self.model_client_options:
149+
self.model_client_options["apiKey"] = self.model_api_key
150+
151+
# Handle streaming response setting directly
152+
self.streamed_response = (
153+
stream_response if stream_response is not None else True
154+
)
155+
156+
self.on_log = on_log or default_log_handler
157+
self.timeout_settings = timeout_settings or httpx.Timeout(
158+
connect=180.0,
159+
read=180.0,
160+
write=180.0,
161+
pool=180.0,
110162
)
111163

112164
self.env = env.upper() if env else "BROWSERBASE"
113165
self.local_browser_launch_options = (
114166
getattr(config, "local_browser_launch_options", {})
115167
if config
116-
else local_browser_launch_options
168+
else local_browser_launch_options if local_browser_launch_options else {}
117169
)
118170
self._local_user_data_dir_temp: Optional[Path] = (
119171
None # To store path if created temporarily
@@ -127,6 +179,11 @@ def __init__(
127179
if self.env not in ["BROWSERBASE", "LOCAL"]:
128180
raise ValueError("env must be either 'BROWSERBASE' or 'LOCAL'")
129181

182+
# Initialize the centralized logger with the specified verbosity
183+
self.logger = StagehandLogger(
184+
verbose=self.verbose, external_logger=on_log, use_rich=use_rich_logging
185+
)
186+
130187
# If using BROWSERBASE, session_id or creation params are needed
131188
if self.env == "BROWSERBASE":
132189
if not self.session_id:
@@ -155,18 +212,7 @@ def __init__(
155212
"browserbase_project_id is required for BROWSERBASE env with existing session_id (or set BROWSERBASE_PROJECT_ID in env)."
156213
)
157214

158-
# Initialize the centralized logger with the specified verbosity
159-
self.logger = StagehandLogger(
160-
verbose=self.verbose, external_logger=on_log, use_rich=use_rich_logging
161-
)
162-
163215
self.httpx_client = httpx_client
164-
self.timeout_settings = timeout_settings or httpx.Timeout(
165-
connect=180.0,
166-
read=180.0,
167-
write=180.0,
168-
pool=180.0,
169-
)
170216
self._client: Optional[httpx.AsyncClient] = (
171217
None # Used for server communication in BROWSERBASE
172218
)

stagehand/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class StagehandConfig(BaseModel):
7777
description="System prompt to use for LLM interactions",
7878
)
7979
local_browser_launch_options: Optional[dict[str, Any]] = Field(
80-
None,
80+
{},
8181
alias="localBrowserLaunchOptions",
8282
description="Local browser launch options",
8383
)

stagehand/context.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,14 @@ async def wrapped_new_page(*args, **kwargs):
8787

8888
return wrapped_new_page
8989
elif name == "pages":
90-
# Wrap the pages method to return StagehandPage objects
91-
def wrapped_pages(*args, **kwargs):
92-
pw_pages = self._context.pages(*args, **kwargs)
93-
# This is synchronous, so we can't await here
94-
# We'll return the unwrapped pages and let the caller handle wrapping if needed
95-
return pw_pages
90+
async def wrapped_pages():
91+
pw_pages = self._context.pages
92+
# Return StagehandPage objects
93+
result = []
94+
for pw_page in pw_pages:
95+
stagehand_page = await self.get_stagehand_page(pw_page)
96+
result.append(stagehand_page)
97+
return result
9698

9799
return wrapped_pages
98100
return attr

stagehand/handlers/act_handler_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ async def handle_possible_page_navigation(
444444
},
445445
)
446446

447-
if new_opened_tab:
447+
if new_opened_tab and new_opened_tab.url != 'about:blank':
448448
logger.info(
449449
message="new page detected (new tab) with URL",
450450
category="action",

0 commit comments

Comments
 (0)