44import sys
55import time
66from pathlib import Path
7- from typing import Any , Literal , Optional
7+ from typing import Any , Optional
88
99import httpx
1010from dotenv import load_dotenv
2525from .config import StagehandConfig , default_config
2626from .context import StagehandContext
2727from .llm import LLMClient
28- from .logging import LogConfig , StagehandLogger , default_log_handler
2928from .metrics import StagehandFunctionName , StagehandMetrics
3029from .page import StagehandPage
3130from .schemas import AgentConfig
32- from .utils import make_serializable
31+ from .utils import (
32+ StagehandLogger ,
33+ default_log_handler ,
34+ make_serializable ,
35+ )
3336
3437load_dotenv ()
3538
3639
3740class 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