4
4
import sys
5
5
import time
6
6
from pathlib import Path
7
- from typing import Any , Literal , Optional
7
+ from typing import Any , Optional
8
8
9
9
import httpx
10
10
from dotenv import load_dotenv
25
25
from .config import StagehandConfig , default_config
26
26
from .context import StagehandContext
27
27
from .llm import LLMClient
28
- from .logging import LogConfig , StagehandLogger , default_log_handler
29
28
from .metrics import StagehandFunctionName , StagehandMetrics
30
29
from .page import StagehandPage
31
30
from .schemas import AgentConfig
32
- from .utils import make_serializable
31
+ from .utils import (
32
+ StagehandLogger ,
33
+ default_log_handler ,
34
+ make_serializable ,
35
+ )
33
36
34
37
load_dotenv ()
35
38
36
39
37
40
class Stagehand :
38
41
"""
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.
44
43
"""
45
44
46
- # Dictionary to store one lock per session_id
47
45
_session_locks = {}
48
-
49
- # Flag to track if cleanup has been called
50
46
_cleanup_called = False
51
47
52
48
def __init__ (
53
49
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 ,
63
51
** config_overrides ,
64
52
):
65
53
"""
66
54
Initialize the Stagehand client.
67
55
68
56
Args:
69
57
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.
77
58
**config_overrides: Additional configuration overrides to apply to the config.
78
59
"""
79
- # Start with provided config or default config
80
- if config is None :
81
- config = default_config
82
60
83
61
# Apply any overrides
84
62
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
95
63
96
64
# Add any additional config overrides
97
65
overrides .update (config_overrides )
@@ -103,8 +71,9 @@ def __init__(
103
71
self .config = config
104
72
105
73
# 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
108
77
109
78
# Extract frequently used values from config for convenience
110
79
self .browserbase_api_key = self .config .api_key or os .getenv (
@@ -114,7 +83,6 @@ def __init__(
114
83
"BROWSERBASE_PROJECT_ID"
115
84
)
116
85
self .session_id = self .config .browserbase_session_id
117
- self .model_name = self .config .model_name
118
86
self .dom_settle_timeout_ms = self .config .dom_settle_timeout_ms
119
87
self .self_heal = self .config .self_heal
120
88
self .wait_for_captcha_solves = self .config .wait_for_captcha_solves
@@ -138,8 +106,7 @@ def __init__(
138
106
# Handle streaming response setting
139
107
self .streamed_response = True
140
108
141
- self .httpx_client = httpx_client
142
- self .timeout_settings = timeout_settings or httpx .Timeout (
109
+ self .timeout_settings = httpx .Timeout (
143
110
connect = 180.0 ,
144
111
read = 180.0 ,
145
112
write = 180.0 ,
@@ -158,19 +125,14 @@ def __init__(
158
125
if self .env not in ["BROWSERBASE" , "LOCAL" ]:
159
126
raise ValueError ("env must be either 'BROWSERBASE' or 'LOCAL'" )
160
127
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 (
163
131
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 ,
168
134
)
169
135
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
-
174
136
# If using BROWSERBASE, session_id or creation params are needed
175
137
if self .env == "BROWSERBASE" :
176
138
if not self .session_id :
@@ -185,7 +147,7 @@ def __init__(
185
147
)
186
148
if not self .model_api_key :
187
149
# Model API key needed if Stagehand server creates the session
188
- self .logger .info (
150
+ self .logger .warning (
189
151
"model_api_key is recommended when creating a new BROWSERBASE session to configure the Stagehand server's LLM."
190
152
)
191
153
elif self .session_id :
@@ -221,7 +183,6 @@ def __init__(
221
183
self .llm = None
222
184
if self .env == "LOCAL" :
223
185
self .llm = LLMClient (
224
- stagehand_logger = self .logger ,
225
186
api_key = self .model_api_key ,
226
187
default_model = self .model_name ,
227
188
metrics_callback = self ._handle_llm_metrics ,
@@ -403,11 +364,13 @@ def _get_lock_for_session(self) -> asyncio.Lock:
403
364
return self ._session_locks [self .session_id ]
404
365
405
366
async def __aenter__ (self ):
367
+ self .logger .debug ("Entering Stagehand context manager (__aenter__)..." )
406
368
# Just call init() if not already done
407
369
await self .init ()
408
370
return self
409
371
410
372
async def __aexit__ (self , exc_type , exc_val , exc_tb ):
373
+ self .logger .debug ("Exiting Stagehand context manager (__aexit__)..." )
411
374
await self .close ()
412
375
413
376
async def init (self ):
@@ -428,18 +391,16 @@ async def init(self):
428
391
429
392
if self .env == "BROWSERBASE" :
430
393
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 )
434
395
435
396
# Create session if we don't have one
436
397
if not self .session_id :
437
398
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 } "
440
401
)
441
402
else :
442
- self .logger .info (
403
+ self .logger .debug (
443
404
f"Using existing Browserbase session: { self .session_id } "
444
405
)
445
406
@@ -538,12 +499,11 @@ async def close(self):
538
499
f"Error ending server session { self .session_id } : { str (e )} "
539
500
)
540
501
elif self .session_id :
541
- self .logger .debug (
502
+ self .logger .warning (
542
503
"Cannot end server session: HTTP client not available."
543
504
)
544
505
545
- # Close internal HTTPX client if it was created by Stagehand
546
- if self ._client and not self .httpx_client :
506
+ if self ._client :
547
507
self .logger .debug ("Closing the internal HTTPX client..." )
548
508
await self ._client .aclose ()
549
509
self ._client = None
@@ -580,16 +540,17 @@ async def _handle_log(self, msg: dict[str, Any]):
580
540
581
541
# Map level strings to internal levels
582
542
level_map = {
583
- "debug" : 2 ,
543
+ "debug" : 3 ,
584
544
"info" : 1 ,
545
+ "warning" : 2 ,
585
546
"error" : 0 ,
586
547
}
587
548
588
549
# Convert string level to int if needed
589
550
if isinstance (level_str , str ):
590
551
internal_level = level_map .get (level_str .lower (), 1 )
591
552
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
593
554
594
555
# Handle the case where message itself might be a JSON-like object
595
556
if isinstance (message , dict ):
@@ -623,7 +584,7 @@ def _log(
623
584
624
585
Args:
625
586
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)
627
588
category: Optional category for the message
628
589
auxiliary: Optional auxiliary data to include
629
590
"""
0 commit comments