Skip to content

Commit 1ebf52f

Browse files
committed
Merge branch 'main' into miguel/stg-470-fix-logging
2 parents 43501a0 + 4986c27 commit 1ebf52f

File tree

5 files changed

+65
-85
lines changed

5 files changed

+65
-85
lines changed

pyproject.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ dependencies = [
2424
"playwright>=1.42.1",
2525
"requests>=2.31.0",
2626
"browserbase>=1.4.0",
27+
"rich>=14.0.0",
28+
"openai>=1.83.0",
29+
"anthropic>=0.51.0",
30+
"litellm>=1.72.0",
2731
]
2832

2933
[project.optional-dependencies]
@@ -36,15 +40,18 @@ dev = [
3640
"isort>=5.12.0",
3741
"mypy>=1.3.0",
3842
"ruff",
39-
"rich", # Useful for development/debugging
4043
]
4144

4245
[project.urls]
4346
Homepage = "https://github.com/browserbase/stagehand-python"
4447
Repository = "https://github.com/browserbase/stagehand-python"
4548

4649
[tool.setuptools]
47-
packages = ["stagehand"]
50+
# Omit explicit package list so we can use the nested `.packages.find` table below.
51+
52+
[tool.setuptools.packages.find]
53+
where = ["."]
54+
include = ["stagehand*"]
4855

4956
[tool.setuptools.package-data]
5057
stagehand = ["domScripts.js"]

stagehand/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Stagehand - The AI Browser Automation Framework"""
22

33
from .agent import Agent
4-
from .client import Stagehand
5-
from .config import StagehandConfig
4+
from .config import StagehandConfig, default_config
65
from .handlers.observe_handler import ObserveHandler
6+
from .main import Stagehand
77
from .llm import LLMClient
88
from .logging import LogConfig, configure_logging
99
from .metrics import StagehandFunctionName, StagehandMetrics

stagehand/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ async def _create_session(self):
7373
"x-language": "python",
7474
}
7575

76-
client = self.httpx_client or httpx.AsyncClient(timeout=self.timeout_settings)
76+
client = httpx.AsyncClient(timeout=self.timeout_settings)
7777
async with client:
7878
resp = await client.post(
7979
f"{self.api_url}/sessions/start",
@@ -109,7 +109,7 @@ async def _execute(self, method: str, payload: dict[str, Any]) -> Any:
109109
# Convert snake_case keys to camelCase for the API
110110
modified_payload = convert_dict_keys_to_camel_case(payload)
111111

112-
client = self.httpx_client or httpx.AsyncClient(timeout=self.timeout_settings)
112+
client = httpx.AsyncClient(timeout=self.timeout_settings)
113113

114114
async with client:
115115
try:

stagehand/config.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Callable, Optional
1+
from typing import Any, Callable, Literal, Optional
22

33
from browserbase.types import SessionCreateParams as BrowserbaseSessionCreateParams
44
from pydantic import BaseModel, ConfigDict, Field
@@ -12,37 +12,49 @@ class StagehandConfig(BaseModel):
1212
1313
Attributes:
1414
env (str): Environment type. 'BROWSERBASE' for remote usage
15-
api_key (Optional[str]): API key for authentication.
16-
project_id (Optional[str]): Project identifier.
17-
headless (bool): Run browser in headless mode.
18-
logger (Optional[Callable[[Any], None]]): Custom logging function.
19-
dom_settle_timeout_ms (Optional[int]): Timeout for DOM to settle (in milliseconds).
15+
api_key (Optional[str]): BrowserbaseAPI key for authentication.
16+
project_id (Optional[str]): Browserbase Project identifier.
17+
api_url (Optional[str]): Stagehand API URL.
2018
browserbase_session_create_params (Optional[BrowserbaseSessionCreateParams]): Browserbase session create params.
21-
enable_caching (Optional[bool]): Enable caching functionality.
2219
browserbase_session_id (Optional[str]): Session ID for resuming Browserbase sessions.
2320
model_name (Optional[str]): Name of the model to use.
21+
model_api_key (Optional[str]): Model API key.
22+
logger (Optional[Callable[[Any], None]]): Custom logging function.
23+
verbose (Optional[int]): Verbosity level for logs (1=minimal, 2=medium, 3=detailed).
24+
use_rich_logging (bool): Whether to use Rich for colorized logging.
25+
dom_settle_timeout_ms (Optional[int]): Timeout for DOM to settle (in milliseconds).
26+
enable_caching (Optional[bool]): Enable caching functionality.
2427
self_heal (Optional[bool]): Enable self-healing functionality.
2528
wait_for_captcha_solves (Optional[bool]): Whether to wait for CAPTCHA to be solved.
2629
act_timeout_ms (Optional[int]): Timeout for act commands (in milliseconds).
30+
headless (bool): Run browser in headless mode
2731
system_prompt (Optional[str]): System prompt to use for LLM interactions.
28-
verbose (Optional[int]): Verbosity level for logs (0=errors, 1=info, 2=debug).
2932
local_browser_launch_options (Optional[dict[str, Any]]): Local browser launch options.
3033
"""
3134

