Files
browser-use__browser-use/browser_use/agent/run_logging.py
2026-05-31 03:38:50 -07:00

246 lines
9.8 KiB
Python

import json
import logging
import time
from typing import Any
from urllib.parse import urlparse
from browser_use.agent.views import ActionResult, AgentHistoryList, AgentOutput, AgentSettings, AgentState
from browser_use.browser import BrowserSession
from browser_use.browser.views import BrowserStateSummary
from browser_use.config import CONFIG
from browser_use.llm.base import BaseChatModel
from browser_use.telemetry.service import ProductTelemetry
from browser_use.telemetry.views import AgentTelemetryEvent
from browser_use.tokens.service import TokenCost
from browser_use.utils import check_latest_browser_use_version
class AgentRunLoggingMixin:
task: str
version: str
source: str
history: AgentHistoryList
llm: BaseChatModel
browser_session: BrowserSession | None
settings: AgentSettings
state: AgentState
telemetry: ProductTelemetry
token_cost_service: TokenCost
_demo_mode_enabled: bool
logger: logging.Logger
async def _log_agent_run(self) -> None:
"""Log the agent run"""
# Blue color for task
self.logger.info(f'\033[34m🎯 Task: {self.task}\033[0m')
self.logger.debug(f'🤖 Browser-Use Library Version {self.version} ({self.source})')
# Check for latest version and log upgrade message if needed
if CONFIG.BROWSER_USE_VERSION_CHECK:
latest_version = await check_latest_browser_use_version()
if latest_version and latest_version != self.version:
self.logger.info(
f'📦 Newer version available: {latest_version} (current: {self.version}). Upgrade with: uv add browser-use=={latest_version}'
)
def _log_first_step_startup(self) -> None:
"""Log startup message only on the first step"""
if len(self.history.history) == 0:
self.logger.info(
f'Starting a browser-use agent with version {self.version}, with provider={self.llm.provider} and model={self.llm.model}'
)
def _log_step_context(self, browser_state_summary: BrowserStateSummary) -> None:
"""Log step context information"""
url = browser_state_summary.url if browser_state_summary else ''
url_short = url[:50] + '...' if len(url) > 50 else url
interactive_count = len(browser_state_summary.dom_state.selector_map) if browser_state_summary else 0
self.logger.info('\n')
self.logger.info(f'📍 Step {self.state.n_steps}:')
self.logger.debug(f'Evaluating page with {interactive_count} interactive elements on: {url_short}')
def _log_next_action_summary(self, parsed: AgentOutput) -> None:
"""Log a comprehensive summary of the next action(s)"""
if not (self.logger.isEnabledFor(logging.DEBUG) and parsed.action):
return
# Collect action details
action_details = []
for _i, action in enumerate(parsed.action):
action_data = action.model_dump(exclude_unset=True)
action_name = next(iter(action_data.keys())) if action_data else 'unknown'
action_params = action_data.get(action_name, {}) if action_data else {}
# Format key parameters concisely
param_summary = []
if isinstance(action_params, dict):
for key, value in action_params.items():
if key == 'index':
param_summary.append(f'#{value}')
elif key == 'text' and isinstance(value, str):
text_preview = value[:30] + '...' if len(value) > 30 else value
param_summary.append(f'text="{text_preview}"')
elif key == 'url':
param_summary.append(f'url="{value}"')
elif key == 'success':
param_summary.append(f'success={value}')
elif isinstance(value, (str, int, bool)):
val_str = str(value)[:30] + '...' if len(str(value)) > 30 else str(value)
param_summary.append(f'{key}={val_str}')
param_str = f'({", ".join(param_summary)})' if param_summary else ''
action_details.append(f'{action_name}{param_str}')
def _prepare_demo_message(self, message: str, limit: int = 600) -> str:
# Previously truncated long entries; keep full text for better context in demo panel
return message.strip()
async def _demo_mode_log(self, message: str, level: str = 'info', metadata: dict[str, Any] | None = None) -> None:
if not self._demo_mode_enabled or not message or self.browser_session is None:
return
try:
await self.browser_session.send_demo_mode_log(
message=self._prepare_demo_message(message),
level=level,
metadata=metadata or {},
)
except Exception as exc:
self.logger.debug(f'[DemoMode] Failed to send overlay log: {exc}')
async def _broadcast_model_state(self, parsed: AgentOutput) -> None:
if not self._demo_mode_enabled:
return
state = parsed.current_state
step_meta = {'step': self.state.n_steps}
if state.thinking:
await self._demo_mode_log(state.thinking, 'thought', step_meta)
if state.evaluation_previous_goal:
eval_text = state.evaluation_previous_goal
level = 'success' if 'success' in eval_text.lower() else 'warning' if 'failure' in eval_text.lower() else 'info'
await self._demo_mode_log(eval_text, level, step_meta)
if state.memory:
await self._demo_mode_log(f'Memory: {state.memory}', 'info', step_meta)
if state.next_goal:
await self._demo_mode_log(f'Next goal: {state.next_goal}', 'info', step_meta)
def _log_step_completion_summary(self, step_start_time: float, result: list[ActionResult]) -> str | None:
"""Log step completion summary with action count, timing, and success/failure stats"""
if not result:
return None
step_duration = time.time() - step_start_time
action_count = len(result)
# Count success and failures
success_count = sum(1 for r in result if not r.error)
failure_count = action_count - success_count
# Format success/failure indicators
success_indicator = f'✅ {success_count}' if success_count > 0 else ''
failure_indicator = f'❌ {failure_count}' if failure_count > 0 else ''
status_parts = [part for part in [success_indicator, failure_indicator] if part]
status_str = ' | '.join(status_parts) if status_parts else '✅ 0'
message = (
f'📍 Step {self.state.n_steps}: Ran {action_count} action{"" if action_count == 1 else "s"} '
f'in {step_duration:.2f}s: {status_str}'
)
self.logger.debug(message)
return message
def _log_final_outcome_messages(self) -> None:
"""Log helpful messages to user based on agent run outcome"""
# Check if agent failed
is_successful = self.history.is_successful()
if is_successful is False or is_successful is None:
# Get final result to check for specific failure reasons
final_result = self.history.final_result()
final_result_str = str(final_result).lower() if final_result else ''
# Check for captcha/cloudflare related failures
captcha_keywords = ['captcha', 'cloudflare', 'recaptcha', 'challenge', 'bot detection', 'access denied']
has_captcha_issue = any(keyword in final_result_str for keyword in captcha_keywords)
if has_captcha_issue:
self.logger.warning(
'Agent was blocked by a captcha. Cloud browsers include stealth fingerprinting and proxy rotation to avoid this.\n'
' Try: Browser(use_cloud=True) | Get an API key: https://cloud.browser-use.com?utm_source=oss&utm_medium=captcha_nudge'
)
# General failure message
self.logger.info('')
self.logger.info('Did the Agent not work as expected? Let us fix this!')
self.logger.info(' Open a short issue on GitHub: https://github.com/browser-use/browser-use/issues')
def _log_agent_event(self, max_steps: int, agent_run_error: str | None = None) -> None:
"""Sent the agent event for this run to telemetry"""
token_summary = self.token_cost_service.get_usage_tokens_for_model(self.llm.model)
# Prepare action_history data correctly
action_history_data = []
for item in self.history.history:
if item.model_output and item.model_output.action:
# Convert each ActionModel in the step to its dictionary representation
step_actions = [
action.model_dump(exclude_unset=True)
for action in item.model_output.action
if action # Ensure action is not None if list allows it
]
action_history_data.append(step_actions)
else:
# Append None or [] if a step had no actions or no model output
action_history_data.append(None)
final_res = self.history.final_result()
final_result_str = json.dumps(final_res) if final_res is not None else None
# Extract judgement data if available
judgement_data = self.history.judgement()
judge_verdict = judgement_data.get('verdict') if judgement_data else None
judge_reasoning = judgement_data.get('reasoning') if judgement_data else None
judge_failure_reason = judgement_data.get('failure_reason') if judgement_data else None
judge_reached_captcha = judgement_data.get('reached_captcha') if judgement_data else None
judge_impossible_task = judgement_data.get('impossible_task') if judgement_data else None
self.telemetry.capture(
AgentTelemetryEvent(
task=self.task,
model=self.llm.model,
model_provider=self.llm.provider,
max_steps=max_steps,
max_actions_per_step=self.settings.max_actions_per_step,
use_vision=self.settings.use_vision,
version=self.version,
source=self.source,
cdp_url=urlparse(self.browser_session.cdp_url).hostname
if self.browser_session and self.browser_session.cdp_url
else None,
agent_type=None, # Regular Agent (not code-use)
action_errors=self.history.errors(),
action_history=action_history_data,
urls_visited=self.history.urls(),
steps=self.state.n_steps,
total_input_tokens=token_summary.prompt_tokens,
total_output_tokens=token_summary.completion_tokens,
prompt_cached_tokens=token_summary.prompt_cached_tokens,
total_tokens=token_summary.total_tokens,
total_duration_seconds=self.history.total_duration_seconds(),
success=self.history.is_successful(),
final_result_response=final_result_str,
error_message=agent_run_error,
judge_verdict=judge_verdict,
judge_reasoning=judge_reasoning,
judge_failure_reason=judge_failure_reason,
judge_reached_captcha=judge_reached_captcha,
judge_impossible_task=judge_impossible_task,
)
)