Skip to content

Commit 76bfb90

Browse files
committed
centralize logging
1 parent ddae14f commit 76bfb90

File tree

5 files changed

+147
-46
lines changed

5 files changed

+147
-46
lines changed

stagehand/__init__.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
"""Stagehand - Browser automation with AI"""
2+
13
from .agent import Agent
24
from .client import Stagehand
3-
from .config import StagehandConfig, default_config
5+
from .config import StagehandConfig
46
from .handlers.observe_handler import ObserveHandler
5-
from .logging import configure_logging
7+
from .llm import LLMClient
8+
from .logging import LogConfig, configure_logging, get_logger
69
from .metrics import StagehandFunctionName, StagehandMetrics
710
from .page import StagehandPage
811
from .schemas import (
@@ -12,35 +15,34 @@
1215
AgentExecuteOptions,
1316
AgentExecuteResult,
1417
AgentProvider,
15-
AvailableModel,
1618
ExtractOptions,
1719
ExtractResult,
1820
ObserveOptions,
1921
ObserveResult,
2022
)
2123

22-
__version__ = "0.0.1"
24+
__version__ = "0.1.0"
2325

2426
__all__ = [
2527
"Stagehand",
2628
"StagehandConfig",
27-
"default_config",
2829
"StagehandPage",
2930
"Agent",
30-
"configure_logging",
31+
"AgentConfig",
32+
"AgentExecuteOptions",
33+
"AgentExecuteResult",
34+
"AgentProvider",
3135
"ActOptions",
3236
"ActResult",
33-
"AvailableModel",
3437
"ExtractOptions",
3538
"ExtractResult",
3639
"ObserveOptions",
3740
"ObserveResult",
38-
"AgentConfig",
39-
"AgentExecuteOptions",
40-
"AgentExecuteResult",
41-
"AgentProvider",
4241
"ObserveHandler",
43-
"observe",
42+
"LLMClient",
43+
"configure_logging",
4444
"StagehandFunctionName",
4545
"StagehandMetrics",
46+
"LogConfig",
47+
"get_logger",
4648
]