32-
env: str = "BROWSERBASE"
35+
env: Literal["BROWSERBASE", "LOCAL"] = "BROWSERBASE"
3336
api_key: Optional[str] = Field(
3437
None, alias="apiKey", description="Browserbase API key for authentication"
3538
)
3639
project_id: Optional[str] = Field(
3740
None, alias="projectId", description="Browserbase project ID"
3841
)
42+
api_url: Optional[str] = Field(
43+
None, alias="apiUrl", description="Stagehand API URL"
44+
) # might add a default value here
45+
model_api_key: Optional[str] = Field(
46+
None, alias="modelApiKey", description="Model API key"
47+
)
3948
verbose: Optional[int] = Field(
4049
1,
41-
description="Verbosity level for logs: 0=errors only, 1=info, 2=debug",
50+
description="Verbosity level for logs: 0=minimal (ERROR), 1=medium (INFO), 2=detailed (DEBUG)",
4251
)
4352
logger: Optional[Callable[[Any], None]] = Field(
4453
None, description="Custom logging function"
4554
)
55+
use_rich_logging: Optional[bool] = Field(
56+
True, description="Whether to use Rich for colorized logging"
57+
)
4658
dom_settle_timeout_ms: Optional[int] = Field(
4759
3000,
4860
alias="domSettleTimeoutMs",

stagehand/client.py renamed to stagehand/main.py

Lines changed: 30 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
import time
66
from pathlib import Path
7-
from typing import Any, Literal, Optional
7+
from typing import Any, Optional
88

99
import httpx
1010
from dotenv import load_dotenv
@@ -25,73 +25,41 @@
2525
from .config import StagehandConfig, default_config
2626
from .context import StagehandContext
2727
from .llm import LLMClient
28-
from .logging import LogConfig, StagehandLogger, default_log_handler
2928
from .metrics import StagehandFunctionName, StagehandMetrics
3029
from .page import StagehandPage
3130
from .schemas import AgentConfig
32-
from .utils import make_serializable
31+
from .utils import (
32+
StagehandLogger,
33+
default_log_handler,
34+
make_serializable,
35+
)
3336

3437
load_dotenv()
3538

3639

3740
class Stagehand:
3841
"""
39-
Python client for interacting with a running Stagehand server and Browserbase remote headless browser.
40-
41-
Now supports automatically creating a new session if no session_id is provided.
42-
You can provide a configuration via the 'config' parameter, or use individual parameters to override
43-
the default configuration values.
42+
Main Stagehand class.
4443
"""
4544

46-
# Dictionary to store one lock per session_id
4745
_session_locks = {}
48-
49-
# Flag to track if cleanup has been called
5046
_cleanup_called = False
5147

5248
def __init__(
5349
self,
54-
config: Optional[StagehandConfig] = None,
55-
*,
56-
api_url: Optional[str] = None,
57-
model_api_key: Optional[str] = None,
58-
session_id: Optional[str] = None,
59-
env: Optional[Literal["BROWSERBASE", "LOCAL"]] = None,
60-
httpx_client: Optional[httpx.AsyncClient] = None,
61-
timeout_settings: Optional[httpx.Timeout] = None,
62-
use_rich_logging: bool = True,
50+
config: StagehandConfig = default_config,
6351
**config_overrides,
6452
):
6553
"""
6654
Initialize the Stagehand client.
6755
6856
Args:
6957
config (Optional[StagehandConfig]): Configuration object. If not provided, uses default_config.
70-
api_url (Optional[str]): The running Stagehand server URL. Overrides config if provided.
71-
model_api_key (Optional[str]): Your model API key (e.g. OpenAI, Anthropic, etc.). Overrides config if provided.
72-
session_id (Optional[str]): Existing Browserbase session ID to connect to. Overrides config if provided.
73-
env (Optional[Literal["BROWSERBASE", "LOCAL"]]): Environment to run in. Overrides config if provided.
74-
httpx_client (Optional[httpx.AsyncClient]): Optional custom httpx.AsyncClient instance.
75-
timeout_settings (Optional[httpx.Timeout]): Optional custom timeout settings for httpx.
76-
use_rich_logging (bool): Whether to use Rich for colorized logging.
7758
**config_overrides: Additional configuration overrides to apply to the config.
7859
"""
79-
# Start with provided config or default config
80-
if config is None:
81-
config = default_config
8260

8361
# Apply any overrides
8462
overrides = {}
85-
if api_url is not None:
86-
# api_url isn't in config, handle separately
87-
pass
88-
if model_api_key is not None:
89-
# model_api_key isn't in config, handle separately
90-
pass
91-
if session_id is not None:
92-
overrides["browserbase_session_id"] = session_id
93-
if env is not None:
94-
overrides["env"] = env
9563

9664
# Add any additional config overrides
9765
overrides.update(config_overrides)
@@ -103,8 +71,9 @@ def __init__(
10371
self.config = config
10472

10573
# Handle non-config parameters
106-
self.api_url = api_url or os.getenv("STAGEHAND_API_URL")
107-
self.model_api_key = model_api_key or os.getenv("MODEL_API_KEY")
74+
self.api_url = self.config.api_url or os.getenv("STAGEHAND_API_URL")
75+
self.model_api_key = self.config.model_api_key or os.getenv("MODEL_API_KEY")
76+
self.model_name = self.config.model_name
10877

10978
# Extract frequently used values from config for convenience
11079
self.browserbase_api_key = self.config.api_key or os.getenv(
@@ -114,7 +83,6 @@ def __init__(
11483
"BROWSERBASE_PROJECT_ID"
11584
)
11685
self.session_id = self.config.browserbase_session_id
117-
self.model_name = self.config.model_name
11886
self.dom_settle_timeout_ms = self.config.dom_settle_timeout_ms
11987
self.self_heal = self.config.self_heal
12088
self.wait_for_captcha_solves = self.config.wait_for_captcha_solves
@@ -138,8 +106,7 @@ def __init__(
138106
# Handle streaming response setting
139107
self.streamed_response = True
140108

141-
self.httpx_client = httpx_client
142-
self.timeout_settings = timeout_settings or httpx.Timeout(
109+
self.timeout_settings = httpx.Timeout(
143110
connect=180.0,
144111
read=180.0,
145112
write=180.0,
@@ -158,19 +125,14 @@ def __init__(
158125
if self.env not in ["BROWSERBASE", "LOCAL"]:
159126
raise ValueError("env must be either 'BROWSERBASE' or 'LOCAL'")
160127

161-
# Create centralized log configuration
162-
self.log_config = LogConfig(
128+
# Initialize the centralized logger with the specified verbosity
129+
self.on_log = self.config.logger or default_log_handler
130+
self.logger = StagehandLogger(
163131
verbose=self.verbose,
164-
use_rich=use_rich_logging,
165-
env=self.env,
166-
external_logger=self.config.logger or default_log_handler,
167-
quiet_dependencies=True,
132+
external_logger=self.on_log,
133+
use_rich=self.config.use_rich_logging,
168134
)
169135

170-
# Initialize the centralized logger with the LogConfig
171-
self.on_log = self.log_config.external_logger
172-
self.logger = StagehandLogger(config=self.log_config)
173-
174136
# If using BROWSERBASE, session_id or creation params are needed
175137
if self.env == "BROWSERBASE":
176138
if not self.session_id:
@@ -185,7 +147,7 @@ def __init__(
185147
)
186148
if not self.model_api_key:
187149
# Model API key needed if Stagehand server creates the session
188-
self.logger.info(
150+
self.logger.warning(
189151
"model_api_key is recommended when creating a new BROWSERBASE session to configure the Stagehand server's LLM."
190152
)
191153
elif self.session_id:
@@ -221,7 +183,6 @@ def __init__(
221183
self.llm = None
222184
if self.env == "LOCAL":
223185
self.llm = LLMClient(
224-
stagehand_logger=self.logger,
225186
api_key=self.model_api_key,
226187
default_model=self.model_name,
227188
metrics_callback=self._handle_llm_metrics,
@@ -403,11 +364,13 @@ def _get_lock_for_session(self) -> asyncio.Lock:
403364
return self._session_locks[self.session_id]
404365

405366
async def __aenter__(self):
367+
self.logger.debug("Entering Stagehand context manager (__aenter__)...")
406368
# Just call init() if not already done
407369
await self.init()
408370
return self
409371

410372
async def __aexit__(self, exc_type, exc_val, exc_tb):
373+
self.logger.debug("Exiting Stagehand context manager (__aexit__)...")
411374
await self.close()
412375

413376
async def init(self):
@@ -428,18 +391,16 @@ async def init(self):
428391

429392
if self.env == "BROWSERBASE":
430393
if not self._client:
431-
self._client = self.httpx_client or httpx.AsyncClient(
432-
timeout=self.timeout_settings
433-
)
394+
self._client = httpx.AsyncClient(timeout=self.timeout_settings)
434395

435396
# Create session if we don't have one
436397
if not self.session_id:
437398
await self._create_session() # Uses self._client and api_url
438-
self.logger.info(
439-
f"Created new Browserbase session. Session ID: {self.session_id}"
399+
self.logger.debug(
400+
f"Created new Browserbase session via Stagehand server: {self.session_id}"
440401
)
441402
else:
442-
self.logger.info(
403+
self.logger.debug(
443404
f"Using existing Browserbase session: {self.session_id}"
444405
)
445406

@@ -538,12 +499,11 @@ async def close(self):
538499
f"Error ending server session {self.session_id}: {str(e)}"
539500
)
540501
elif self.session_id:
541-
self.logger.debug(
502+
self.logger.warning(
542503
"Cannot end server session: HTTP client not available."
543504
)
544505

545-
# Close internal HTTPX client if it was created by Stagehand
546-
if self._client and not self.httpx_client:
506+
if self._client:
547507
self.logger.debug("Closing the internal HTTPX client...")
548508
await self._client.aclose()
549509
self._client = None
@@ -580,16 +540,17 @@ async def _handle_log(self, msg: dict[str, Any]):
580540

581541
# Map level strings to internal levels
582542
level_map = {
583-
"debug": 2,
543+
"debug": 3,
584544
"info": 1,
545+
"warning": 2,
585546
"error": 0,
586547
}
587548

588549
# Convert string level to int if needed
589550
if isinstance(level_str, str):
590551
internal_level = level_map.get(level_str.lower(), 1)
591552
else:
592-
internal_level = min(level_str, 2) # Ensure level is between 0-2
553+
internal_level = min(level_str, 3) # Ensure level is between 0-3
593554

594555
# Handle the case where message itself might be a JSON-like object
595556
if isinstance(message, dict):
@@ -623,7 +584,7 @@ def _log(
623584
624585
Args:
625586
message: The message to log
626-
level: Verbosity level (0=error, 1=info, 2=debug)
587+
level: Verbosity level (0=error, 1=info, 2=detailed, 3=debug)
627588
category: Optional category for the message
628589
auxiliary: Optional auxiliary data to include
629590
"""

0 commit comments

Comments
 (0)