stagehand/agent/anthropic_cua.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ async def run_task(
243243
break
244244

245245
if not agent_action and not task_completed:
246-
self.logger.debug(
246+
self.logger.info(
247247
"Model did not request an action and task not marked complete. Ending task to prevent loop.",
248248
category=StagehandFunctionName.AGENT,
249249
)
@@ -290,7 +290,7 @@ def _process_provider_response(
290290
block.model_dump() for block in response.content
291291
]
292292
except Exception as e:
293-
self.logger.debug(
293+
self.logger.error(
294294
f"Could not model_dump response.content blocks: {e}",
295295
category=StagehandFunctionName.AGENT,
296296
)
@@ -337,7 +337,7 @@ def _convert_tool_use_to_agent_action(
337337
and tool_name != "goto"
338338
and tool_name != "navigate_back"
339339
):
340-
self.logger.debug(
340+
self.logger.error(
341341
f"Unsupported tool name from Anthropic: {tool_name}",
342342
category=StagehandFunctionName.AGENT,
343343
)
@@ -501,7 +501,7 @@ def _convert_tool_use_to_agent_action(
501501
)
502502
action_type_str = "drag" # Normalize
503503
else:
504-
self.logger.debug(
504+
self.logger.error(
505505
"Drag action missing valid start or end coordinates.",
506506
category=StagehandFunctionName.AGENT,
507507
)
@@ -559,7 +559,7 @@ def _convert_tool_use_to_agent_action(
559559
)
560560
action_type_str = "function"
561561
else:
562-
self.logger.debug(
562+
self.logger.error(
563563
"Goto action from Anthropic missing URL",
564564
category=StagehandFunctionName.AGENT,
565565
)
@@ -572,7 +572,7 @@ def _convert_tool_use_to_agent_action(
572572
)
573573
action_type_str = "function"
574574
else:
575-
self.logger.debug(
575+
self.logger.error(
576576
f"Unsupported action type '{action_type_str}' from Anthropic computer tool.",
577577
category=StagehandFunctionName.AGENT,
578578
)
@@ -613,7 +613,7 @@ def _format_action_feedback(
613613
self.format_screenshot(new_screenshot_base64)
614614
)
615615
else:
616-
self.logger.debug(
616+
self.logger.error(
617617
"Missing screenshot for computer tool feedback (empty string passed).",
618618
category=StagehandFunctionName.AGENT,
619619
)

stagehand/client.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from .config import StagehandConfig, default_config
2424
from .context import StagehandContext
2525
from .llm import LLMClient
26-
from .logging import StagehandLogger, default_log_handler
26+
from .logging import LogConfig, StagehandLogger, default_log_handler
2727
from .metrics import StagehandFunctionName, StagehandMetrics
2828
from .page import StagehandPage
2929
from .schemas import AgentConfig
@@ -159,12 +159,19 @@ def __init__(
159159
if self.env not in ["BROWSERBASE", "LOCAL"]:
160160
raise ValueError("env must be either 'BROWSERBASE' or 'LOCAL'")
161161

162-
# Initialize the centralized logger with the specified verbosity
163-
self.on_log = self.config.logger or default_log_handler
164-
self.logger = StagehandLogger(
165-
verbose=self.verbose, external_logger=self.on_log, use_rich=use_rich_logging
162+
# Create centralized log configuration
163+
self.log_config = LogConfig(
164+
verbose=self.verbose,
165+
use_rich=use_rich_logging,
166+
env=self.env,
167+
external_logger=self.config.logger or default_log_handler,
168+
quiet_dependencies=True,
166169
)
167170

171+
# Initialize the centralized logger with the LogConfig
172+
self.on_log = self.log_config.external_logger
173+
self.logger = StagehandLogger(config=self.log_config)
174+
168175
# If using BROWSERBASE, session_id or creation params are needed
169176
if self.env == "BROWSERBASE":
170177
if not self.session_id:
@@ -393,17 +400,14 @@ def _get_lock_for_session(self) -> asyncio.Lock:
393400
"""
394401
if self.session_id not in self._session_locks:
395402
self._session_locks[self.session_id] = asyncio.Lock()
396-
self.logger.debug(f"Created lock for session {self.session_id}")
397403
return self._session_locks[self.session_id]
398404

399405
async def __aenter__(self):
400-
self.logger.debug("Entering Stagehand context manager (__aenter__)...")
401406
# Just call init() if not already done
402407
await self.init()
403408
return self
404409

405410
async def __aexit__(self, exc_type, exc_val, exc_tb):
406-
self.logger.debug("Exiting Stagehand context manager (__aexit__)...")
407411
await self.close()
408412

409413
async def init(self):
@@ -463,7 +467,6 @@ async def init(self):
463467
self._browser = await self._playwright.chromium.connect_over_cdp(
464468
connect_url
465469
)
466-
self.logger.debug(f"Connected to remote browser: {self._browser}")
467470
except Exception as e:
468471
self.logger.error(f"Failed to connect Playwright via CDP: {str(e)}")
469472
await self.close()
@@ -769,7 +772,7 @@ async def _create_session(self):
769772

770773
payload = {
771774
"modelName": self.model_name,
772-
"verbose": self.verbose,
775+
"verbose": self.log_config.get_remote_verbose(),
773776
"domSettleTimeoutMs": self.dom_settle_timeout_ms,
774777
"browserbaseSessionCreateParams": (
775778
browserbase_session_create_params
@@ -850,10 +853,6 @@ async def _execute(self, method: str, payload: dict[str, Any]) -> Any:
850853
modified_payload = convert_dict_keys_to_camel_case(payload)
851854

852855
client = self.httpx_client or httpx.AsyncClient(timeout=self.timeout_settings)
853-
self.logger.debug(f"\n==== EXECUTING {method.upper()} ====")
854-
self.logger.debug(f"URL: {self.api_url}/sessions/{self.session_id}/{method}")
855-
self.logger.debug(f"Payload: {modified_payload}")
856-
self.logger.debug(f"Headers: {headers}")
857856

858857
async with client:
859858
try:
@@ -874,7 +873,6 @@ async def _execute(self, method: str, payload: dict[str, Any]) -> Any:
874873
f"Request failed with status {response.status_code}: {error_message}"
875874
)
876875

877-
self.logger.debug("[STREAM] Processing server response")
878876
result = None
879877

880878
async for line in response.aiter_lines():
@@ -903,9 +901,7 @@ async def _execute(self, method: str, payload: dict[str, Any]) -> Any:
903901
)
904902
elif status == "finished":
905903
result = message.get("data", {}).get("result")
906-
self.logger.debug(
907-
"[SYSTEM] Operation completed successfully"
908-
)
904+
909905
elif msg_type == "log":
910906
# Process log message using _handle_log
911907
await self._handle_log(message)

stagehand/logging.py

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,62 @@
1111
from rich.table import Table
1212
from rich.theme import Theme
1313

14+
15+
class LogConfig:
16+
"""
17+
Centralized configuration for logging across Stagehand.
18+
Manages log levels, formatting, and environment-specific settings.
19+
"""
20+
21+
def __init__(
22+
self,
23+
verbose: int = 1,
24+
use_rich: bool = True,
25+
env: str = "LOCAL",
26+
external_logger: Optional[Callable] = None,
27+
quiet_dependencies: bool = True,
28+
):
29+
"""
30+
Initialize logging configuration.
31+
32+
Args:
33+
verbose: Verbosity level (0=error, 1=info, 2=debug)
34+
use_rich: Whether to use Rich for formatted output
35+
env: Environment ("LOCAL" or "BROWSERBASE")
36+
external_logger: Optional external logging callback
37+
quiet_dependencies: Whether to quiet noisy dependencies
38+
"""
39+
self.verbose = verbose
40+
self.use_rich = use_rich
41+
self.env = env
42+
self.external_logger = external_logger
43+
self.quiet_dependencies = quiet_dependencies
44+
45+
def get_remote_verbose(self) -> int:
46+
"""
47+
Map local verbose levels to remote levels.
48+
Since we now use the same 3-level system, this is a direct mapping.
49+
"""
50+
return self.verbose
51+
52+
def get_python_log_level(self) -> int:
53+
"""Get the Python logging level based on verbose setting."""
54+
level_map = {
55+
0: logging.ERROR,
56+
1: logging.INFO,
57+
2: logging.DEBUG,
58+
}
59+
return level_map.get(self.verbose, logging.INFO)
60+
61+
def should_log(self, level: int) -> bool:
62+
"""Check if a message at the given level should be logged."""
63+
# Always log errors (level 0)
64+
if level == 0:
65+
return True
66+
# Otherwise check against verbose setting
67+
return level <= self.verbose
68+
69+
1470
# Custom theme for Rich
1571
stagehand_theme = Theme(
1672
{
@@ -27,8 +83,25 @@
2783
}
2884
)
2985

30-
# Create console instance with theme
31-
console = Console(theme=stagehand_theme)
86+
87+
def get_console(use_rich: bool = True) -> Console:
88+
"""
89+
Get a console instance based on whether Rich formatting is enabled.
90+
91+
Args:
92+
use_rich: If True, returns a console with theme. If False, returns a plain console.
93+
94+
Returns:
95+
Console instance configured appropriately
96+
"""
97+
if use_rich:
98+
return Console(theme=stagehand_theme)
99+
else:
100+
return Console(theme=None)
101+
102+
103+
# Create default console instance with theme (for backward compatibility)
104+
console = get_console(use_rich=True)
32105

33106
# Setup logging with Rich handler
34107
logger = logging.getLogger(__name__)
@@ -77,6 +150,8 @@ def configure_logging(
77150

78151
# Configure root logger with custom format
79152
if use_rich:
153+
# Get a console with theme for Rich handler
154+
rich_console = get_console(use_rich=True)
80155
# Use Rich handler for root logger
81156
logging.basicConfig(
82157
level=level,
@@ -86,7 +161,7 @@ def configure_logging(
86161
RichHandler(
87162
rich_tracebacks=True,
88163
markup=True,
89-
console=console,
164+
console=rich_console,
90165
show_time=False,
91166
show_level=False,
92167
)
@@ -127,6 +202,7 @@ def __init__(
127202
verbose: int = 1,
128203
external_logger: Optional[Callable] = None,
129204
use_rich: bool = True,
205+
config: Optional[LogConfig] = None,
130206
):
131207
"""
132208
Initialize the logger with specified verbosity and optional external logger.
@@ -135,13 +211,27 @@ def __init__(
135211
verbose: Verbosity level (0=error only, 1=info, 2=debug)
136212
external_logger: Optional callback function for log events
137213
use_rich: Whether to use Rich for pretty output (default: True)
214+
config: Optional LogConfig instance. If provided, overrides other parameters.
138215
"""
139-
self.verbose = verbose
140-
self.external_logger = external_logger
141-
self.use_rich = use_rich
142-
self.console = console
216+
if config:
217+
self.verbose = config.verbose
218+
self.external_logger = config.external_logger
219+
self.use_rich = config.use_rich
220+
self.config = config
221+
else:
222+
self.verbose = verbose
223+
self.external_logger = external_logger
224+
self.use_rich = use_rich
225+
self.config = LogConfig(
226+
verbose=verbose,
227+
use_rich=use_rich,
228+
external_logger=external_logger,
229+
)
230+
231+
self.console = get_console(self.use_rich)
143232

144233
# Map our verbosity levels to Python's logging levels
234+
# Now using only 3 levels to match the remote Fastify server
145235
self.level_map = {
146236
0: logging.ERROR, # Critical errors only
147237
1: logging.INFO, # Standard information
@@ -152,7 +242,7 @@ def __init__(
152242
self.level_style = {0: "error", 1: "info", 2: "debug"}
153243

154244
# Update logger level based on verbosity
155-
self._set_verbosity(verbose)
245+
self._set_verbosity(self.verbose)
156246

157247
def _set_verbosity(self, level: int):
158248
"""Set the logger verbosity level"""
@@ -560,6 +650,20 @@ def debug(
560650
self.log(message, level=2, category=category, auxiliary=auxiliary)
561651

562652

653+
def get_logger(name: str, config: LogConfig) -> StagehandLogger:
654+
"""
655+
Factory function to get a configured logger instance for a module.
656+
657+
Args:
658+
name: The name of the module requesting the logger
659+
config: LogConfig instance with logging configuration
660+
661+
Returns:
662+
StagehandLogger: Configured logger instance
663+
"""
664+
return StagehandLogger(config=config)
665+
666+
563667
# Create a synchronous wrapper for the async default_log_handler
564668
def sync_log_handler(log_data: dict[str, Any]) -> None:
565669
"""

0 commit comments

Comments
 (0)