From 4d10c5160780b5ddee3b408dd3e6430dce5fa8f2 Mon Sep 17 00:00:00 2001 From: David Mieloch Date: Sun, 19 Jan 2025 15:17:03 -0500 Subject: [PATCH 01/10] Add CLI functionality with tests and configuration --- .gitignore | 9 + .vscode/settings.json | 6 + cli/__init__.py | 3 + cli/browser-tasks-example.ts | 181 ++++++++++++++ cli/browser-use | 20 ++ cli/browser-use.toolchain.json | 50 ++++ cli/browser_use_cli.py | 233 ++++++++++++++++++ pyproject.toml | 23 ++ pytest.ini | 19 ++ src/__init__.py | 21 +- src/controller/custom_controller.py | 6 + tests/__init__.py | 7 + tests/requirements-test.txt | 4 + tests/test_api.py | 73 ++++++ tests/test_browser_cli.py | 352 ++++++++++++++++++++++++++++ 15 files changed, 1006 insertions(+), 1 deletion(-) create mode 100644 cli/__init__.py create mode 100644 cli/browser-tasks-example.ts create mode 100755 cli/browser-use create mode 100644 cli/browser-use.toolchain.json create mode 100644 cli/browser_use_cli.py create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 tests/__init__.py create mode 100644 tests/requirements-test.txt create mode 100644 tests/test_api.py create mode 100644 tests/test_browser_cli.py diff --git a/.gitignore b/.gitignore index 2d83410f..f8feaed7 100644 --- a/.gitignore +++ b/.gitignore @@ -184,3 +184,12 @@ AgentHistoryList.json # For Docker data/ + +# cursor +.cursorrules +.cursorignore +.backup.env +.brain/** */ + +# Brain directory +.brain/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 8b09300d..58dcb3c6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,11 @@ "source.fixAll.ruff": "explicit", "source.organizeImports.ruff": "explicit" } + }, + "dotenv.enableAutocloaking": false, + "workbench.colorCustomizations": { + "activityBar.background": "#452606", + "titleBar.activeBackground": "#603608", + "titleBar.activeForeground": "#FEFBF7" } } diff --git a/cli/__init__.py b/cli/__init__.py new file mode 100644 index 00000000..d1f449a5 --- /dev/null +++ b/cli/__init__.py @@ -0,0 +1,3 @@ +""" +Command-line interface for browser-use. +""" \ No newline at end of file diff --git a/cli/browser-tasks-example.ts b/cli/browser-tasks-example.ts new file mode 100644 index 00000000..4b1a6936 --- /dev/null +++ b/cli/browser-tasks-example.ts @@ -0,0 +1,181 @@ +/** + * Browser Automation Task Sequences + * + * This file defines task sequences for browser automation using the browser-use command. + * Each sequence represents a series of browser interactions that can be executed in order. + */ + +export interface BrowserCommand { + prompt: string; + model?: 'deepseek-chat' | 'gemini' | 'gpt-4' | 'claude-3'; + headless?: boolean; + vision?: boolean; + keepSessionAlive?: boolean; +} + +export interface BrowserTask { + description: string; + command: BrowserCommand; + subtasks?: BrowserTask[]; +} + +export interface BrowserTaskSequence { + name: string; + description: string; + tasks: BrowserTask[]; +} + +// Example task sequences +export const browserTasks: BrowserTaskSequence[] = [ + { + name: "Product Research", + description: "Compare product prices across multiple e-commerce sites", + tasks: [ + { + description: "Search Amazon for wireless earbuds", + command: { + prompt: "go to amazon.com and search for 'wireless earbuds' and tell me the price of the top 3 results", + model: "gemini", + vision: true, + keepSessionAlive: true + } + }, + { + description: "Search Best Buy for comparison", + command: { + prompt: "go to bestbuy.com and search for 'wireless earbuds' and tell me the price of the top 3 results", + model: "gemini", + vision: true, + keepSessionAlive: true + } + }, + { + description: "Create price comparison", + command: { + prompt: "create a comparison table of the prices from both sites", + keepSessionAlive: false + } + } + ] + }, + { + name: "Site Health Check", + description: "Monitor website availability and performance", + tasks: [ + { + description: "Check main site", + command: { + prompt: "go to example.com and check if it loads properly", + headless: true + } + }, + { + description: "Verify API health", + command: { + prompt: "go to api.example.com/health and tell me the status", + headless: true + } + }, + { + description: "Test documentation site", + command: { + prompt: "go to docs.example.com and verify all navigation links are working", + headless: true + } + } + ] + }, + { + name: "Content Analysis", + description: "Analyze blog content and engagement", + tasks: [ + { + description: "List articles", + command: { + prompt: "go to blog.example.com and list all article titles from the homepage", + model: "gemini", + vision: true + } + }, + { + description: "Analyze first article", + command: { + prompt: "click on the first article and summarize its main points" + }, + subtasks: [ + { + description: "Get metadata", + command: { + prompt: "tell me the author, publication date, and reading time" + } + }, + { + description: "Analyze comments", + command: { + prompt: "scroll to the comments section and summarize the main discussion points", + vision: true + } + } + ] + } + ] + }, + { + name: "Advanced Content Analysis", + description: "Analyze website content using different models for different tasks", + tasks: [ + { + description: "Initial navigation and basic text extraction", + command: { + prompt: "go to docs.github.com and navigate to the Actions documentation", + model: "deepseek-chat", // Use DeepSeek for basic navigation + keepSessionAlive: true + } + }, + { + description: "Visual analysis of page structure", + command: { + prompt: "analyze the layout of the page and tell me how the documentation is structured, including sidebars, navigation, and content areas", + model: "gemini", // Switch to Gemini for visual analysis + vision: true, + keepSessionAlive: true + } + }, + { + description: "Complex content summarization", + command: { + prompt: "summarize the key concepts of GitHub Actions based on the documentation", + model: "claude-3", // Switch to Claude for complex summarization + keepSessionAlive: true + } + }, + { + description: "Extract code examples", + command: { + prompt: "find and list all YAML workflow examples on the page", + model: "deepseek-chat", // Back to DeepSeek for code extraction + keepSessionAlive: false // Close browser after final task + } + } + ] + } +]; + +// Example of executing a task sequence +const executeTask = (task: BrowserCommand): string => { + const options: string[] = []; + if (task.model) options.push(`--model ${task.model}`); + if (task.headless) options.push('--headless'); + if (task.vision) options.push('--vision'); + if (task.keepSessionAlive) options.push('--keep-browser-open'); + + return `browser-use "${task.prompt}" ${options.join(' ')}`.trim(); +}; + +// Example usage: +const sequence = browserTasks[0]; // Get Product Research sequence +console.log(`Executing sequence: ${sequence.name}`); +sequence.tasks.forEach(task => { + console.log(`\n${task.description}:`); + console.log(executeTask(task.command)); +}); \ No newline at end of file diff --git a/cli/browser-use b/cli/browser-use new file mode 100755 index 00000000..6a57e0e4 --- /dev/null +++ b/cli/browser-use @@ -0,0 +1,20 @@ +#!/bin/bash + +# Get the absolute directory of this script +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Add the project directory to PYTHONPATH +export PYTHONPATH="$SCRIPT_DIR:$PYTHONPATH" + +# Activate the virtual environment if it exists +if [ -f "$SCRIPT_DIR/venv/bin/activate" ]; then + source "$SCRIPT_DIR/venv/bin/activate" +fi + +# Run the Python script with all arguments passed through +"$SCRIPT_DIR/venv/bin/python" "$SCRIPT_DIR/browser-use-cli.py" "$@" + +# Deactivate the virtual environment if it was activated +if [ -n "$VIRTUAL_ENV" ]; then + deactivate +fi \ No newline at end of file diff --git a/cli/browser-use.toolchain.json b/cli/browser-use.toolchain.json new file mode 100644 index 00000000..1f3b677c --- /dev/null +++ b/cli/browser-use.toolchain.json @@ -0,0 +1,50 @@ +{ + "name": "browser-use", + "description": "Execute natural language browser automation commands", + "type": "terminal_command", + "functions": [ + { + "name": "browser_command", + "description": "Control a browser using natural language instructions", + "parameters": { + "properties": { + "prompt": { + "type": "string", + "description": "The natural language instruction (e.g., 'go to google.com and search for OpenAI')" + }, + "model": { + "type": "string", + "enum": ["deepseek-chat", "gemini", "gpt-4", "claude-3"], + "default": "deepseek-chat", + "description": "The LLM model to use (optional)" + }, + "headless": { + "type": "boolean", + "default": false, + "description": "Run browser in headless mode (optional)" + }, + "vision": { + "type": "boolean", + "default": false, + "description": "Enable vision capabilities for supported models (optional)" + } + }, + "required": ["prompt"] + } + } + ], + "examples": [ + { + "description": "Basic usage", + "command": "browser-use \"go to google.com and search for OpenAI\"" + }, + { + "description": "Using vision to analyze a webpage", + "command": "browser-use \"go to openai.com and tell me what you see\" --model gemini --vision" + }, + { + "description": "Running a check in headless mode", + "command": "browser-use \"check if github.com is up\" --headless" + } + ] +} \ No newline at end of file diff --git a/cli/browser_use_cli.py b/cli/browser_use_cli.py new file mode 100644 index 00000000..d9cff6a9 --- /dev/null +++ b/cli/browser_use_cli.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +import asyncio +import argparse +import os +import sys +from pathlib import Path +import json +import tempfile + +# Add the project root to PYTHONPATH +project_root = str(Path(__file__).parent.parent) +sys.path.append(project_root) + +from browser_use.browser.browser import Browser, BrowserConfig +from browser_use.browser.context import BrowserContext, BrowserContextConfig, BrowserContextWindowSize +from src.agent.custom_agent import CustomAgent +from src.controller.custom_controller import CustomController +from src.agent.custom_prompts import CustomSystemPrompt +from src.utils import utils +from dotenv import load_dotenv + +# Load .env from the project root +load_dotenv(Path(project_root) / '.env') + +# Global variables for browser persistence +_global_browser = None +_global_browser_context = None + +def _get_browser_state(): + """Get browser state from environment.""" + return os.environ.get("BROWSER_USE_RUNNING", "false").lower() == "true" + +def _set_browser_state(running=True): + """Set browser state in environment.""" + os.environ["BROWSER_USE_RUNNING"] = str(running).lower() + +async def initialize_browser( + headless=False, + window_size=(1920, 1080), + disable_security=False, + user_data_dir=None, + proxy=None +): + """Initialize a new browser instance with the given configuration.""" + global _global_browser, _global_browser_context + + if _get_browser_state(): + print("Browser is already running. Close it first with browser-use close") + return False + + window_w, window_h = window_size + + # Initialize browser with launch-time options + browser = Browser( + config=BrowserConfig( + headless=headless, + disable_security=disable_security, + chrome_instance_path=user_data_dir, + extra_chromium_args=[f"--window-size={window_w},{window_h}"], + proxy=proxy + ) + ) + + # Create initial browser context + browser_context = await browser.new_context( + config=BrowserContextConfig( + no_viewport=False, + browser_window_size=BrowserContextWindowSize( + width=window_w, + height=window_h + ), + disable_security=disable_security + ) + ) + + # Store globally + _global_browser = browser + _global_browser_context = browser_context + _set_browser_state(True) + return True + +async def close_browser(): + """Close the current browser instance if one exists.""" + global _global_browser, _global_browser_context + + if _global_browser_context is not None: + await _global_browser_context.close() + _global_browser_context = None + + if _global_browser is not None: + await _global_browser.close() + _global_browser = None + + _set_browser_state(False) + +async def run_browser_task( + prompt, + model="deepseek-chat", + vision=False, + record=False, + record_path=None, + trace_path=None, + max_steps=10, + max_actions=1, + add_info="" +): + """Execute a task using the current browser instance.""" + global _global_browser, _global_browser_context + + if not _get_browser_state(): + print("No browser session found. Start one with: browser-use start") + return + + if _global_browser is None or _global_browser_context is None: + print("Browser session state is inconsistent. Try closing and restarting the browser.") + return + + # Initialize controller + controller = CustomController() + + # Get LLM model + llm = utils.get_llm_model( + provider="deepseek" if model == "deepseek-chat" else model, + model_name=model, + temperature=0.8 + ) + + # Update context with runtime options if needed + if record or trace_path: + _global_browser_context = await _global_browser.new_context( + config=BrowserContextConfig( + trace_path=trace_path, + save_recording_path=record_path if record else None, + no_viewport=False, + browser_window_size=_global_browser_context.config.browser_window_size, + disable_security=_global_browser_context.config.disable_security + ) + ) + + # Create and run agent + agent = CustomAgent( + task=prompt, + add_infos=add_info, + llm=llm, + browser=_global_browser, + browser_context=_global_browser_context, + controller=controller, + system_prompt_class=CustomSystemPrompt, + use_vision=vision, + tool_call_in_content=True, + max_actions_per_step=max_actions + ) + + try: + history = await agent.run(max_steps=max_steps) + return history.final_result() + except Exception as e: + raise e + +def main(): + parser = argparse.ArgumentParser(description="Control a browser using natural language") + subparsers = parser.add_subparsers(dest="command", help="Commands") + + # Start command - browser initialization + start_parser = subparsers.add_parser("start", help="Start a new browser session") + start_parser.add_argument("--headless", action="store_true", help="Run browser in headless mode") + start_parser.add_argument("--window-size", default="1920x1080", help="Browser window size (WxH)") + start_parser.add_argument("--disable-security", action="store_true", help="Disable browser security features") + start_parser.add_argument("--user-data-dir", help="Use custom Chrome profile directory") + start_parser.add_argument("--proxy", help="Proxy server URL") + + # Run command - task execution + run_parser = subparsers.add_parser("run", help="Run a task in the current browser session") + run_parser.add_argument("prompt", help="The task to perform") + run_parser.add_argument("--model", "-m", choices=["deepseek-chat", "gemini", "gpt-4", "claude-3"], + default="deepseek-chat", help="The LLM model to use") + run_parser.add_argument("--vision", action="store_true", help="Enable vision capabilities") + run_parser.add_argument("--record", action="store_true", help="Enable session recording") + run_parser.add_argument("--record-path", default="./tmp/record_videos", help="Path to save recordings") + run_parser.add_argument("--trace-path", default="./tmp/traces", help="Path to save debugging traces") + run_parser.add_argument("--max-steps", type=int, default=10, help="Maximum number of steps per task") + run_parser.add_argument("--max-actions", type=int, default=1, help="Maximum actions per step") + run_parser.add_argument("--add-info", help="Additional context for the agent") + + # Close command - cleanup + subparsers.add_parser("close", help="Close the current browser session") + + args = parser.parse_args() + + if args.command == "start": + # Parse window size + try: + window_w, window_h = map(int, args.window_size.split('x')) + except ValueError: + print(f"Invalid window size format: {args.window_size}. Using default 1920x1080") + window_w, window_h = 1920, 1080 + + # Start browser + success = asyncio.run(initialize_browser( + headless=args.headless, + window_size=(window_w, window_h), + disable_security=args.disable_security, + user_data_dir=args.user_data_dir, + proxy=args.proxy + )) + if success: + print("Browser session started successfully") + + elif args.command == "run": + # Run task + result = asyncio.run(run_browser_task( + prompt=args.prompt, + model=args.model, + vision=args.vision, + record=args.record, + record_path=args.record_path if args.record else None, + trace_path=args.trace_path, + max_steps=args.max_steps, + max_actions=args.max_actions, + add_info=args.add_info + )) + print(result) + + elif args.command == "close": + # Close browser + asyncio.run(close_browser()) + print("Browser session closed") + + else: + parser.print_help() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..c260ce12 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "browser-use" +version = "0.1.19" +authors = [ + { name = "Your Name", email = "your.email@example.com" } +] +description = "A Python package for browser automation with AI" +readme = "README.md" +requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[tool.setuptools.packages.find] +where = ["."] +include = ["src*"] +namespaces = false \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..88001be3 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,19 @@ +[pytest] +asyncio_mode = auto +asyncio_default_fixture_loop_scope = function + +# Test discovery +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* + +# Output configuration +console_output_style = count +log_cli = True +log_cli_level = INFO + +# Warnings +filterwarnings = + ignore::DeprecationWarning + ignore::pytest.PytestDeprecationWarning \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py index 93fbe7f8..0edfbf30 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -3,4 +3,23 @@ # @Author : wenshao # @Email : wenshaoguo1026@gmail.com # @Project : browser-use-webui -# @FileName: __init__.py.py +# @FileName: __init__.py + +from browser_use.browser.browser import Browser +from browser_use.browser.browser import BrowserConfig +from browser_use.browser.context import BrowserContextConfig, BrowserContextWindowSize +from .agent.custom_agent import CustomAgent +from .controller.custom_controller import CustomController +from .agent.custom_prompts import CustomSystemPrompt +from .utils import utils + +__all__ = [ + 'Browser', + 'BrowserConfig', + 'BrowserContextConfig', + 'BrowserContextWindowSize', + 'CustomAgent', + 'CustomController', + 'CustomSystemPrompt', + 'utils' +] diff --git a/src/controller/custom_controller.py b/src/controller/custom_controller.py index 6e57dd4a..21a56b5a 100644 --- a/src/controller/custom_controller.py +++ b/src/controller/custom_controller.py @@ -8,6 +8,7 @@ from browser_use.agent.views import ActionResult from browser_use.browser.context import BrowserContext from browser_use.controller.service import Controller +from browser_use.browser.views import BrowserState class CustomController(Controller): @@ -31,3 +32,8 @@ async def paste_from_clipboard(browser: BrowserContext): await page.keyboard.type(text) return ActionResult(extracted_content=text) + + async def get_browser_state(self, browser_context: BrowserContext) -> BrowserState: + """Get the current state of the browser""" + state = await browser_context.get_state(use_vision=True) + return state diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..c74cef78 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,7 @@ +""" +Test suite for the browser-use project. + +This package contains tests for: +- Browser automation (CLI, core functionality, Playwright) +- API integration (endpoints, LLM integration) +""" \ No newline at end of file diff --git a/tests/requirements-test.txt b/tests/requirements-test.txt new file mode 100644 index 00000000..ecae0c67 --- /dev/null +++ b/tests/requirements-test.txt @@ -0,0 +1,4 @@ +pytest>=7.0.0 +pytest-asyncio>=0.21.0 +pytest-timeout>=2.1.0 +pytest-cov>=4.0.0 \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 00000000..5dc4fae7 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,73 @@ +import asyncio +from browser_use.browser.browser import Browser, BrowserConfig +from browser_use.browser.context import BrowserContextConfig, BrowserContextWindowSize +from browser_use.agent.service import Agent +from src.utils import utils +from src.controller.custom_controller import CustomController +from src.agent.custom_agent import CustomAgent +from src.agent.custom_prompts import CustomSystemPrompt +import os + +async def main(): + window_w, window_h = 1920, 1080 + + # Initialize the browser + browser = Browser( + config=BrowserConfig( + headless=False, + disable_security=True, + extra_chromium_args=[f"--window-size={window_w},{window_h}"], + ) + ) + + # Create a browser context + async with await browser.new_context( + config=BrowserContextConfig( + trace_path="./tmp/traces", + save_recording_path="./tmp/record_videos", + no_viewport=False, + browser_window_size=BrowserContextWindowSize( + width=window_w, height=window_h + ), + ) + ) as browser_context: + # Initialize the controller + controller = CustomController() + + # Initialize the agent with a simple task using CustomAgent + agent = CustomAgent( + task="go to google.com and search for 'OpenAI'", + add_infos="", # hints for the LLM if needed + llm=utils.get_llm_model( + provider="deepseek", + model_name="deepseek-chat", # Using V2.5 via deepseek-chat endpoint + temperature=0.8, + base_url="https://api.deepseek.com/v1", + api_key=os.getenv("DEEPSEEK_API_KEY", "") + ), + browser=browser, + browser_context=browser_context, + controller=controller, + system_prompt_class=CustomSystemPrompt, + use_vision=False, # Must be False for DeepSeek + tool_call_in_content=True, # Required for DeepSeek as per test files + max_actions_per_step=1 # Control granularity of actions + ) + + # Run the agent + history = await agent.run(max_steps=10) + + print("Final Result:") + print(history.final_result()) + + print("\nErrors:") + print(history.errors()) + + print("\nModel Actions:") + print(history.model_actions()) + + print("\nThoughts:") + print(history.model_thoughts()) + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/tests/test_browser_cli.py b/tests/test_browser_cli.py new file mode 100644 index 00000000..57c91756 --- /dev/null +++ b/tests/test_browser_cli.py @@ -0,0 +1,352 @@ +import sys +from pathlib import Path +import tempfile +import logging + +# Add project root to Python path +PROJECT_ROOT = Path(__file__).parent.parent +sys.path.insert(0, str(PROJECT_ROOT)) + +import pytest +import asyncio +import os +from cli.browser_use_cli import initialize_browser, run_browser_task, close_browser, main, _global_browser, _global_browser_context + +# Configure logging for tests +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Reset global state before each test +@pytest.fixture(autouse=True) +async def cleanup(): + """Ensure proper cleanup of browser and event loop between tests""" + global _global_browser, _global_browser_context + + logger.info(f"Cleanup start - Browser state: {_global_browser is not None}") + + # Reset globals before test + _global_browser = None + _global_browser_context = None + + logger.info("Globals reset before test") + + try: + yield + finally: + try: + logger.info(f"Cleanup finally - Browser state: {_global_browser is not None}") + if _global_browser is not None: + await close_browser() + logger.info("Browser closed") + # Clean up any remaining event loop resources + loop = asyncio.get_event_loop() + tasks = [t for t in asyncio.all_tasks(loop=loop) if not t.done()] + if tasks: + logger.info(f"Found {len(tasks)} pending tasks") + for task in tasks: + if not task.cancelled(): + task.cancel() + logger.info("Tasks cancelled") + except Exception as e: + logger.error(f"Error during cleanup: {e}") + finally: + # Always reset globals after test + _global_browser = None + _global_browser_context = None + logger.info("Globals reset after test") + +class TestBrowserInitialization: + """Test browser launch-time options""" + + async def test_basic_initialization(self): + """Test basic browser initialization with defaults""" + success = await initialize_browser() + assert success is True + + async def test_window_size(self): + """Test custom window size""" + success = await initialize_browser(window_size=(800, 600)) + assert success is True + + # Create a simple HTML page that displays window size + result = await run_browser_task( + "go to data:text/html,", + model="deepseek-chat" + ) + assert result is not None and "800" in result.lower() and "600" in result.lower() + + async def test_headless_mode(self): + """Test headless mode""" + success = await initialize_browser(headless=True) + assert success is True + # Verify we can still run tasks + result = await run_browser_task( + "go to example.com and tell me the title", + model="deepseek-chat" + ) + assert result is not None and "example" in result.lower() + + async def test_user_data_dir(self, tmp_path): + """Test custom user data directory""" + user_data = tmp_path / "chrome_data" + user_data.mkdir() + success = await initialize_browser(user_data_dir=str(user_data)) + assert success is True + assert user_data.exists() + + async def test_proxy_configuration(self): + """Test proxy configuration""" + # Using a test proxy - in practice you'd use a real proxy server + test_proxy = "localhost:8080" + success = await initialize_browser(proxy=test_proxy) + assert success is True + + @pytest.mark.timeout(30) # Add 30 second timeout + async def test_disable_security(self): + """Test security disable option""" + success = await initialize_browser(disable_security=True) + assert success is True + # Try accessing a cross-origin resource that would normally be blocked + result = await run_browser_task( + "go to a test page and try to access cross-origin content", + model="deepseek-chat", + max_steps=5 # Limit steps to prevent timeout + ) + assert result is not None and "error" not in result.lower() + + async def test_multiple_initialization(self): + """Test that second initialization fails while browser is running""" + success1 = await initialize_browser() + assert success1 is True + success2 = await initialize_browser() + assert success2 is False + +class TestBrowserTasks: + """Test runtime task options""" + + @pytest.fixture(autouse=True) + async def setup_browser(self): + """Start browser before each test""" + await initialize_browser() + yield + + @pytest.fixture + def local_test_page(self): + """Create a local HTML file for testing""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as f: + f.write(""" + + + + Test Page + + +

Test Content

+

This is a test paragraph with specific content.

+ +
+ + + """) + return f.name + + async def test_model_switching(self): + """Test switching between different LLM models""" + # Test DeepSeek - Note: 422 errors are expected but don't affect functionality + try: + result1 = await run_browser_task( + "go to example.com and summarize the page", + model="deepseek-chat" + ) + assert result1 is not None + except Exception as e: + if "422" not in str(e): # Only ignore 422 errors + raise + + # Test Gemini + result2 = await run_browser_task( + "what do you see on the page?", + model="gemini", + vision=True + ) + assert result2 is not None and len(result2) > 0 + assert result1 is not None and len(result1) > 0 + assert result1 != result2 # Different models should give different responses + + async def test_vision_capability(self): + """Test vision capabilities""" + # Without vision + result1 = await run_browser_task( + "what do you see on example.com?", + model="gemini", + vision=False + ) + + # With vision + result2 = await run_browser_task( + "what do you see on example.com?", + model="gemini", + vision=True + ) + + assert result1 is not None and result2 is not None and len(result2) > len(result1) # Vision should provide more details + + async def test_recording(self, tmp_path): + """Test session recording""" + record_path = tmp_path / "recordings" + record_path.mkdir() + + await run_browser_task( + "go to example.com", + record=True, + record_path=str(record_path) + ) + + # Check that recording file was created + recordings = list(record_path.glob("*.webm")) + assert len(recordings) > 0 + + async def test_tracing(self, tmp_path): + """Test debug tracing""" + trace_path = tmp_path / "traces" + trace_path.mkdir() + + await run_browser_task( + "go to example.com", + trace_path=str(trace_path) + ) + + # Check that trace file was created + traces = list(trace_path.glob("*.zip")) + assert len(traces) > 0 + + async def test_max_steps_limit(self): + """Test max steps limitation""" + with pytest.raises(Exception): + # This task would normally take more than 2 steps + await run_browser_task( + "go to google.com, search for 'OpenAI', click first result", + max_steps=2 + ) + + async def test_max_actions_limit(self): + """Test max actions per step limitation""" + with pytest.raises(Exception): + # This would require multiple actions in one step + await run_browser_task( + "go to google.com and click all search results", + max_actions=1 + ) + + async def test_additional_context(self): + """Test providing additional context""" + result = await run_browser_task( + "summarize the content", + add_info="Focus on technical details and pricing information" + ) + assert result is not None and ("technical" in result.lower() or "pricing" in result.lower()) + + async def test_report_generation(self, local_test_page): + """Test that the agent can analyze a page and return a report""" + logger.info("Starting report generation test") + + # Check initial state + logger.info(f"Initial browser state: {_global_browser is not None}") + + # Initialize browser + success = await initialize_browser() + logger.info(f"Browser initialization result: {success}") + + assert success is True, "Browser initialization failed" + + # Create the task prompt + prompt = f"Go to file://{local_test_page} and create a report about the page structure, including any interactive elements found" + + try: + result = await run_browser_task( + prompt=prompt, + model="deepseek-chat", + max_steps=3 + ) + + logger.info(f"Received report: {result}") + + # Verify the report contains expected information + assert result is not None + assert "Test Content" in result + assert "button" in result.lower() + assert "paragraph" in result.lower() + + logger.info("Report verification successful") + + except Exception as e: + logger.error(f"Error during report generation: {e}") + raise + finally: + # Cleanup + os.unlink(local_test_page) + logger.info("Test cleanup completed") + +class TestBrowserLifecycle: + """Test browser lifecycle management""" + + async def test_close_and_reopen(self): + """Test closing and reopening browser""" + # First session + success1 = await initialize_browser() + assert success1 is True + result1 = await run_browser_task("go to example.com") + assert result1 is not None + await close_browser() + + # Second session + success2 = await initialize_browser() + assert success2 is True + result2 = await run_browser_task("go to example.com") + assert result2 is not None + + async def test_error_handling(self): + """Test error handling in various scenarios""" + # Test running task without browser + with pytest.raises(Exception): + await run_browser_task("this should fail") + + # Test closing already closed browser + await close_browser() + await close_browser() # Should not raise error + + # Test recovery after error + success = await initialize_browser() + assert success is True + result = await run_browser_task("go to example.com") + assert result is not None + +def test_cli_commands(): + """Test CLI command parsing""" + import sys + from io import StringIO + + # Test start command + output = StringIO() + sys.stdout = output + sys.argv = ["browser-use", "start", "--window-size", "800x600"] + from cli.browser_use_cli import main + main() + assert "Browser session started successfully" in output.getvalue() + + # Test run command + output = StringIO() + sys.stdout = output + sys.argv = ["browser-use", "run", "go to example.com", "--model", "deepseek-chat"] + main() + assert len(output.getvalue()) > 0 + + # Test close command + output = StringIO() + sys.stdout = output + sys.argv = ["browser-use", "close"] + main() + assert "Browser session closed" in output.getvalue() + + # Restore stdout + sys.stdout = sys.__stdout__ \ No newline at end of file From c5a5d7949c414495f41fe429c763be682605dbb6 Mon Sep 17 00:00:00 2001 From: David Mieloch Date: Sun, 19 Jan 2025 16:31:30 -0500 Subject: [PATCH 02/10] Enhance Google Gemini integration and environment configuration - Added support for Google Gemini API in `test_gemini_connection.py`, including model validation and content generation. - Updated `.gitignore` to include `.env.google` for environment variable management. - Modified `browser_use_cli.py` to dynamically set the model name from environment variables. - Improved `get_llm_model` function in `utils.py` to handle model names and API keys more securely using `SecretStr`. - Updated tests in `test_browser_cli.py` to set the Google model via environment variable for better test isolation. --- .gitignore | 2 ++ cli/browser_use_cli.py | 5 +++-- src/utils/utils.py | 42 +++++++++++++++++++++++----------- test_gemini_connection.py | 47 +++++++++++++++++++++++++++++++++++++++ tests/test_browser_cli.py | 5 +++++ 5 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 test_gemini_connection.py diff --git a/.gitignore b/.gitignore index f8feaed7..4045b9c5 100644 --- a/.gitignore +++ b/.gitignore @@ -193,3 +193,5 @@ data/ # Brain directory .brain/ + +.env.google diff --git a/cli/browser_use_cli.py b/cli/browser_use_cli.py index d9cff6a9..9d5aa092 100644 --- a/cli/browser_use_cli.py +++ b/cli/browser_use_cli.py @@ -121,8 +121,9 @@ async def run_browser_task( # Get LLM model llm = utils.get_llm_model( provider="deepseek" if model == "deepseek-chat" else model, - model_name=model, - temperature=0.8 + model_name=os.getenv("GOOGLE_API_MODEL", "gemini-pro-vision") if model == "gemini" else model, + temperature=0.8, + vision=vision ) # Update context with runtime options if needed diff --git a/src/utils/utils.py b/src/utils/utils.py index 3ab38977..58d213d3 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -9,6 +9,7 @@ import time from pathlib import Path from typing import Dict, Optional +from pydantic import SecretStr from langchain_anthropic import ChatAnthropic from langchain_google_genai import ChatGoogleGenerativeAI @@ -30,15 +31,17 @@ def get_llm_model(provider: str, **kwargs): base_url = kwargs.get("base_url") if not kwargs.get("api_key", ""): - api_key = os.getenv("ANTHROPIC_API_KEY", "") + api_key = SecretStr(os.getenv("ANTHROPIC_API_KEY") or "") else: - api_key = kwargs.get("api_key") + api_key = SecretStr(kwargs.get("api_key") or "") return ChatAnthropic( model_name=kwargs.get("model_name", "claude-3-5-sonnet-20240620"), temperature=kwargs.get("temperature", 0.0), base_url=base_url, api_key=api_key, + timeout=kwargs.get("timeout", 60), + stop=kwargs.get("stop", None) ) elif provider == "openai": if not kwargs.get("base_url", ""): @@ -47,15 +50,16 @@ def get_llm_model(provider: str, **kwargs): base_url = kwargs.get("base_url") if not kwargs.get("api_key", ""): - api_key = os.getenv("OPENAI_API_KEY", "") + api_key = SecretStr(os.getenv("OPENAI_API_KEY") or "") else: - api_key = kwargs.get("api_key") + api_key = SecretStr(kwargs.get("api_key") or "") return ChatOpenAI( - model=kwargs.get("model_name", "gpt-4o"), + model=kwargs.get("model_name", "gpt-4"), temperature=kwargs.get("temperature", 0.0), base_url=base_url, api_key=api_key, + timeout=kwargs.get("timeout", 60), ) elif provider == "deepseek": if not kwargs.get("base_url", ""): @@ -64,25 +68,37 @@ def get_llm_model(provider: str, **kwargs): base_url = kwargs.get("base_url") if not kwargs.get("api_key", ""): - api_key = os.getenv("DEEPSEEK_API_KEY", "") + api_key = SecretStr(os.getenv("DEEPSEEK_API_KEY") or "") else: - api_key = kwargs.get("api_key") + api_key = SecretStr(kwargs.get("api_key") or "") return ChatOpenAI( model=kwargs.get("model_name", "deepseek-chat"), temperature=kwargs.get("temperature", 0.0), base_url=base_url, api_key=api_key, + timeout=kwargs.get("timeout", 60), ) elif provider == "gemini": if not kwargs.get("api_key", ""): - api_key = os.getenv("GOOGLE_API_KEY", "") + api_key = SecretStr(os.getenv("GOOGLE_API_KEY") or "") else: - api_key = kwargs.get("api_key") + api_key = SecretStr(kwargs.get("api_key") or "") + + # Get model name from environment or kwargs + model_name = kwargs.get("model_name") + if not model_name: + if kwargs.get("vision"): + model_name = os.getenv("GOOGLE_API_MODEL", "gemini-1.5-flash") + else: + model_name = os.getenv("GOOGLE_API_TYPE", "gemini-1.5-flash") + return ChatGoogleGenerativeAI( - model=kwargs.get("model_name", "gemini-2.0-flash-exp"), + model=model_name, temperature=kwargs.get("temperature", 0.0), - google_api_key=api_key, + api_key=api_key, + timeout=kwargs.get("timeout", 60), + convert_system_message_to_human=True ) elif provider == "ollama": return ChatOllama( @@ -97,9 +113,9 @@ def get_llm_model(provider: str, **kwargs): else: base_url = kwargs.get("base_url") if not kwargs.get("api_key", ""): - api_key = os.getenv("AZURE_OPENAI_API_KEY", "") + api_key = SecretStr(os.getenv("AZURE_OPENAI_API_KEY") or "") else: - api_key = kwargs.get("api_key") + api_key = SecretStr(kwargs.get("api_key") or "") return AzureChatOpenAI( model=kwargs.get("model_name", "gpt-4o"), temperature=kwargs.get("temperature", 0.0), diff --git a/test_gemini_connection.py b/test_gemini_connection.py new file mode 100644 index 00000000..0feeecad --- /dev/null +++ b/test_gemini_connection.py @@ -0,0 +1,47 @@ +import google.generativeai as genai +import os +from dotenv import load_dotenv, find_dotenv + +# Force reload of environment variables +load_dotenv(find_dotenv(), override=True) + +api_key = os.environ.get("GOOGLE_API_KEY") +model_name = os.environ.get("GOOGLE_API_MODEL") + +if not api_key or not model_name: + raise ValueError("Missing required environment variables: GOOGLE_API_KEY or GOOGLE_API_MODEL") + +print(f"Using model: {model_name}") +genai.configure(api_key=api_key, transport="rest") + +# List all available models +print("\nAvailable models:") +for m in genai.list_models(): + print(f"- {m.name}") + +# Check that the model exists in the client +found_model = False +for m in genai.list_models(): + model_id = m.name.replace("models/", "") + if model_id == model_name: + found_model = True + print(f"\nFound model: {m.name}") + break + +if not found_model: + print("\nAvailable model IDs:") + for m in genai.list_models(): + print(f"- {m.name.replace('models/', '')}") + +assert found_model, f"Model not found: {model_name}" + +# Load the model +model = genai.GenerativeModel(model_name) + +# Perform a simple generation task +try: + response = model.generate_content("Hello, I'm testing the Gemini API connection. Please respond with a short greeting.") + print(f"\nResponse: {response.text}") +except Exception as e: + print(f"\nError generating content: {e}") + raise \ No newline at end of file diff --git a/tests/test_browser_cli.py b/tests/test_browser_cli.py index 57c91756..0e7c1552 100644 --- a/tests/test_browser_cli.py +++ b/tests/test_browser_cli.py @@ -11,6 +11,7 @@ import asyncio import os from cli.browser_use_cli import initialize_browser, run_browser_task, close_browser, main, _global_browser, _global_browser_context +from src.utils.utils import model_names # Import model names from utils # Configure logging for tests logging.basicConfig(level=logging.INFO) @@ -164,6 +165,7 @@ async def test_model_switching(self): raise # Test Gemini + os.environ["GOOGLE_API_MODEL"] = model_names["gemini"][0] # Set model via environment result2 = await run_browser_task( "what do you see on the page?", model="gemini", @@ -175,6 +177,9 @@ async def test_model_switching(self): async def test_vision_capability(self): """Test vision capabilities""" + # Set Gemini model via environment + os.environ["GOOGLE_API_MODEL"] = model_names["gemini"][0] + # Without vision result1 = await run_browser_task( "what do you see on example.com?", From c8ad372e1206cc6b6c893a8071f81e958122f870 Mon Sep 17 00:00:00 2001 From: David Mieloch Date: Sun, 19 Jan 2025 18:09:51 -0500 Subject: [PATCH 03/10] Add browser tracing and debugging features with enhanced CLI support - Introduced a new `pytest_output.txt` file to capture test session logs. - Updated `browser_use_cli.py` to manage browser state more effectively and support tracing options. - Enhanced `browser-tasks-example.ts` with new tasks for page structure analysis and debugging sessions. - Modified `custom_prompts.py` to ensure detailed reporting on page structure. - Improved `custom_context.py` to handle tracing and context management. - Expanded tests in `test_browser_cli.py` to cover new CLI commands and tracing functionalities. --- cli/browser-tasks-example.ts | 69 ++++++++ cli/browser_use_cli.py | 76 ++++++--- pytest_output.txt | 64 +++++++ src/agent/custom_prompts.py | 11 +- src/browser/custom_context.py | 18 ++ tests/test_browser_cli.py | 304 ++++++++++++++++++++++++++++++---- 6 files changed, 479 insertions(+), 63 deletions(-) create mode 100644 pytest_output.txt diff --git a/cli/browser-tasks-example.ts b/cli/browser-tasks-example.ts index 4b1a6936..d0acca18 100644 --- a/cli/browser-tasks-example.ts +++ b/cli/browser-tasks-example.ts @@ -11,6 +11,7 @@ export interface BrowserCommand { headless?: boolean; vision?: boolean; keepSessionAlive?: boolean; + trace?: boolean; } export interface BrowserTask { @@ -158,6 +159,73 @@ export const browserTasks: BrowserTaskSequence[] = [ } } ] + }, + { + name: "Page Structure Analysis", + description: "Generate detailed reports about page structure and interactive elements", + tasks: [ + { + description: "Analyze homepage structure", + command: { + prompt: "go to example.com and create a report about the page structure, including the page title, headings, and any interactive elements found", + model: "gemini", + vision: true, + keepSessionAlive: true + } + }, + { + description: "Analyze navigation structure", + command: { + prompt: "focus on the navigation menu and create a detailed report of its structure and all available links", + model: "gemini", + vision: true, + keepSessionAlive: true + } + }, + { + description: "Document forms and inputs", + command: { + prompt: "find all forms on the page and document their inputs, buttons, and validation requirements", + model: "gemini", + vision: true, + keepSessionAlive: false + } + } + ] + }, + { + name: "Debug Session", + description: "Record and analyze browser interactions for debugging", + tasks: [ + { + description: "Start debug session", + command: { + prompt: "go to example.com/login and attempt to log in with test credentials", + model: "deepseek-chat", + headless: false, + keepSessionAlive: true, + trace: true + } + }, + { + description: "Navigate complex workflow", + command: { + prompt: "complete the multi-step registration process", + model: "deepseek-chat", + keepSessionAlive: true, + trace: true + } + }, + { + description: "Generate debug report", + command: { + prompt: "create a report of all actions taken and any errors encountered", + model: "claude-3", + keepSessionAlive: false, + trace: true + } + } + ] } ]; @@ -168,6 +236,7 @@ const executeTask = (task: BrowserCommand): string => { if (task.headless) options.push('--headless'); if (task.vision) options.push('--vision'); if (task.keepSessionAlive) options.push('--keep-browser-open'); + if (task.trace) options.push('--trace-path ./tmp/traces/trace.zip'); return `browser-use "${task.prompt}" ${options.join(' ')}`.trim(); }; diff --git a/cli/browser_use_cli.py b/cli/browser_use_cli.py index 9d5aa092..42c176f4 100644 --- a/cli/browser_use_cli.py +++ b/cli/browser_use_cli.py @@ -44,9 +44,13 @@ async def initialize_browser( """Initialize a new browser instance with the given configuration.""" global _global_browser, _global_browser_context - if _get_browser_state(): - print("Browser is already running. Close it first with browser-use close") - return False + # Check both environment and global variables + if _get_browser_state() or _global_browser is not None: + # Close any existing browser first + if _global_browser is not None: + await close_browser() + else: + _set_browser_state(False) window_w, window_h = window_size @@ -126,36 +130,62 @@ async def run_browser_task( vision=vision ) - # Update context with runtime options if needed + # Create new context with tracing/recording enabled if record or trace_path: + # Close existing context first + if _global_browser_context is not None: + await _global_browser_context.close() + + # Create new context with tracing/recording enabled + if trace_path: + trace_dir = Path(trace_path) + trace_dir.mkdir(parents=True, exist_ok=True) + trace_file = str(trace_dir / "trace.zip") + else: + trace_file = None + _global_browser_context = await _global_browser.new_context( config=BrowserContextConfig( - trace_path=trace_path, - save_recording_path=record_path if record else None, + trace_path=trace_file, + save_recording_path=str(record_path) if record else None, no_viewport=False, - browser_window_size=_global_browser_context.config.browser_window_size, - disable_security=_global_browser_context.config.disable_security + browser_window_size=BrowserContextWindowSize( + width=1920, + height=1080 + ), + disable_security=False ) ) - # Create and run agent - agent = CustomAgent( - task=prompt, - add_infos=add_info, - llm=llm, - browser=_global_browser, - browser_context=_global_browser_context, - controller=controller, - system_prompt_class=CustomSystemPrompt, - use_vision=vision, - tool_call_in_content=True, - max_actions_per_step=max_actions - ) - try: + # Create and run agent + agent = CustomAgent( + task=prompt, + add_infos=add_info, + llm=llm, + browser=_global_browser, + browser_context=_global_browser_context, + controller=controller, + system_prompt_class=CustomSystemPrompt, + use_vision=vision, + tool_call_in_content=True, + max_actions_per_step=max_actions + ) + history = await agent.run(max_steps=max_steps) - return history.final_result() + result = history.final_result() + + # Close the context if tracing was enabled + if trace_path: + await _global_browser_context.close() + _global_browser_context = None + + return result except Exception as e: + # Close the context if tracing was enabled + if trace_path: + await _global_browser_context.close() + _global_browser_context = None raise e def main(): diff --git a/pytest_output.txt b/pytest_output.txt new file mode 100644 index 00000000..fe9b67ce --- /dev/null +++ b/pytest_output.txt @@ -0,0 +1,64 @@ +============================= test session starts ============================== +platform darwin -- Python 3.11.9, pytest-8.3.4, pluggy-1.5.0 -- /Users/dmieloch/Dev/experiments/web-ui/venv/bin/python +cachedir: .pytest_cache +rootdir: /Users/dmieloch/Dev/experiments/web-ui +configfile: pytest.ini +plugins: cov-6.0.0, asyncio-0.25.2, anyio-4.8.0, timeout-2.3.1 +asyncio: mode=Mode.AUTO, asyncio_default_fixture_loop_scope=function +collecting ... +----------------------------- live log collection ------------------------------ +INFO root:service.py:51 Anonymized telemetry enabled. See https://github.com/gregpr07/browser-use for more information. +INFO httpx:_client.py:1038 HTTP Request: GET https://api.gradio.app/gradio-messaging/en "HTTP/1.1 200 OK" +collected 28 items + +tests/test_browser_cli.py::TestBrowserInitialization::test_basic_initialization +-------------------------------- live log setup -------------------------------- +INFO tests.test_browser_cli:test_browser_cli.py:28 Cleanup start - Browser state: False +INFO tests.test_browser_cli:test_browser_cli.py:39 Globals and environment reset before test +PASSED [ 1/28] +------------------------------ live log teardown ------------------------------- +INFO tests.test_browser_cli:test_browser_cli.py:45 Cleanup finally - Browser state: False +INFO tests.test_browser_cli:test_browser_cli.py:65 Globals and environment reset after test + +tests/test_browser_cli.py::TestBrowserInitialization::test_window_size +-------------------------------- live log setup -------------------------------- +INFO tests.test_browser_cli:test_browser_cli.py:28 Cleanup start - Browser state: False +INFO tests.test_browser_cli:test_browser_cli.py:39 Globals and environment reset before test +-------------------------------- live log call --------------------------------- +INFO src.agent.custom_agent:custom_agent.py:356 🚀 Starting task: go to data:text/html, +INFO src.agent.custom_agent:custom_agent.py:196 +📍 Step 1 +INFO httpx:_client.py:1786 HTTP Request: POST https://api.deepseek.com/v1/chat/completions "HTTP/1.1 422 Unprocessable Entity" +INFO httpx:_client.py:1038 HTTP Request: POST https://api.deepseek.com/v1/chat/completions "HTTP/1.1 200 OK" +INFO src.agent.custom_agent:custom_agent.py:128 🤷 Eval: Unknown - No previous actions to evaluate. +INFO src.agent.custom_agent:custom_agent.py:129 🧠 New Memory: +INFO src.agent.custom_agent:custom_agent.py:130 ⏳ Task Progress: +INFO src.agent.custom_agent:custom_agent.py:131 🤔 Thought: The task requires navigating to a specific URL to display the window size. The current page is 'about:blank', and no actions have been taken yet. +INFO src.agent.custom_agent:custom_agent.py:132 🎯 Summary: Navigate to the specified URL to display the window size. +INFO src.agent.custom_agent:custom_agent.py:134 🛠️ Action 1/1: {"go_to_url":{"url":"data:text/html,"}} +INFO src.agent.custom_agent:custom_agent.py:207 🧠 All Memory: +INFO browser_use.controller.service:service.py:59 🔗 Navigated to data:text/html, +INFO src.agent.custom_agent:custom_agent.py:196 +📍 Step 2 +INFO httpx:_client.py:1786 HTTP Request: POST https://api.deepseek.com/v1/chat/completions "HTTP/1.1 422 Unprocessable Entity" +INFO httpx:_client.py:1038 HTTP Request: POST https://api.deepseek.com/v1/chat/completions "HTTP/1.1 200 OK" +INFO src.agent.custom_agent:custom_agent.py:128 ✅ Eval: Success - Successfully navigated to the specified URL and displayed the window size. +INFO src.agent.custom_agent:custom_agent.py:129 🧠 New Memory: Window size: 800x600 +INFO src.agent.custom_agent:custom_agent.py:130 ⏳ Task Progress: 1. Navigated to the specified URL to display the window size. +INFO src.agent.custom_agent:custom_agent.py:131 🤔 Thought: The task has been completed as the window size is now displayed on the page. No further actions are required. +INFO src.agent.custom_agent:custom_agent.py:132 🎯 Summary: The task is complete. The window size is displayed as 800x600. +INFO src.agent.custom_agent:custom_agent.py:134 🛠️ Action 1/1: {"done":{"text":"The task is complete. The window size is displayed as 800x600."}} +INFO src.agent.custom_agent:custom_agent.py:207 🧠 All Memory: Window size: 800x600 + +INFO src.agent.custom_agent:custom_agent.py:218 📄 Result: The task is complete. The window size is displayed as 800x600. +INFO src.agent.custom_agent:custom_agent.py:399 ✅ Task completed successfully +WARNING src.agent.custom_agent:custom_agent.py:260 No history or first screenshot to create GIF from +PASSED [ 2/28] +------------------------------ live log teardown ------------------------------- +INFO tests.test_browser_cli:test_browser_cli.py:45 Cleanup finally - Browser state: False +INFO tests.test_browser_cli:test_browser_cli.py:65 Globals and environment reset after test + +tests/test_browser_cli.py::TestBrowserInitialization::test_headless_mode +-------------------------------- live log setup -------------------------------- +INFO tests.test_browser_cli:test_browser_cli.py:28 Cleanup start - Browser state: False +INFO tests.test_browser_cli:test_browser_cli.py:39 Globals and environment reset before test diff --git a/src/agent/custom_prompts.py b/src/agent/custom_prompts.py index 56aeb64b..b64b3b9f 100644 --- a/src/agent/custom_prompts.py +++ b/src/agent/custom_prompts.py @@ -66,9 +66,10 @@ def important_rules(self) -> str: - Use scroll to find elements you are looking for 5. TASK COMPLETION: - - If you think all the requirements of user\'s instruction have been completed and no further operation is required, output the done action to terminate the operation process. + - If you think all the requirements of user's instruction have been completed and no further operation is required, output the done action to terminate the operation process. - Don't hallucinate actions. - If the task requires specific information - make sure to include everything in the done function. This is what the user will see. + - When generating reports about page structure, always include the page title and headings. - If you are running out of steps (current step), think about speeding it up, and ALWAYS use the done action as the last action. 6. VISUAL CONTEXT: @@ -163,13 +164,13 @@ def __init__( def get_user_message(self) -> HumanMessage: state_description = f""" - 1. Task: {self.step_info.task} + 1. Task: {self.step_info.task if self.step_info else ""} 2. Hints(Optional): - {self.step_info.add_infos} + {self.step_info.add_infos if self.step_info else ""} 3. Memory: - {self.step_info.memory} + {self.step_info.memory if self.step_info else ""} 4. Task Progress: - {self.step_info.task_progress} + {self.step_info.task_progress if self.step_info else ""} 5. Current url: {self.state.url} 6. Available tabs: {self.state.tabs} diff --git a/src/browser/custom_context.py b/src/browser/custom_context.py index 6de991bf..c0aa1961 100644 --- a/src/browser/custom_context.py +++ b/src/browser/custom_context.py @@ -8,6 +8,7 @@ import json import logging import os +from pathlib import Path from browser_use.browser.browser import Browser from browser_use.browser.context import BrowserContext, BrowserContextConfig @@ -25,6 +26,7 @@ def __init__( config: BrowserContextConfig = BrowserContextConfig() ): super(CustomBrowserContext, self).__init__(browser=browser, config=config) + self._context = None async def _create_context(self, browser: PlaywrightBrowser) -> PlaywrightBrowserContext: """Creates a new browser context with anti-detection measures and loads cookies if available.""" @@ -93,4 +95,20 @@ async def _create_context(self, browser: PlaywrightBrowser) -> PlaywrightBrowser """ ) + self._context = context return context + + @property + def context(self) -> PlaywrightBrowserContext | None: + """Get the underlying Playwright browser context.""" + return self._context + + async def close(self): + """Close the browser context and stop tracing if enabled.""" + if self.config.trace_path and self._context: + trace_path = Path(self.config.trace_path) + trace_path.parent.mkdir(parents=True, exist_ok=True) + if not trace_path.suffix: + trace_path = trace_path / "trace.zip" + await self._context.tracing.stop(path=str(trace_path)) + await super().close() diff --git a/tests/test_browser_cli.py b/tests/test_browser_cli.py index 0e7c1552..7974ea62 100644 --- a/tests/test_browser_cli.py +++ b/tests/test_browser_cli.py @@ -2,6 +2,8 @@ from pathlib import Path import tempfile import logging +from io import StringIO +import contextlib # Add project root to Python path PROJECT_ROOT = Path(__file__).parent.parent @@ -25,11 +27,16 @@ async def cleanup(): logger.info(f"Cleanup start - Browser state: {_global_browser is not None}") - # Reset globals before test + # Reset globals and environment before test + if _global_browser is not None: + await close_browser() + logger.info("Browser closed") + _global_browser = None _global_browser_context = None + os.environ["BROWSER_USE_RUNNING"] = "false" - logger.info("Globals reset before test") + logger.info("Globals and environment reset before test") try: yield @@ -45,16 +52,17 @@ async def cleanup(): if tasks: logger.info(f"Found {len(tasks)} pending tasks") for task in tasks: - if not task.cancelled(): - task.cancel() - logger.info("Tasks cancelled") + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + logger.info("Pending tasks cancelled") except Exception as e: logger.error(f"Error during cleanup: {e}") + raise finally: - # Always reset globals after test _global_browser = None _global_browser_context = None - logger.info("Globals reset after test") + os.environ["BROWSER_USE_RUNNING"] = "false" + logger.info("Globals and environment reset after test") class TestBrowserInitialization: """Test browser launch-time options""" @@ -221,6 +229,9 @@ async def test_tracing(self, tmp_path): trace_path=str(trace_path) ) + # Wait a bit for the trace file to be written + await asyncio.sleep(1) + # Check that trace file was created traces = list(trace_path.glob("*.zip")) assert len(traces) > 0 @@ -326,32 +337,255 @@ async def test_error_handling(self): result = await run_browser_task("go to example.com") assert result is not None -def test_cli_commands(): - """Test CLI command parsing""" - import sys - from io import StringIO - - # Test start command - output = StringIO() - sys.stdout = output - sys.argv = ["browser-use", "start", "--window-size", "800x600"] - from cli.browser_use_cli import main - main() - assert "Browser session started successfully" in output.getvalue() - - # Test run command - output = StringIO() - sys.stdout = output - sys.argv = ["browser-use", "run", "go to example.com", "--model", "deepseek-chat"] - main() - assert len(output.getvalue()) > 0 +class TestCLICommands: + """Comprehensive tests for CLI command functionality""" - # Test close command - output = StringIO() - sys.stdout = output - sys.argv = ["browser-use", "close"] - main() - assert "Browser session closed" in output.getvalue() - - # Restore stdout - sys.stdout = sys.__stdout__ \ No newline at end of file + @pytest.fixture(autouse=True) + def setup_cli(self): + """Setup and cleanup for CLI tests""" + # Store original argv and stdout + self.original_argv = sys.argv.copy() + self.original_stdout = sys.stdout + + # Create StringIO buffer and redirect stdout + self.output = StringIO() + sys.stdout = self.output + + yield + + # Restore original argv and stdout + sys.argv = self.original_argv + sys.stdout = self.original_stdout + + # Close the StringIO buffer + self.output.close() + + def test_start_command_basic(self): + """Test basic browser start command""" + # Ensure output buffer is empty + self.output.truncate(0) + self.output.seek(0) + + sys.argv = ["browser-use", "start"] + with contextlib.redirect_stdout(self.output): + main() + output = self.output.getvalue() + assert "Browser session started successfully" in output + + def test_start_command_with_options(self): + """Test browser start with various options""" + # Ensure output buffer is empty + self.output.truncate(0) + self.output.seek(0) + + sys.argv = [ + "browser-use", "start", + "--window-size", "800x600", + "--headless", + "--disable-security" + ] + with contextlib.redirect_stdout(self.output): + main() + output = self.output.getvalue() + assert "Browser session started successfully" in output + + def test_run_command_basic(self): + """Test basic run command""" + # First start the browser + self.output.truncate(0) + self.output.seek(0) + sys.argv = ["browser-use", "start"] + with contextlib.redirect_stdout(self.output): + main() + + # Then run a task + self.output.truncate(0) + self.output.seek(0) + sys.argv = [ + "browser-use", "run", + "go to example.com", + "--model", "deepseek-chat" + ] + with contextlib.redirect_stdout(self.output): + main() + output = self.output.getvalue() + assert len(output) > 0 + + def test_run_command_with_options(self): + """Test run command with various options""" + # First start the browser + self.output.truncate(0) + self.output.seek(0) + sys.argv = ["browser-use", "start"] + with contextlib.redirect_stdout(self.output): + main() + + # Then run a task with multiple options + self.output.truncate(0) + self.output.seek(0) + sys.argv = [ + "browser-use", "run", + "go to example.com", + "--model", "gemini", + "--vision", + "--max-steps", "5", + "--max-actions", "2", + "--add-info", "Focus on the main content" + ] + with contextlib.redirect_stdout(self.output): + main() + output = self.output.getvalue() + assert len(output) > 0 + + def test_close_command(self): + """Test browser close command""" + # First start the browser + self.output.truncate(0) + self.output.seek(0) + sys.argv = ["browser-use", "start"] + with contextlib.redirect_stdout(self.output): + main() + + # Then close it + self.output.truncate(0) + self.output.seek(0) + sys.argv = ["browser-use", "close"] + with contextlib.redirect_stdout(self.output): + main() + output = self.output.getvalue() + assert "Browser session closed" in output + + def test_invalid_command(self): + """Test handling of invalid commands""" + self.output.truncate(0) + self.output.seek(0) + sys.argv = ["browser-use", "invalid-command"] + with pytest.raises(SystemExit): + with contextlib.redirect_stdout(self.output): + main() + + def test_missing_required_args(self): + """Test handling of missing required arguments""" + self.output.truncate(0) + self.output.seek(0) + sys.argv = ["browser-use", "run"] # Missing prompt + with pytest.raises(SystemExit): + with contextlib.redirect_stdout(self.output): + main() + + def test_invalid_window_size(self): + """Test handling of invalid window size format""" + self.output.truncate(0) + self.output.seek(0) + sys.argv = ["browser-use", "start", "--window-size", "invalid"] + with contextlib.redirect_stdout(self.output): + main() # Should use default size + output = self.output.getvalue() + assert "Browser session started successfully" in output + + def test_recording_options(self): + """Test recording functionality via CLI""" + with tempfile.TemporaryDirectory() as tmp_dir: + # First start the browser + self.output.truncate(0) + self.output.seek(0) + sys.argv = ["browser-use", "start"] + with contextlib.redirect_stdout(self.output): + main() + + # Then run with recording + self.output.truncate(0) + self.output.seek(0) + sys.argv = [ + "browser-use", "run", + "go to example.com", + "--record", + "--record-path", tmp_dir + ] + with contextlib.redirect_stdout(self.output): + main() + recordings = list(Path(tmp_dir).glob("*.webm")) + assert len(recordings) > 0 + + def test_tracing_options(self): + """Test tracing functionality via CLI""" + with tempfile.TemporaryDirectory() as tmp_dir: + # First start the browser + self.output.truncate(0) + self.output.seek(0) + sys.argv = ["browser-use", "start"] + with contextlib.redirect_stdout(self.output): + main() + + # Then run with tracing + self.output.truncate(0) + self.output.seek(0) + sys.argv = [ + "browser-use", "run", + "go to example.com", + "--trace-path", tmp_dir + ] + with contextlib.redirect_stdout(self.output): + main() + traces = list(Path(tmp_dir).glob("*.zip")) + assert len(traces) > 0 + + def test_model_switching_cli(self): + """Test switching between different models via CLI""" + # First start the browser + self.output.truncate(0) + self.output.seek(0) + sys.argv = ["browser-use", "start"] + with contextlib.redirect_stdout(self.output): + main() + + # Test with DeepSeek + self.output.truncate(0) + self.output.seek(0) + sys.argv = [ + "browser-use", "run", + "go to example.com", + "--model", "deepseek-chat" + ] + with contextlib.redirect_stdout(self.output): + main() + deepseek_output = self.output.getvalue() + + # Close browser to clean up event loop + self.output.truncate(0) + self.output.seek(0) + sys.argv = ["browser-use", "close"] + with contextlib.redirect_stdout(self.output): + main() + + # Start new browser for Gemini test + self.output.truncate(0) + self.output.seek(0) + sys.argv = ["browser-use", "start"] + with contextlib.redirect_stdout(self.output): + main() + + # Test with Gemini + self.output.truncate(0) + self.output.seek(0) + os.environ["GOOGLE_API_MODEL"] = model_names["gemini"][0] + sys.argv = [ + "browser-use", "run", + "go to example.com", + "--model", "gemini", + "--vision" + ] + with contextlib.redirect_stdout(self.output): + main() + gemini_output = self.output.getvalue() + + # Close browser + self.output.truncate(0) + self.output.seek(0) + sys.argv = ["browser-use", "close"] + with contextlib.redirect_stdout(self.output): + main() + + assert len(deepseek_output) > 0 + assert len(gemini_output) > 0 + assert deepseek_output != gemini_output \ No newline at end of file From 070535244279529d2c0625a85db4486937f2aed8 Mon Sep 17 00:00:00 2001 From: David Mieloch Date: Sun, 19 Jan 2025 18:14:30 -0500 Subject: [PATCH 04/10] Enhance README and add CLI usage guide for browser automation - Expanded the README.md to include a detailed purpose for the fork, CLI documentation, example tasks, and configuration options. - Introduced a new cli/usage-guide.md file that provides comprehensive instructions on using the browser-use API for automation with various LLM models. - Included code snippets for basic setup, browser context configuration, model configuration, agent setup, and common tasks. - Added best practices for error handling, resource management, and performance optimization in the usage guide. --- README.md | 54 ++++++-- cli/usage-guide.md | 308 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 355 insertions(+), 7 deletions(-) create mode 100644 cli/usage-guide.md diff --git a/README.md b/README.md index 184eeb93..53c3cdbd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,32 @@ +# Fork Purpose + +This fork of browser-use/web-ui adds CLI support specifically designed for AI agents like Cursor Agent. It enables direct command-line interaction with browser automation tasks, making it ideal for integration with AI development environments and automated workflows. + +## CLI Documentation + +- [Usage Guide](cli/usage-guide.md) - Comprehensive guide for CLI usage, including: + - Model configuration (DeepSeek, Gemini, GPT-4, Claude-3) + - Browser automation tasks + - Tracing and debugging + - Report generation + +### Example Tasks + +The [browser-tasks-example.ts](cli/browser-tasks-example.ts) provides ready-to-use task sequences for: + +- Product research automation +- Documentation analysis +- Page structure analysis +- Debug sessions with tracing + +### Configuration + +See [.env.example](.env.example) for all available configuration options, including: + +- API keys for different LLM providers +- Browser settings +- Session persistence options + Browser Use Web UI
@@ -60,6 +89,7 @@ playwright install - Git to clone the repository 2. **Setup:** + ```bash # Clone the repository git clone https://github.com/browser-use/web-ui.git @@ -71,6 +101,7 @@ playwright install ``` 3. **Run with Docker:** + ```bash # Build and start the container with default settings (browser closes after AI tasks) docker compose up --build @@ -82,18 +113,20 @@ playwright install 4. **Access the Application:** - WebUI: `http://localhost:7788` - VNC Viewer (to see browser interactions): `http://localhost:6080/vnc.html` - - Default VNC password is "vncpassword". You can change it by setting the `VNC_PASSWORD` environment variable in your `.env` file. + Default VNC password is "vncpassword". You can change it by setting the `VNC_PASSWORD` environment variable in your `.env` file. ## Usage ### Local Setup -1. Copy `.env.example` to `.env` and set your environment variables, including API keys for the LLM. `cp .env.example .env` -2. **Run the WebUI:** + +1. Copy `.env.example` to `.env` and set your environment variables, including API keys for the LLM. `cp .env.example .env` +2. **Run the WebUI:** + ```bash python webui.py --ip 127.0.0.1 --port 7788 ``` + 4. WebUI options: - `--ip`: The IP address to bind the WebUI to. Default is `127.0.0.1`. - `--port`: The port to bind the WebUI to. Default is `7788`. @@ -106,20 +139,24 @@ playwright install - **Citrus**: A vibrant, citrus-inspired palette with bright and fresh colors. - **Ocean** (default): A blue, ocean-inspired theme providing a calming effect. - `--dark-mode`: Enables dark mode for the user interface. -3. **Access the WebUI:** Open your web browser and navigate to `http://127.0.0.1:7788`. -4. **Using Your Own Browser(Optional):** +3. **Access the WebUI:** Open your web browser and navigate to `http://127.0.0.1:7788`. +4. **Using Your Own Browser(Optional):** - Set `CHROME_PATH` to the executable path of your browser and `CHROME_USER_DATA` to the user data directory of your browser. - Windows + ```env CHROME_PATH="C:\Program Files\Google\Chrome\Application\chrome.exe" CHROME_USER_DATA="C:\Users\YourUsername\AppData\Local\Google\Chrome\User Data" ``` + > Note: Replace `YourUsername` with your actual Windows username for Windows systems. - Mac + ```env CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" CHROME_USER_DATA="~/Library/Application Support/Google/Chrome/Profile 1" ``` + - Close all Chrome windows - Open the WebUI in a non-Chrome browser, such as Firefox or Edge. This is important because the persistent browser context will use the Chrome data when running the agent. - Check the "Use Own Browser" option within the Browser Settings. @@ -127,9 +164,11 @@ playwright install - Set `CHROME_PERSISTENT_SESSION=true` in the `.env` file. ### Docker Setup + 1. **Environment Variables:** - All configuration is done through the `.env` file - Available environment variables: + ``` # LLM API Keys OPENAI_API_KEY=your_key_here @@ -164,6 +203,7 @@ playwright install - You can now see all browser interactions in real-time 4. **Container Management:** + ```bash # Start with persistent browser CHROME_PERSISTENT_SESSION=true docker compose up -d @@ -181,4 +221,4 @@ playwright install ## Changelog - [x] **2025/01/10:** Thanks to @casistack. Now we have Docker Setup option and also Support keep browser open between tasks.[Video tutorial demo](https://github.com/browser-use/web-ui/issues/1#issuecomment-2582511750). -- [x] **2025/01/06:** Thanks to @richard-devbot. A New and Well-Designed WebUI is released. [Video tutorial demo](https://github.com/warmshao/browser-use-webui/issues/1#issuecomment-2573393113). \ No newline at end of file +- [x] **2025/01/06:** Thanks to @richard-devbot. A New and Well-Designed WebUI is released. [Video tutorial demo](https://github.com/warmshao/browser-use-webui/issues/1#issuecomment-2573393113). diff --git a/cli/usage-guide.md b/cli/usage-guide.md new file mode 100644 index 00000000..8a26a61e --- /dev/null +++ b/cli/usage-guide.md @@ -0,0 +1,308 @@ +# Browser-Use API Usage Guide + +## Overview + +This guide explains how to use the browser-use API to automate browser interactions using different LLM models. The API provides a powerful way to control a browser programmatically through Python. + +## Basic Setup + +```python +import asyncio +from browser_use.browser.browser import Browser, BrowserConfig +from browser_use.browser.context import BrowserContextConfig, BrowserContextWindowSize +from src.agent.custom_agent import CustomAgent +from src.controller.custom_controller import CustomController +from src.agent.custom_prompts import CustomSystemPrompt +from src.utils import utils +import os + +# Window size configuration +window_w, window_h = 1920, 1080 + +# Browser initialization +browser = Browser( + config=BrowserConfig( + headless=False, # Set to True for headless mode + disable_security=True, + extra_chromium_args=[f"--window-size={window_w},{window_h}"], + ) +) +``` + +## Browser Context Configuration + +```python +# Create a browser context with recording and tracing +browser_context = await browser.new_context( + config=BrowserContextConfig( + trace_path="./tmp/traces", # For debugging + save_recording_path="./tmp/record_videos", # For session recording + no_viewport=False, + browser_window_size=BrowserContextWindowSize( + width=window_w, height=window_h + ), + ) +) +``` + +## Model Configuration + +### DeepSeek (Default) + +```python +llm = utils.get_llm_model( + provider="deepseek", + model_name="deepseek-chat", # V2.5 model + temperature=0.8, + base_url="https://api.deepseek.com/v1", + api_key=os.getenv("DEEPSEEK_API_KEY", "") +) +``` + +### Gemini Pro + +```python +llm = utils.get_llm_model( + provider="gemini", + model_name="gemini-2.0-flash-exp", + temperature=1.0, + api_key=os.getenv("GOOGLE_API_KEY", "") +) +``` + +### GPT-4 Turbo + +```python +llm = utils.get_llm_model( + provider="openai", + model_name="gpt-4-turbo-preview", + temperature=0.8, + api_key=os.getenv("OPENAI_API_KEY", "") +) +``` + +### Claude-3 Opus + +```python +llm = utils.get_llm_model( + provider="anthropic", + model_name="claude-3-opus-20240229", + temperature=0.8, + api_key=os.getenv("ANTHROPIC_API_KEY", "") +) +``` + +## Agent Configuration + +```python +# Initialize controller +controller = CustomController() + +# Initialize agent +agent = CustomAgent( + task="your task description here", + add_infos="", # Optional hints for the LLM + llm=llm, # LLM model configured above + browser=browser, + browser_context=browser_context, + controller=controller, + system_prompt_class=CustomSystemPrompt, + use_vision=False, # Must be False for DeepSeek + tool_call_in_content=True, # Required for DeepSeek + max_actions_per_step=1 # Control action granularity +) +``` + +## Running Tasks + +```python +# Run the agent with a maximum number of steps +history = await agent.run(max_steps=10) + +# Access results +print("Final Result:", history.final_result()) +print("Errors:", history.errors()) +print("Model Actions:", history.model_actions()) +print("Thoughts:", history.model_thoughts()) +``` + +## Common Tasks + +### Navigation + +```python +task="go to google.com" +``` + +### Search + +```python +task="go to google.com and search for 'OpenAI'" +``` + +### Form Filling + +```python +task="go to example.com/login and fill in username 'user' and password 'pass'" +``` + +### Clicking Elements + +```python +task="click the 'Submit' button" +``` + +## Model-Specific Considerations + +1. **DeepSeek** + - Set `use_vision=False` + - Set `tool_call_in_content=True` + - Uses OpenAI-compatible API format + +2. **Gemini** + - Set `use_vision=True` + - Works well with visual tasks + +3. **GPT-4 & Claude-3** + - Support both vision and non-vision tasks + - Higher reasoning capabilities for complex tasks + +## Best Practices + +1. **Error Handling** + - Always check `history.errors()` for any issues + - Monitor `history.model_thoughts()` for debugging + +2. **Resource Management** + - Use async context managers for browser and context + - Close resources properly after use + +3. **Task Description** + - Be specific and clear in task descriptions + - Include necessary context in `add_infos` + +4. **Performance** + - Use `headless=True` for automated tasks + - Adjust `max_steps` and `max_actions_per_step` based on task complexity + +## Example Implementation + +```python +async def main(): + # Browser setup + browser = Browser(config=BrowserConfig(...)) + + async with await browser.new_context(...) as browser_context: + # Controller setup + controller = CustomController() + + # Agent setup + agent = CustomAgent( + task="your task", + llm=your_configured_llm, + browser=browser, + browser_context=browser_context, + controller=controller, + system_prompt_class=CustomSystemPrompt, + use_vision=False, + tool_call_in_content=True, + max_actions_per_step=1 + ) + + # Run task + history = await agent.run(max_steps=10) + + # Process results + print(history.final_result()) + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## Troubleshooting + +1. **JSON Schema Errors with DeepSeek** + - Ensure using latest DeepSeek V2.5 endpoint + - Verify correct base URL and API key + - Use `tool_call_in_content=True` + +2. **Browser Connection Issues** + - Check browser configuration + - Verify Chrome/Chromium installation + - Ensure proper port access + +3. **Model Response Issues** + - Adjust temperature for more/less deterministic behavior + - Try different models for complex tasks + - Check API key validity and quotas + +## Tracing and Debugging + +### Enabling Tracing + +```python +# Enable tracing in browser context +browser_context = await browser.new_context( + config=BrowserContextConfig( + trace_path="./tmp/traces/trace.zip", # Must have .zip extension + no_viewport=False, + browser_window_size=BrowserContextWindowSize( + width=window_w, height=window_h + ), + ) +) +``` + +### Using Traces for Debugging + +1. **Recording Traces** + - Traces are automatically saved when `trace_path` is provided + - Files are saved with `.zip` extension + - Contains browser actions, network requests, and screenshots + +2. **Analyzing Traces** + - Use Playwright Trace Viewer to analyze traces + - View step-by-step browser actions + - Inspect network requests and responses + - Review page states at each step + +## Report Generation + +### Best Practices + +1. **Structure** + - Always include page title and headings + - List interactive elements with their types + - Provide clear hierarchy of content + - Include relevant metadata (URLs, timestamps) + +2. **Content** + - Focus on task-relevant information + - Include both static and dynamic content + - Document interactive elements and their states + - Note any errors or warnings + +3. **Format** + - Use clear section headings + - Include numbered or bulleted lists + - Add summary sections for complex pages + - Use markdown formatting for readability + +### Example Report Task + +```python +task = "create a report about the page structure, including any interactive elements found" +add_infos = "Focus on navigation elements and forms" + +agent = CustomAgent( + task=task, + add_infos=add_infos, + llm=llm, + browser=browser, + browser_context=browser_context, + controller=controller, + system_prompt_class=CustomSystemPrompt, + use_vision=True, # Enable vision for better structure analysis + max_actions_per_step=1 +) +``` From b4a9e3a379fbc0a969fd158307ce25baa16784ef Mon Sep 17 00:00:00 2001 From: David Mieloch Date: Sun, 19 Jan 2025 19:12:00 -0500 Subject: [PATCH 05/10] Enhance CLI functionality and README documentation for browser automation - Added a comprehensive "Quick Start" section and detailed CLI commands to the README.md, improving user guidance for browser automation tasks. - Updated `browser_use_cli.py` to support additional options for browser initialization, including headless mode, window size, security settings, user data directory, and proxy configuration. - Improved error handling during browser initialization and reinitialization processes to ensure consistent browser state management. --- README.md | 35 +++++++++++++++++++++++++++++ cli/browser_use_cli.py | 50 +++++++++++++++++++++++++++++++++++------- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 53c3cdbd..3dc57385 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,41 @@ This fork of browser-use/web-ui adds CLI support specifically designed for AI ag - Tracing and debugging - Report generation +### Quick Start + +```bash +# Run a task (browser will auto-start if needed) +browser-use run "go to example.com and create a report about the page structure" + +# Run with specific model and options +browser-use run --model claude-3 --vision --trace-path ./traces "analyze the layout and visual elements" + +# Explicitly start browser with custom options (optional) +browser-use start --headless --window-size 1920x1080 + +# Close browser when done +browser-use close +``` + +### CLI Commands + +- `start` - (Optional) Initialize browser session with custom options: + - `--headless` - Run in headless mode + - `--window-size` - Set window dimensions (e.g., "1920x1080") + - `--disable-security` - Disable security features + - `--user-data-dir` - Use custom Chrome profile + - `--proxy` - Set proxy server + +- `run` - Execute tasks (auto-starts browser if needed): + - `--model` - Choose LLM (deepseek-chat, gemini, gpt-4, claude-3) + - `--vision` - Enable visual analysis + - `--record` - Record browser session + - `--trace-path` - Save debugging traces + - `--max-steps` - Limit task steps + - `--add-info` - Provide additional context + +- `close` - Clean up browser session + ### Example Tasks The [browser-tasks-example.ts](cli/browser-tasks-example.ts) provides ready-to-use task sequences for: diff --git a/cli/browser_use_cli.py b/cli/browser_use_cli.py index 42c176f4..e36045bb 100644 --- a/cli/browser_use_cli.py +++ b/cli/browser_use_cli.py @@ -106,18 +106,46 @@ async def run_browser_task( trace_path=None, max_steps=10, max_actions=1, - add_info="" + add_info="", + on_init=None, + headless=False, + window_size=(1920, 1080), + disable_security=False, + user_data_dir=None, + proxy=None ): - """Execute a task using the current browser instance.""" + """Execute a task using the current browser instance, auto-initializing if needed.""" global _global_browser, _global_browser_context + # Check if browser is running and initialize if needed if not _get_browser_state(): - print("No browser session found. Start one with: browser-use start") - return + print("Browser not running. Starting browser session...") + if not await initialize_browser( + headless=headless, + window_size=window_size, + disable_security=disable_security, + user_data_dir=user_data_dir, + proxy=proxy + ): + return "Browser initialization failed" + # Signal successful initialization if callback provided + if _get_browser_state() and on_init: + await on_init() + + # Verify browser state is consistent if _global_browser is None or _global_browser_context is None: - print("Browser session state is inconsistent. Try closing and restarting the browser.") - return + print("Browser session state is inconsistent. Attempting to reinitialize...") + if not await initialize_browser( + headless=headless, + window_size=window_size, + disable_security=disable_security, + user_data_dir=user_data_dir, + proxy=proxy + ): + return "Browser reinitialization failed" + if _global_browser is None or _global_browser_context is None: + return "Browser session state remains inconsistent after reinitialization" # Initialize controller controller = CustomController() @@ -248,9 +276,15 @@ def main(): trace_path=args.trace_path, max_steps=args.max_steps, max_actions=args.max_actions, - add_info=args.add_info + add_info=args.add_info, + headless=False, + window_size=(1920, 1080), + disable_security=False, + user_data_dir=None, + proxy=None )) - print(result) + if result: + print(result) elif args.command == "close": # Close browser From 8c0118ae8ac78061e0513d0c48070cbf899dd9ec Mon Sep 17 00:00:00 2001 From: David Mieloch Date: Mon, 20 Jan 2025 11:35:04 -0500 Subject: [PATCH 06/10] Refactor browser state management and enhance CLI options - Updated `_get_browser_state` and `_set_browser_state` functions to utilize a temporary file for managing browser state instead of environment variables. - Added `--temp-file` argument to `start`, `run`, and `close` commands for specifying the temporary file path. - Enhanced `browser-use` script to create and manage a temporary state file during execution. - Introduced new options in `browser-tasks-example.ts` for session recording and debugging traces. - Updated `browser-use.toolchain.json` to include new parameters for recording and tracing. - Added VSCode settings for improved development experience. --- cli/.vscode/settings.json | 7 +++ cli/browser-tasks-example.ts | 50 ++++++++++----------- cli/browser-use | 82 ++++++++++++++++++++++++++++------ cli/browser-use.toolchain.json | 50 +++++++++++++++++++-- cli/browser_use_cli.py | 36 ++++++++++----- 5 files changed, 173 insertions(+), 52 deletions(-) create mode 100644 cli/.vscode/settings.json diff --git a/cli/.vscode/settings.json b/cli/.vscode/settings.json new file mode 100644 index 00000000..29777348 --- /dev/null +++ b/cli/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "workbench.colorCustomizations": { + "activityBar.background": "#2D2F09", + "titleBar.activeBackground": "#3F420C", + "titleBar.activeForeground": "#FAFBEA" + } +} \ No newline at end of file diff --git a/cli/browser-tasks-example.ts b/cli/browser-tasks-example.ts index d0acca18..076658ef 100644 --- a/cli/browser-tasks-example.ts +++ b/cli/browser-tasks-example.ts @@ -10,8 +10,12 @@ export interface BrowserCommand { model?: 'deepseek-chat' | 'gemini' | 'gpt-4' | 'claude-3'; headless?: boolean; vision?: boolean; - keepSessionAlive?: boolean; - trace?: boolean; + record?: boolean; + recordPath?: string; + tracePath?: string; + maxSteps?: number; + maxActions?: number; + addInfo?: string; } export interface BrowserTask { @@ -37,8 +41,7 @@ export const browserTasks: BrowserTaskSequence[] = [ command: { prompt: "go to amazon.com and search for 'wireless earbuds' and tell me the price of the top 3 results", model: "gemini", - vision: true, - keepSessionAlive: true + vision: true } }, { @@ -46,15 +49,13 @@ export const browserTasks: BrowserTaskSequence[] = [ command: { prompt: "go to bestbuy.com and search for 'wireless earbuds' and tell me the price of the top 3 results", model: "gemini", - vision: true, - keepSessionAlive: true + vision: true } }, { description: "Create price comparison", command: { - prompt: "create a comparison table of the prices from both sites", - keepSessionAlive: false + prompt: "create a comparison table of the prices from both sites" } } ] @@ -130,7 +131,6 @@ export const browserTasks: BrowserTaskSequence[] = [ command: { prompt: "go to docs.github.com and navigate to the Actions documentation", model: "deepseek-chat", // Use DeepSeek for basic navigation - keepSessionAlive: true } }, { @@ -139,7 +139,6 @@ export const browserTasks: BrowserTaskSequence[] = [ prompt: "analyze the layout of the page and tell me how the documentation is structured, including sidebars, navigation, and content areas", model: "gemini", // Switch to Gemini for visual analysis vision: true, - keepSessionAlive: true } }, { @@ -147,7 +146,6 @@ export const browserTasks: BrowserTaskSequence[] = [ command: { prompt: "summarize the key concepts of GitHub Actions based on the documentation", model: "claude-3", // Switch to Claude for complex summarization - keepSessionAlive: true } }, { @@ -155,7 +153,6 @@ export const browserTasks: BrowserTaskSequence[] = [ command: { prompt: "find and list all YAML workflow examples on the page", model: "deepseek-chat", // Back to DeepSeek for code extraction - keepSessionAlive: false // Close browser after final task } } ] @@ -170,7 +167,6 @@ export const browserTasks: BrowserTaskSequence[] = [ prompt: "go to example.com and create a report about the page structure, including the page title, headings, and any interactive elements found", model: "gemini", vision: true, - keepSessionAlive: true } }, { @@ -179,7 +175,6 @@ export const browserTasks: BrowserTaskSequence[] = [ prompt: "focus on the navigation menu and create a detailed report of its structure and all available links", model: "gemini", vision: true, - keepSessionAlive: true } }, { @@ -187,8 +182,7 @@ export const browserTasks: BrowserTaskSequence[] = [ command: { prompt: "find all forms on the page and document their inputs, buttons, and validation requirements", model: "gemini", - vision: true, - keepSessionAlive: false + vision: true } } ] @@ -203,8 +197,7 @@ export const browserTasks: BrowserTaskSequence[] = [ prompt: "go to example.com/login and attempt to log in with test credentials", model: "deepseek-chat", headless: false, - keepSessionAlive: true, - trace: true + tracePath: "./tmp/traces/login" } }, { @@ -212,8 +205,7 @@ export const browserTasks: BrowserTaskSequence[] = [ command: { prompt: "complete the multi-step registration process", model: "deepseek-chat", - keepSessionAlive: true, - trace: true + tracePath: "./tmp/traces/registration" } }, { @@ -221,24 +213,30 @@ export const browserTasks: BrowserTaskSequence[] = [ command: { prompt: "create a report of all actions taken and any errors encountered", model: "claude-3", - keepSessionAlive: false, - trace: true + tracePath: "./tmp/traces/report" } } ] } ]; -// Example of executing a task sequence +// Updated execute task function to match CLI arguments const executeTask = (task: BrowserCommand): string => { const options: string[] = []; + if (task.model) options.push(`--model ${task.model}`); if (task.headless) options.push('--headless'); if (task.vision) options.push('--vision'); - if (task.keepSessionAlive) options.push('--keep-browser-open'); - if (task.trace) options.push('--trace-path ./tmp/traces/trace.zip'); + if (task.record) { + options.push('--record'); + if (task.recordPath) options.push(`--record-path ${task.recordPath}`); + } + if (task.tracePath) options.push(`--trace-path ${task.tracePath}`); + if (task.maxSteps) options.push(`--max-steps ${task.maxSteps}`); + if (task.maxActions) options.push(`--max-actions ${task.maxActions}`); + if (task.addInfo) options.push(`--add-info "${task.addInfo}"`); - return `browser-use "${task.prompt}" ${options.join(' ')}`.trim(); + return `browser-use run "${task.prompt}" ${options.join(' ')}`.trim(); }; // Example usage: diff --git a/cli/browser-use b/cli/browser-use index 6a57e0e4..4c83fb85 100755 --- a/cli/browser-use +++ b/cli/browser-use @@ -1,20 +1,76 @@ #!/bin/bash -# Get the absolute directory of this script -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +# Get the absolute path of the script's real location (dereference symbolic link) +REAL_SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}") -# Add the project directory to PYTHONPATH -export PYTHONPATH="$SCRIPT_DIR:$PYTHONPATH" +# Get the directory of the script +SCRIPT_DIR="$(dirname "$REAL_SCRIPT_PATH")" -# Activate the virtual environment if it exists -if [ -f "$SCRIPT_DIR/venv/bin/activate" ]; then - source "$SCRIPT_DIR/venv/bin/activate" +# Project root is one level up from the script's directory +PROJECT_ROOT="$SCRIPT_DIR/.." + +# Change to the project root directory +cd "$PROJECT_ROOT" + +# Activate the virtual environment +if [ -f "venv/bin/activate" ]; then + echo "Activating virtual environment" + source "venv/bin/activate" + echo "VIRTUAL_ENV: $VIRTUAL_ENV" +else + echo "Virtual environment activation script not found" +fi + +# Create a temporary file for state transfer +TEMP_STATE_FILE=$(mktemp) +echo "Created temporary state file: $TEMP_STATE_FILE" + +# Run the Python script and capture its output +echo "Running: venv/bin/python cli/browser_use_cli.py '$@'" +if ! "venv/bin/python" "cli/browser_use_cli.py" "$@" --temp-file "$TEMP_STATE_FILE"; then + echo "Error running command. Exiting." + echo "Cleaning up temp file: $TEMP_STATE_FILE" + rm -f "$TEMP_STATE_FILE" + exit 1 +fi + +# Check the exit code of the Python script +PYTHON_EXIT_CODE=$? + +# If Python script exited with a non-zero code, exit with the same code +if [ $PYTHON_EXIT_CODE -ne 0 ]; then + echo "Python script exited with error code: $PYTHON_EXIT_CODE" + echo "Cleaning up temp file: $TEMP_STATE_FILE" + rm -f "$TEMP_STATE_FILE" + exit $PYTHON_EXIT_CODE +fi + +# Read the BROWSER_USE_RUNNING value from the temporary file +if [ -f "$TEMP_STATE_FILE" ]; then + BROWSER_USE_RUNNING=$(cat "$TEMP_STATE_FILE") + echo "Read BROWSER_USE_RUNNING from file: $BROWSER_USE_RUNNING" + echo "Cleaning up temp file: $TEMP_STATE_FILE" + rm -f "$TEMP_STATE_FILE" +else + BROWSER_USE_RUNNING="false" + echo "Warning: Temp file not found at: $TEMP_STATE_FILE" + echo "Defaulting BROWSER_USE_RUNNING to: false" fi -# Run the Python script with all arguments passed through -"$SCRIPT_DIR/venv/bin/python" "$SCRIPT_DIR/browser-use-cli.py" "$@" +# Set the environment variable in the shell script based on captured value +export BROWSER_USE_RUNNING +echo "Environment variable BROWSER_USE_RUNNING set to: $BROWSER_USE_RUNNING" -# Deactivate the virtual environment if it was activated -if [ -n "$VIRTUAL_ENV" ]; then - deactivate -fi \ No newline at end of file +# Check if the BROWSER_USE_RUNNING environment variable is set to true +echo "BROWSER_USE_RUNNING: $BROWSER_USE_RUNNING" +if [ "$BROWSER_USE_RUNNING" = "true" ]; then + echo "Keeping virtual environment active for persistent session." +else + # Deactivate the virtual environment only if not running persistently + if [ -n "$VIRTUAL_ENV" ]; then + echo "Deactivating virtual environment" + deactivate + else + echo "Virtual environment was not active." + fi +fi \ No newline at end of file diff --git a/cli/browser-use.toolchain.json b/cli/browser-use.toolchain.json index 1f3b677c..cd27e10c 100644 --- a/cli/browser-use.toolchain.json +++ b/cli/browser-use.toolchain.json @@ -27,6 +27,34 @@ "type": "boolean", "default": false, "description": "Enable vision capabilities for supported models (optional)" + }, + "record": { + "type": "boolean", + "default": false, + "description": "Enable session recording (optional)" + }, + "recordPath": { + "type": "string", + "default": "./tmp/record_videos", + "description": "Path to save recordings (optional)" + }, + "tracePath": { + "type": "string", + "description": "Path to save debugging traces (optional)" + }, + "maxSteps": { + "type": "integer", + "default": 10, + "description": "Maximum number of steps per task (optional)" + }, + "maxActions": { + "type": "integer", + "default": 1, + "description": "Maximum actions per step (optional)" + }, + "addInfo": { + "type": "string", + "description": "Additional context for the agent (optional)" } }, "required": ["prompt"] @@ -36,15 +64,31 @@ "examples": [ { "description": "Basic usage", - "command": "browser-use \"go to google.com and search for OpenAI\"" + "command": "browser-use run \"go to google.com and search for OpenAI\"" }, { "description": "Using vision to analyze a webpage", - "command": "browser-use \"go to openai.com and tell me what you see\" --model gemini --vision" + "command": "browser-use run \"go to openai.com and tell me what you see\" --model gemini --vision" }, { "description": "Running a check in headless mode", - "command": "browser-use \"check if github.com is up\" --headless" + "command": "browser-use run \"check if github.com is up\" --headless" + }, + { + "description": "Recording a debug session", + "command": "browser-use run \"complete the login process\" --record --record-path ./tmp/debug_session" + }, + { + "description": "Using traces for debugging", + "command": "browser-use run \"test the checkout flow\" --trace-path ./tmp/traces/checkout" + }, + { + "description": "Starting a new browser session", + "command": "browser-use start --user-data-dir '/Users/dmieloch/Library/Application Support/Google/Chrome/Default'" + }, + { + "description": "Closing a browser session", + "command": "browser-use close --temp-file /tmp/browser_use_state" } ] } \ No newline at end of file diff --git a/cli/browser_use_cli.py b/cli/browser_use_cli.py index e36045bb..9f158b2b 100644 --- a/cli/browser_use_cli.py +++ b/cli/browser_use_cli.py @@ -27,12 +27,20 @@ _global_browser_context = None def _get_browser_state(): - """Get browser state from environment.""" - return os.environ.get("BROWSER_USE_RUNNING", "false").lower() == "true" + """Get browser state from temporary file.""" + temp_file = os.path.join(tempfile.gettempdir(), "browser_use_state") + try: + with open(temp_file, "r") as f: + return f.read().strip().lower() == "true" + except FileNotFoundError: + return False -def _set_browser_state(running=True): - """Set browser state in environment.""" - os.environ["BROWSER_USE_RUNNING"] = str(running).lower() +def _set_browser_state(running=True, temp_file_path=None): + """Set browser state in a temporary file.""" + value = str(running).lower() + if temp_file_path: + with open(temp_file_path, "w") as f: + f.write(value) async def initialize_browser( headless=False, @@ -220,16 +228,18 @@ def main(): parser = argparse.ArgumentParser(description="Control a browser using natural language") subparsers = parser.add_subparsers(dest="command", help="Commands") - # Start command - browser initialization + # Start command start_parser = subparsers.add_parser("start", help="Start a new browser session") + start_parser.add_argument("--temp-file", help="Path to temporary file for storing browser state") start_parser.add_argument("--headless", action="store_true", help="Run browser in headless mode") start_parser.add_argument("--window-size", default="1920x1080", help="Browser window size (WxH)") start_parser.add_argument("--disable-security", action="store_true", help="Disable browser security features") start_parser.add_argument("--user-data-dir", help="Use custom Chrome profile directory") start_parser.add_argument("--proxy", help="Proxy server URL") - # Run command - task execution + # Run command run_parser = subparsers.add_parser("run", help="Run a task in the current browser session") + run_parser.add_argument("--temp-file", help="Path to temporary file for storing browser state") run_parser.add_argument("prompt", help="The task to perform") run_parser.add_argument("--model", "-m", choices=["deepseek-chat", "gemini", "gpt-4", "claude-3"], default="deepseek-chat", help="The LLM model to use") @@ -241,9 +251,10 @@ def main(): run_parser.add_argument("--max-actions", type=int, default=1, help="Maximum actions per step") run_parser.add_argument("--add-info", help="Additional context for the agent") - # Close command - cleanup - subparsers.add_parser("close", help="Close the current browser session") - + # Close command + close_parser = subparsers.add_parser("close", help="Close the current browser session") + close_parser.add_argument("--temp-file", help="Path to temporary file for storing browser state") + args = parser.parse_args() if args.command == "start": @@ -264,6 +275,10 @@ def main(): )) if success: print("Browser session started successfully") + _set_browser_state(True, args.temp_file) + else: + print("Failed to start browser session") + _set_browser_state(False, args.temp_file) elif args.command == "run": # Run task @@ -290,6 +305,7 @@ def main(): # Close browser asyncio.run(close_browser()) print("Browser session closed") + _set_browser_state(False, args.temp_file) else: parser.print_help() From c01bddf90f95f73dcbb6bf4a84a91266918441b3 Mon Sep 17 00:00:00 2001 From: David Mieloch Date: Mon, 20 Jan 2025 15:04:59 -0500 Subject: [PATCH 07/10] Add demo logging feature and enhance structured logging system - Introduced a new demo_logging.py file to showcase the logging capabilities of the TaskLogger, including navigation, interaction, and data extraction phases. - Enhanced the structured logging system with color-coded log levels, task states, and visual separators for better readability. - Implemented error handling improvements with smart retry logic and structured error logging. - Added support for detailed performance metrics and progress tracking in task execution. - Updated README.md to reflect the new logging features and improvements in the logging system. - Introduced new utility functions for structured logging and error handling in the src/utils directory. - Added comprehensive tests for the new logging features and error handling mechanisms. --- README.md | 37 ++ demo_logging.py | 99 +++++ src/agent/custom_agent.py | 122 +++++- src/utils/browser_controller.py | 141 +++++++ src/utils/error_handling.py | 53 +++ src/utils/logging.py | 158 ++++++++ src/utils/structured_logging.py | 223 +++++++++++ src/utils/task_logging.py | 562 ++++++++++++++++++++++++++ tests/test_browser_controller.py | 125 ++++++ tests/test_error_handling.py | 98 +++++ tests/test_logging.py | 216 ++++++++++ tests/test_logging_integration.py | 219 ++++++++++ tests/test_structured_logging.py | 270 +++++++++++++ tests/test_task_logging.py | 641 ++++++++++++++++++++++++++++++ webui.py | 22 +- 15 files changed, 2962 insertions(+), 24 deletions(-) create mode 100644 demo_logging.py create mode 100644 src/utils/browser_controller.py create mode 100644 src/utils/error_handling.py create mode 100644 src/utils/logging.py create mode 100644 src/utils/structured_logging.py create mode 100644 src/utils/task_logging.py create mode 100644 tests/test_browser_controller.py create mode 100644 tests/test_error_handling.py create mode 100644 tests/test_logging.py create mode 100644 tests/test_logging_integration.py create mode 100644 tests/test_structured_logging.py create mode 100644 tests/test_task_logging.py diff --git a/README.md b/README.md index 3dc57385..c33615c4 100644 --- a/README.md +++ b/README.md @@ -257,3 +257,40 @@ playwright install - [x] **2025/01/10:** Thanks to @casistack. Now we have Docker Setup option and also Support keep browser open between tasks.[Video tutorial demo](https://github.com/browser-use/web-ui/issues/1#issuecomment-2582511750). - [x] **2025/01/06:** Thanks to @richard-devbot. A New and Well-Designed WebUI is released. [Video tutorial demo](https://github.com/warmshao/browser-use-webui/issues/1#issuecomment-2573393113). + +## Fork Information +This is a fork of the original browser-use project with additional features and improvements. + +## Changelog + +### January 2025 - Logging System Overhaul +- **Enhanced Logging System** + - Added structured task logging with context and state tracking + - Implemented color-coded log levels and task states + - Added visual separators between task phases + - Introduced emoji indicators for different action types (🌐 navigation, 🖱️ interaction, 📑 extraction) + - Added status symbols for task states (→ running, ✓ complete, × failed) + +- **Error Handling Improvements** + - Implemented smart retry logic with exponential backoff + - Added structured error logging with context + - Introduced visual error separators + - Added retry history and statistics tracking + +- **Progress Tracking** + - Added percentage-based progress tracking + - Implemented step duration tracking + - Added detailed browser state information + - Introduced performance metrics breakdown + +- **Log Management** + - Added semantic step descriptions + - Implemented message filtering and deduplication + - Added support for both JSON and human-readable output + - Introduced custom color schemes and formatting options + +### Coming Soon +- Log buffering for high-frequency events +- Recovery suggestions for common errors +- Real-time monitoring dashboard +- Interactive log viewer interface diff --git a/demo_logging.py b/demo_logging.py new file mode 100644 index 00000000..f7c70093 --- /dev/null +++ b/demo_logging.py @@ -0,0 +1,99 @@ +import asyncio +from src.utils.task_logging import ( + TaskLogger, TaskStatus, ActionType, RetryConfig, + ColorScheme, SeparatorStyle +) + +async def demo_logging(): + # Initialize logger with custom styles + logger = TaskLogger( + "demo_task", + "Demonstrate all logging features", + color_scheme=ColorScheme(), + separator_style=SeparatorStyle( + task="★" * 40, + phase="•" * 30, + error="!" * 35 + ) + ) + + # Start navigation phase + logger.start_phase("Navigation Phase") + logger.update_step( + "Navigate to example.com", + TaskStatus.RUNNING, + action_type=ActionType.NAVIGATION, + context={"url": "https://example.com"} + ) + + # Update browser state + logger.update_browser_state( + url="https://example.com", + page_ready=True, + dynamic_content_loaded=True, + visible_elements=15, + page_title="Example Domain" + ) + + # Complete navigation + logger.update_step( + "Page loaded successfully", + TaskStatus.COMPLETE, + action_type=ActionType.NAVIGATION, + progress=0.25, + results={"status": 200, "load_time": 0.5} + ) + + # Start interaction phase + logger.start_phase("Interaction Phase") + logger.update_step( + "Click search button", + TaskStatus.RUNNING, + action_type=ActionType.INTERACTION, + context={"element": "search_button"} + ) + + # Simulate error and retry + async def failing_operation(): + raise ValueError("Search button not found") + + try: + await logger.execute_with_retry( + failing_operation, + "click_search", + RetryConfig(max_retries=2, base_delay=0.1) + ) + except ValueError: + pass + + # Start extraction phase + logger.start_phase("Data Extraction Phase") + logger.update_step( + "Extract search results", + TaskStatus.RUNNING, + action_type=ActionType.EXTRACTION, + progress=0.75 + ) + + # Complete extraction + logger.update_step( + "Data extracted successfully", + TaskStatus.COMPLETE, + action_type=ActionType.EXTRACTION, + progress=1.0, + results={"items_found": 10} + ) + + # Display log history + print("\nLog History:") + print("=" * 80) + for entry in logger.get_log_history(): + print(entry) + print("=" * 80) + + # Log final state + print("\nFinal State:") + logger.log_state() + +if __name__ == "__main__": + asyncio.run(demo_logging()) \ No newline at end of file diff --git a/src/agent/custom_agent.py b/src/agent/custom_agent.py index ff8908c8..0332067d 100644 --- a/src/agent/custom_agent.py +++ b/src/agent/custom_agent.py @@ -8,11 +8,12 @@ import logging import pdb import traceback -from typing import Optional, Type +from typing import Optional, Type, Any, Dict from PIL import Image, ImageDraw, ImageFont import os import base64 import io +import datetime from browser_use.agent.prompts import SystemPrompt from browser_use.agent.service import Agent @@ -37,11 +38,13 @@ BaseMessage, ) from src.utils.agent_state import AgentState +from src.utils.logging import BatchedEventLogger from .custom_massage_manager import CustomMassageManager from .custom_views import CustomAgentOutput, CustomAgentStepInfo logger = logging.getLogger(__name__) +batched_logger = BatchedEventLogger(logger) class CustomAgent(Agent): @@ -117,23 +120,41 @@ def _setup_action_models(self) -> None: self.AgentOutput = CustomAgentOutput.type_with_custom_actions(self.ActionModel) def _log_response(self, response: CustomAgentOutput) -> None: - """Log the model's response""" - if "Success" in response.current_state.prev_action_evaluation: - emoji = "✅" - elif "Failed" in response.current_state.prev_action_evaluation: - emoji = "❌" - else: - emoji = "🤷" - - logger.info(f"{emoji} Eval: {response.current_state.prev_action_evaluation}") - logger.info(f"🧠 New Memory: {response.current_state.important_contents}") - logger.info(f"⏳ Task Progress: {response.current_state.completed_contents}") - logger.info(f"🤔 Thought: {response.current_state.thought}") - logger.info(f"🎯 Summary: {response.current_state.summary}") + """Log the model's response in a structured format""" + evaluation_status = "success" if "Success" in response.current_state.prev_action_evaluation else "failed" + + log_data = { + "timestamp": datetime.datetime.now().isoformat(), + "action": "model_response", + "status": evaluation_status, + "state": { + "evaluation": response.current_state.prev_action_evaluation, + "memory": response.current_state.important_contents, + "progress": response.current_state.completed_contents, + "thought": response.current_state.thought, + "summary": response.current_state.summary + } + } + + logger.info( + f"Model Response: {evaluation_status}", + extra={ + "event_type": "model_response", + "event_data": log_data + } + ) + + # Batch action logging for i, action in enumerate(response.action): - logger.info( - f"🛠️ Action {i + 1}/{len(response.action)}: {action.model_dump_json(exclude_unset=True)}" + batched_logger.add_event( + "action", + { + "action_number": i + 1, + "total_actions": len(response.action), + "action_data": json.loads(action.model_dump_json(exclude_unset=True)) + } ) + batched_logger.flush() def update_step_info( self, model_output: CustomAgentOutput, step_info: CustomAgentStepInfo = None @@ -193,7 +214,19 @@ async def get_next_action(self, input_messages: list[BaseMessage]) -> AgentOutpu @time_execution_async("--step") async def step(self, step_info: Optional[CustomAgentStepInfo] = None) -> None: """Execute one step of the task""" - logger.info(f"\n📍 Step {self.n_steps}") + step_data = { + "step_number": self.n_steps, + "timestamp": datetime.datetime.now().isoformat() + } + + logger.info( + f"Starting step {self.n_steps}", + extra={ + "event_type": "step_start", + "event_data": step_data + } + ) + state = None model_output = None result: list[ActionResult] = [] @@ -204,9 +237,18 @@ async def step(self, step_info: Optional[CustomAgentStepInfo] = None) -> None: input_messages = self.message_manager.get_messages() model_output = await self.get_next_action(input_messages) self.update_step_info(model_output, step_info) - logger.info(f"🧠 All Memory: {step_info.memory}") + + if step_info: + logger.debug( + "Step memory updated", + extra={ + "event_type": "memory_update", + "event_data": {"memory": step_info.memory} + } + ) + self._save_conversation(input_messages, model_output) - self.message_manager._remove_last_state_message() # we dont want the whole state in the chat history + self.message_manager._remove_last_state_message() self.message_manager.add_model_output(model_output) result: list[ActionResult] = await self.controller.multi_act( @@ -215,17 +257,37 @@ async def step(self, step_info: Optional[CustomAgentStepInfo] = None) -> None: self._last_result = result if len(result) > 0 and result[-1].is_done: - logger.info(f"📄 Result: {result[-1].extracted_content}") + logger.info( + "Task completed", + extra={ + "event_type": "task_complete", + "event_data": { + "result": result[-1].extracted_content + } + } + ) self.consecutive_failures = 0 except Exception as e: result = self._handle_step_error(e) self._last_result = result + logger.error( + f"Step error: {str(e)}", + extra={ + "event_type": "step_error", + "event_data": { + "error": str(e), + "traceback": traceback.format_exc() + } + }, + exc_info=True + ) finally: if not result: return + for r in result: if r.error: self.telemetry.capture( @@ -234,8 +296,28 @@ async def step(self, step_info: Optional[CustomAgentStepInfo] = None) -> None: error=r.error, ) ) + logger.error( + f"Action error: {r.error}", + extra={ + "event_type": "action_error", + "event_data": { + "error": r.error + } + } + ) + if state: self._make_history_item(model_output, state, result) + + step_data["status"] = "completed" + logger.info( + f"Step {self.n_steps} completed", + extra={ + "event_type": "step_complete", + "event_data": step_data + } + ) + def create_history_gif( self, output_path: str = 'agent_history.gif', diff --git a/src/utils/browser_controller.py b/src/utils/browser_controller.py new file mode 100644 index 00000000..2171574b --- /dev/null +++ b/src/utils/browser_controller.py @@ -0,0 +1,141 @@ +from typing import Optional, Any +import asyncio +from playwright.async_api import async_playwright, Browser, Playwright +from .structured_logging import StructuredLogger, setup_structured_logging + +class BrowserController: + def __init__(self): + self.browser: Optional[Browser] = None + self.init_promise: Optional[asyncio.Task] = None + self.init_count: int = 0 + self._playwright: Optional[Playwright] = None + self.logger = StructuredLogger("browser_controller") + setup_structured_logging() + + async def initialize(self) -> None: + """Initialize the browser if not already initialized.""" + if self.init_promise is not None: + try: + await self.init_promise + except Exception as e: + # If the current initialization fails, reset state to allow retry + self.init_promise = None + self.browser = None + self.logger.log_browser_event("initialization_failed", { + "error": str(e), + "attempt": self.init_count + 1 + }) + raise + + if self.browser is not None: + return + + # Create new initialization task + self.logger.log_progress( + step="browser_init", + status="starting", + progress=0.0, + message="Starting browser initialization" + ) + self.init_promise = asyncio.create_task(self._do_browser_init()) + try: + await self.init_promise + self.logger.log_progress( + step="browser_init", + status="completed", + progress=1.0, + message="Browser initialization completed" + ) + except Exception as e: + # Reset state on failure + self.init_promise = None + self.browser = None + self.logger.log_progress( + step="browser_init", + status="failed", + progress=0.0, + message=f"Browser initialization failed: {str(e)}" + ) + raise + + async def _do_browser_init(self) -> None: + """Internal method to handle browser initialization.""" + if self.browser is not None: + return + + self.logger.log_progress( + step="browser_init", + status="launching", + progress=0.3, + message="Launching Playwright" + ) + playwright = await async_playwright().start() + self._playwright = playwright + + try: + self.logger.log_progress( + step="browser_init", + status="configuring", + progress=0.6, + message="Configuring browser" + ) + self.browser = await playwright.chromium.launch( + headless=True, + args=['--no-sandbox'] + ) + self.init_count += 1 + + self.logger.log_browser_event("browser_launched", { + "initialization_count": self.init_count, + "headless": True + }) + + except Exception as e: + await self._cleanup_playwright() + self.logger.log_browser_event("launch_failed", { + "error": str(e), + "initialization_count": self.init_count + }) + raise + + async def _cleanup_playwright(self) -> None: + """Clean up the playwright context.""" + if self._playwright: + self.logger.log_browser_event("cleanup_playwright", { + "status": "starting" + }) + await self._playwright.stop() + self._playwright = None + self.logger.log_browser_event("cleanup_playwright", { + "status": "completed" + }) + + async def cleanup(self) -> None: + """Clean up browser resources.""" + self.logger.log_progress( + step="cleanup", + status="starting", + progress=0.0, + message="Starting browser cleanup" + ) + + if self.browser: + self.logger.log_progress( + step="cleanup", + status="closing_browser", + progress=0.5, + message="Closing browser" + ) + await self.browser.close() + self.browser = None + + await self._cleanup_playwright() + self.init_promise = None + self.init_count = 0 + + self.logger.log_progress( + step="cleanup", + status="completed", + progress=1.0, + message="Browser cleanup completed" + ) \ No newline at end of file diff --git a/src/utils/error_handling.py b/src/utils/error_handling.py new file mode 100644 index 00000000..2a4f744c --- /dev/null +++ b/src/utils/error_handling.py @@ -0,0 +1,53 @@ +import asyncio +from datetime import datetime +from typing import Dict, Any, Optional +import re + +class MaxRetriesExceededError(Exception): + def __init__(self, operation: str, original_error: Exception): + self.operation = operation + self.original_error = original_error + super().__init__(f"Max retries exceeded for operation '{operation}': {str(original_error)}") + +class ErrorHandler: + MAX_RETRIES = 3 + + def __init__(self): + self._retry_counts: Dict[str, int] = {} + self._last_error: Optional[Dict[str, Any]] = None + + async def handle_error(self, error: Exception, operation: str) -> None: + retry_count = self._retry_counts.get(operation, 0) + + if retry_count >= self.MAX_RETRIES: + raise MaxRetriesExceededError(operation, error) + + self._retry_counts[operation] = retry_count + 1 + await self._log_error(error, operation, retry_count) + + # Exponential backoff: 2^retry_count seconds + await asyncio.sleep(2 ** retry_count) + + async def _log_error(self, error: Exception, operation: str, retry_count: int) -> None: + error_context = { + "operation": operation, + "attempt": retry_count + 1, + "timestamp": datetime.now().isoformat(), + "error": { + "name": error.__class__.__name__, + "message": str(error), + "code": self.extract_error_code(error) + } + } + + self._last_error = error_context + # In a real implementation, we would log to a file or logging service + print(f"Error: {error_context}") + + def extract_error_code(self, error: Exception) -> str: + error_message = str(error) + match = re.search(r'ERR_[A-Z_]+', error_message) + return match.group(0) if match else "UNKNOWN_ERROR" + + def get_last_error(self) -> Optional[Dict[str, Any]]: + return self._last_error \ No newline at end of file diff --git a/src/utils/logging.py b/src/utils/logging.py new file mode 100644 index 00000000..982ffdc2 --- /dev/null +++ b/src/utils/logging.py @@ -0,0 +1,158 @@ +import json +import logging +import datetime +from typing import Any, Dict, List, Optional +from enum import Enum +import traceback +import types + +class LogLevel(str, Enum): + CRITICAL = "CRITICAL" + ERROR = "ERROR" + WARNING = "WARNING" + INFO = "INFO" + DEBUG = "DEBUG" + TRACE = "TRACE" + +class LogJSONEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Exception): + return { + 'type': obj.__class__.__name__, + 'message': str(obj), + 'traceback': traceback.format_exception(type(obj), obj, obj.__traceback__) + } + if isinstance(obj, type): + return obj.__name__ + if isinstance(obj, types.TracebackType): + return traceback.format_tb(obj) + return super().default(obj) + +class LogFormatter(logging.Formatter): + def __init__(self, use_json: bool = True): + super().__init__() + self.use_json = use_json + self._event_counter: Dict[str, int] = {} + + def _serialize_error(self, exc_info) -> Dict[str, str]: + """Serialize error information into a dictionary.""" + exc_type, exc_value, exc_tb = exc_info + return { + "type": exc_type.__name__ if exc_type else "Unknown", + "message": str(exc_value) if exc_value else "", + "stack_trace": self.formatException(exc_info) if exc_tb else "" + } + + def format(self, record: logging.LogRecord) -> str: + timestamp = datetime.datetime.fromtimestamp(record.created).strftime("%Y-%m-%dT%H:%M:%S") + + # Extract additional fields if they exist + extra_fields = {} + for key, value in vars(record).items(): + if key not in logging.LogRecord.__dict__ and not key.startswith('_'): + extra_fields[key] = value + + if self.use_json: + log_entry = { + "timestamp": timestamp, + "level": record.levelname, + "logger": record.name or "root", + "message": record.getMessage(), + **extra_fields + } + + if hasattr(record, 'event_type'): + log_entry["event_type"] = getattr(record, 'event_type') + + if hasattr(record, 'event_data'): + log_entry["data"] = getattr(record, 'event_data') + + if record.exc_info and record.levelno >= logging.ERROR: + log_entry["error"] = self._serialize_error(record.exc_info) + + return json.dumps(log_entry, cls=LogJSONEncoder) + else: + # Compact format for non-JSON logs + basic_msg = f"[{timestamp}] {record.levelname[0]}: {record.getMessage()}" + + if record.exc_info and record.levelno >= logging.ERROR: + return f"{basic_msg}\n{self.formatException(record.exc_info)}" + + return basic_msg + +class BatchedEventLogger: + def __init__(self, logger: logging.Logger): + self._logger = logger + self._batched_events: Dict[str, List[Dict[str, Any]]] = {} + + def add_event(self, event_type: str, event_data: Dict[str, Any]) -> None: + if event_type not in self._batched_events: + self._batched_events[event_type] = [] + self._batched_events[event_type].append(event_data) + + def flush(self) -> None: + for event_type, events in self._batched_events.items(): + if events: + self._logger.info( + f"Batch: {len(events)} {event_type} events", + extra={ + "event_type": f"batched_{event_type}", + "event_data": { + "count": len(events), + "events": events + } + } + ) + self._batched_events.clear() + +def setup_logging( + level: str = "INFO", + use_json: bool = True, + log_file: Optional[str] = None, + exclude_patterns: Optional[List[str]] = None +) -> None: + """ + Setup logging configuration with the improved formatter + + Args: + level: The logging level to use + use_json: Whether to use JSON formatting + log_file: Optional file to write logs to + exclude_patterns: Optional list of patterns to exclude from logging + """ + root_logger = logging.getLogger() + root_logger.setLevel(level) + + # Clear any existing handlers + root_logger.handlers.clear() + + # Create console handler + console_handler = logging.StreamHandler() + console_handler.setFormatter(LogFormatter(use_json=use_json)) + + if exclude_patterns: + class ExcludeFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + return not any(pattern in record.getMessage() for pattern in exclude_patterns) + + console_handler.addFilter(ExcludeFilter()) + + root_logger.addHandler(console_handler) + + # Add file handler if specified + if log_file: + file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(LogFormatter(use_json=True)) # Always use JSON for file logging + if exclude_patterns: + file_handler.addFilter(ExcludeFilter()) + root_logger.addHandler(file_handler) + +# Production filter patterns +PRODUCTION_EXCLUDE_PATTERNS = [ + "deprecated", + "virtual environment", + "Activating virtual environment", + "✅ Eval: Success", + "🤔 Thought:", + "VIRTUAL_ENV:" +] \ No newline at end of file diff --git a/src/utils/structured_logging.py b/src/utils/structured_logging.py new file mode 100644 index 00000000..8568de23 --- /dev/null +++ b/src/utils/structured_logging.py @@ -0,0 +1,223 @@ +from typing import Optional, Dict, Any, List +import logging +import json +from dataclasses import dataclass, asdict +from datetime import datetime +from colorama import init, Fore, Style +import os + +# Initialize colorama +init() + +@dataclass +class ColorScheme: + """Color scheme for different log elements.""" + ERROR: str = Fore.RED + WARNING: str = Fore.YELLOW + INFO: str = Fore.CYAN + DEBUG: str = Style.DIM + TIMESTAMP: str = Fore.WHITE + SUCCESS: str = Fore.GREEN + STEP: str = Fore.BLUE + RESET: str = Style.RESET_ALL + +class ColorizedFormatter(logging.Formatter): + """Formatter that adds colors to log output.""" + + def __init__(self, use_colors: bool = True): + super().__init__() + self.use_colors = use_colors and not os.getenv('NO_COLOR') + self.colors = ColorScheme() + + def colorize(self, text: str, color: str) -> str: + """Add color to text if colors are enabled.""" + if self.use_colors: + return f"{color}{text}{self.colors.RESET}" + return text + + def format(self, record: logging.LogRecord) -> str: + """Format the log record with colors.""" + # Get the appropriate color for the log level + level_color = getattr(self.colors, record.levelname, self.colors.INFO) + + # Format timestamp + timestamp = self.colorize( + datetime.utcnow().strftime("%H:%M:%S"), + self.colors.TIMESTAMP + ) + + # Format level + level = self.colorize(record.levelname, level_color) + + # Format message and handle special keywords + msg = record.getMessage() + if "✓" in msg: + msg = msg.replace("✓", self.colorize("✓", self.colors.SUCCESS)) + if "×" in msg: + msg = msg.replace("×", self.colorize("×", self.colors.ERROR)) + if "STEP" in msg: + msg = msg.replace("STEP", self.colorize("STEP", self.colors.STEP)) + + # Build the basic log message + log_message = f"[{timestamp}] {level} {msg}" + + # Add structured data if available + if hasattr(record, 'event_type'): + event_type = self.colorize(record.event_type, self.colors.INFO) + if hasattr(record, 'data'): + # Format the data as JSON but don't colorize it + data_str = json.dumps(record.data, indent=2) + log_message = f"{log_message} | {event_type} | {data_str}" + + return log_message + +class JSONFormatter(logging.Formatter): + """Custom JSON formatter for structured logs.""" + + def format(self, record: logging.LogRecord) -> str: + """Format the log record as a JSON string.""" + output = { + "timestamp": datetime.utcnow().isoformat(), + "level": record.levelname, + "message": record.getMessage(), + "logger": record.name + } + + # Add extra fields from record.__dict__ to handle custom attributes + if hasattr(record, '__dict__'): + for key, value in record.__dict__.items(): + if key not in output and key not in ('args', 'exc_info', 'exc_text', 'msg'): + output[key] = value + + return json.dumps(output) + +def setup_structured_logging(level: int = logging.INFO, use_colors: bool = True, json_output: bool = False) -> None: + """Set up structured logging with optional colorized output.""" + root_logger = logging.getLogger() + root_logger.setLevel(level) + + # Remove existing handlers + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # Create console handler with appropriate formatter + handler = logging.StreamHandler() + if json_output: + handler.setFormatter(JSONFormatter()) + else: + handler.setFormatter(ColorizedFormatter(use_colors=use_colors)) + + root_logger.addHandler(handler) + +@dataclass +class ProgressEvent: + """Represents a progress update in the browser automation process.""" + step: str + status: str + progress: float # 0.0 to 1.0 + message: str + timestamp: Optional[str] = None + + def __post_init__(self): + if self.timestamp is None: + self.timestamp = datetime.utcnow().isoformat() + +@dataclass +class BrowserEvent: + """Represents a browser-related event.""" + event_type: str + details: Dict[str, Any] + timestamp: Optional[str] = None + + def __post_init__(self): + if self.timestamp is None: + self.timestamp = datetime.utcnow().isoformat() + +class StructuredLogger: + """Handles structured logging with progress reporting and feedback.""" + + def __init__(self, logger_name: str = "browser_automation"): + self.logger = logging.getLogger(logger_name) + self.progress_events: List[ProgressEvent] = [] + self.browser_events: List[BrowserEvent] = [] + self._current_progress: float = 0.0 + + def log_progress(self, step: str, status: str, progress: float, message: str) -> None: + """Log a progress update.""" + event = ProgressEvent(step=step, status=status, progress=progress, message=message) + self.progress_events.append(event) + self._current_progress = progress + + self.logger.info("Progress Update", extra={ + "event_type": "progress", + "data": asdict(event) + }) + + def log_browser_event(self, event_type: str, details: Dict[str, Any]) -> None: + """Log a browser-related event.""" + event = BrowserEvent(event_type=event_type, details=details) + self.browser_events.append(event) + + self.logger.info(f"Browser Event: {event_type}", extra={ + "event_type": "browser", + "data": asdict(event) + }) + + def get_current_progress(self) -> float: + """Get the current progress as a float between 0 and 1.""" + return self._current_progress + + def get_progress_history(self) -> List[Dict[str, Any]]: + """Get the history of progress events.""" + return [asdict(event) for event in self.progress_events] + + def get_browser_events(self) -> List[Dict[str, Any]]: + """Get all browser events.""" + return [asdict(event) for event in self.browser_events] + + def clear_history(self) -> None: + """Clear all stored events.""" + self.progress_events.clear() + self.browser_events.clear() + self._current_progress = 0.0 + +class EventBatcher: + def __init__(self, batch_size: int = 5): + self.events: List[BrowserEvent] = [] + self.batch_size = max(1, batch_size) # Ensure minimum batch size of 1 + + def add_event(self, event: BrowserEvent) -> Optional[Dict[str, Any]]: + self.events.append(event) + if len(self.events) >= self.batch_size: + return self.flush_events() + return None + + def flush_events(self) -> Dict[str, Any]: + if not self.events: + return { + "timestamp": datetime.now().isoformat(), + "total_events": 0, + "success_count": 0, + "error_count": 0, + "duration_ms": 0 + } + + summary = { + "timestamp": datetime.now().isoformat(), + "total_events": len(self.events), + "success_count": sum(1 for e in self.events if e.get_status() == "success"), + "error_count": sum(1 for e in self.events if e.get_status() == "failed"), + "duration_ms": self._calculate_total_duration() + } + self.events = [] + return summary + + def get_event_count(self) -> int: + return len(self.events) + + def _calculate_total_duration(self) -> int: + total_duration = 0 + for event in self.events: + if event.metrics and "duration_ms" in event.metrics: + total_duration += event.metrics["duration_ms"] + return total_duration \ No newline at end of file diff --git a/src/utils/task_logging.py b/src/utils/task_logging.py new file mode 100644 index 00000000..908774a2 --- /dev/null +++ b/src/utils/task_logging.py @@ -0,0 +1,562 @@ +from typing import Dict, Any, List, Literal, Optional, Union, Callable, TypeVar, Awaitable +from dataclasses import dataclass, asdict, field +from datetime import datetime +import json +from enum import Enum +import traceback +import asyncio +import random +import os +from colorama import init, Fore, Style + +# Initialize colorama for cross-platform color support +init() + +# Define generic type parameter at module level +T = TypeVar('T') + +class TaskStatus(str, Enum): + PENDING = "pending" + RUNNING = "running" + COMPLETE = "complete" + FAILED = "failed" + +class ActionType(str, Enum): + NAVIGATION = "navigation" + INTERACTION = "interaction" + EXTRACTION = "extraction" + VALIDATION = "validation" + RECOVERY = "recovery" + + @property + def emoji(self) -> str: + """Get the emoji representation of the action type.""" + return { + ActionType.NAVIGATION: "🌐", + ActionType.INTERACTION: "🖱️", + ActionType.EXTRACTION: "📑", + ActionType.VALIDATION: "✅", + ActionType.RECOVERY: "🔄" + }[self] + +@dataclass +class PerformanceMetrics: + """Performance metrics for task execution.""" + total_duration: float = 0.0 + step_breakdown: Dict[str, float] = field(default_factory=dict) + + def add_step_duration(self, step_type: str, duration: float) -> None: + """Add duration for a step type.""" + if step_type not in self.step_breakdown: + self.step_breakdown[step_type] = 0 + self.step_breakdown[step_type] += duration + self.total_duration += duration + + def to_dict(self) -> Dict[str, Any]: + """Convert metrics to a dictionary.""" + return { + "total_duration": self.total_duration, + "step_breakdown": self.step_breakdown + } + +@dataclass +class ErrorInfo: + """Information about an error that occurred.""" + type: str + message: str + step: int + action: str + traceback: Optional[str] = None + +@dataclass +class StepInfo: + """Information about the current step in a task.""" + number: int + description: str + started_at: str + status: Union[TaskStatus, str] + duration: Optional[float] = None + progress: Optional[float] = None + action_type: Optional[ActionType] = None + context: Optional[Dict[str, Any]] = None + results: Optional[Dict[str, Any]] = None + suppress_similar: bool = False + + def __post_init__(self): + if isinstance(self.status, str): + self.status = TaskStatus(self.status) + if isinstance(self.action_type, str): + self.action_type = ActionType(self.action_type) + + @property + def status_value(self) -> str: + """Get the string value of the status.""" + return self.status.value if isinstance(self.status, TaskStatus) else str(self.status) + +@dataclass +class BrowserState: + """Current state of the browser.""" + url: str + page_ready: bool + dynamic_content_loaded: bool + visible_elements: int + current_frame: Optional[str] = None + active_element: Optional[str] = None + page_title: Optional[str] = None + +@dataclass +class RetryConfig: + """Configuration for retry behavior.""" + max_retries: int = 3 + base_delay: float = 1.0 + max_delay: float = 10.0 + jitter: float = 0.1 + + def get_delay(self, attempt: int) -> float: + """Calculate delay for a given attempt using exponential backoff.""" + if attempt == 0: + return 0 + if attempt > self.max_retries: + return -1 + + # Calculate exponential delay + delay = self.base_delay * (2 ** (attempt - 1)) + delay = min(delay, self.max_delay) + + # Add jitter if configured + if self.jitter > 0: + jitter_range = delay * self.jitter + delay += random.uniform(-jitter_range/2, jitter_range/2) + + return max(0, delay) + +@dataclass +class RetryInfo: + """Information about retry attempts.""" + attempts: int = 0 + success: bool = False + history: List[Dict[str, Any]] = field(default_factory=list) + +@dataclass +class TaskContext: + """Context information for a task.""" + id: str + goal: str + current_step: StepInfo + browser_state: BrowserState + started_at: Optional[str] = None + error: Optional[ErrorInfo] = None + performance: Optional[PerformanceMetrics] = None + log_history: List[StepInfo] = field(default_factory=list) + retries: Optional[RetryInfo] = None + + def __post_init__(self): + if self.started_at is None: + self.started_at = datetime.utcnow().isoformat() + if self.performance is None: + self.performance = PerformanceMetrics() + if self.retries is None: + self.retries = RetryInfo() + + def to_dict(self) -> Dict[str, Any]: + """Convert the context to a dictionary for logging.""" + result = { + "timestamp": datetime.utcnow().isoformat(), + "task": { + "id": self.id, + "goal": self.goal, + "progress": self._format_progress(), + "elapsed_time": self._calculate_elapsed_time(), + "status": self.current_step.status_value + } + } + + # Add retry information if available + if self.retries and self.retries.attempts > 0: + result["task"]["retries"] = { + "attempts": self.retries.attempts, + "success": self.retries.success, + "history": self.retries.history + } + + # Add current action information + if self.current_step.action_type: + result["task"]["current_action"] = self.current_step.action_type.value + if self.current_step.context: + result["task"]["action_context"] = self.current_step.context + if self.current_step.results: + result["task"]["action_results"] = self.current_step.results + + # Add browser state + result["browser"] = { + "url": self.browser_state.url, + "state": "ready" if self.browser_state.page_ready else "loading", + "visible_elements": self.browser_state.visible_elements, + "dynamic_content": "loaded" if self.browser_state.dynamic_content_loaded else "loading" + } + + if self.browser_state.current_frame: + result["browser"]["current_frame"] = self.browser_state.current_frame + if self.browser_state.active_element: + result["browser"]["active_element"] = self.browser_state.active_element + if self.browser_state.page_title: + result["browser"]["page_title"] = self.browser_state.page_title + + if self.error: + result["error"] = { + "type": self.error.type, + "message": self.error.message, + "step": self.error.step, + "action": self.error.action + } + if self.error.traceback: + result["error"]["traceback"] = self.error.traceback + + if self.performance and self.performance.step_breakdown: + result["performance"] = self.performance.to_dict() + + return result + + def _format_progress(self) -> str: + """Format the progress information.""" + if self.current_step.progress is not None: + return f"{int(self.current_step.progress * 100)}%" + return f"{self.current_step.number}/unknown steps" + + def _calculate_elapsed_time(self) -> str: + """Calculate the elapsed time since task start.""" + if self.started_at is None: + return "0.0s" + start = datetime.fromisoformat(self.started_at) + elapsed = datetime.utcnow() - start + return f"{elapsed.total_seconds():.1f}s" + +@dataclass +class ColorScheme: + """Color scheme for log messages.""" + error: str = Fore.RED + warning: str = Fore.YELLOW + info: str = Fore.CYAN + success: str = Fore.GREEN + reset: str = Style.RESET_ALL + + @property + def enabled(self) -> bool: + """Check if colors should be enabled.""" + return not bool(os.getenv("NO_COLOR")) + + def apply(self, text: str, color: str) -> str: + """Apply color to text if colors are enabled.""" + if not self.enabled: + return text + return f"{color}{text}{self.reset}" + +class LogFormatter: + """Formatter for log messages with color support.""" + + def __init__(self, color_scheme: Optional[ColorScheme] = None): + self.colors = color_scheme or ColorScheme() + + def format(self, record: Any) -> str: + """Format a log record with appropriate colors.""" + level_colors = { + "ERROR": self.colors.error, + "WARNING": self.colors.warning, + "INFO": self.colors.info + } + + # Format timestamp + timestamp = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S") + + # Color the level name + level_color = level_colors.get(record.levelname, self.colors.info) + colored_level = self.colors.apply(record.levelname, level_color) + + return f"[{timestamp}] {colored_level}: {record.msg}" + +@dataclass +class SeparatorStyle: + """Style configuration for visual separators.""" + task: str = "=" * 50 # Task separator (longer) + phase: str = "-" * 30 # Phase separator (medium) + error: str = "*" * 40 # Error separator (distinct) + +class TaskLogger: + """Advanced logger for task context and state tracking.""" + + def __init__( + self, + task_id: str, + goal: str, + color_scheme: Optional[ColorScheme] = None, + separator_style: Optional[SeparatorStyle] = None, + use_separators: bool = True + ): + self.context = TaskContext( + id=task_id, + goal=goal, + current_step=StepInfo( + number=1, + description="Task initialized", + started_at=datetime.utcnow().isoformat(), + status=TaskStatus.PENDING + ), + browser_state=BrowserState( + url="", + page_ready=False, + dynamic_content_loaded=False, + visible_elements=0 + ), + retries=RetryInfo() + ) + self._step_start_time: Optional[datetime] = None + self.colors = color_scheme or ColorScheme() + self.separators = separator_style or SeparatorStyle() + self.use_separators = use_separators + + # Add initial task separator and goal + if self.use_separators: + self._add_separator("task") + self._add_log_entry(f"TASK GOAL: {goal}") + + def start_phase(self, phase_name: str) -> None: + """Start a new phase in the task.""" + if self.use_separators: + self._add_separator("phase") + self._add_log_entry(f"PHASE: {phase_name}") + + def _add_separator(self, separator_type: Literal["task", "phase", "error"]) -> None: + """Add a visual separator to the log history.""" + if not self.use_separators: + return + + separator = getattr(self.separators, separator_type) + colored_separator = self.colors.apply( + separator, + self.colors.info if separator_type != "error" else self.colors.error + ) + self._add_log_entry(colored_separator) + + def _add_log_entry(self, entry: str) -> None: + """Add a raw log entry to the history.""" + step = StepInfo( + number=self.context.current_step.number, + description=entry, + started_at=datetime.utcnow().isoformat(), + status=TaskStatus.RUNNING + ) + self.context.log_history.append(step) + + def update_step(self, + description: str, + status: TaskStatus, + progress: Optional[float] = None, + action_type: Optional[ActionType] = None, + context: Optional[Dict[str, Any]] = None, + results: Optional[Dict[str, Any]] = None, + suppress_similar: bool = False) -> None: + """Update the current step information.""" + step_duration = None + if self._step_start_time: + step_duration = (datetime.utcnow() - self._step_start_time).total_seconds() + + new_step = StepInfo( + number=self.context.current_step.number + 1, + description=description, + started_at=datetime.utcnow().isoformat(), + status=status, + duration=step_duration, + progress=progress, + action_type=action_type, + context=context, + results=results, + suppress_similar=suppress_similar + ) + + # Check if we should suppress this step + if not suppress_similar or not self._is_similar_to_previous(new_step): + self.context.log_history.append(new_step) + self.context.current_step = new_step + self._step_start_time = datetime.utcnow() + else: + # Update the previous step with new status/results + prev_step = self.context.log_history[-1] + prev_step.status = status + if results: + prev_step.results = results + # Update current step to reflect changes + self.context.current_step = prev_step + + def _is_similar_to_previous(self, step: StepInfo) -> bool: + """Check if a step is similar to the previous one.""" + if not self.context.log_history: + return False + prev_step = self.context.log_history[-1] + return ( + prev_step.action_type == step.action_type and + prev_step.description.split()[0] == step.description.split()[0] # Compare first word + ) + + def get_log_history(self) -> List[str]: + """Get the formatted history of log entries.""" + return [self._format_step(step) for step in self.context.log_history] + + def _format_step(self, step: StepInfo) -> str: + """Format a step as a log entry with colors.""" + timestamp = datetime.fromisoformat(step.started_at).strftime("%Y-%m-%d %H:%M:%S") + duration = f"({step.duration:.1f}s)" if step.duration is not None else "" + + # Color-coded status symbols + if isinstance(step.status, TaskStatus): + status_symbol = { + TaskStatus.COMPLETE: self.colors.apply("✓", self.colors.success), + TaskStatus.FAILED: self.colors.apply("×", self.colors.error), + TaskStatus.RUNNING: self.colors.apply("→", self.colors.info), + TaskStatus.PENDING: self.colors.apply("→", self.colors.info) + }.get(step.status, self.colors.apply("→", self.colors.info)) + else: + status_symbol = self.colors.apply("→", self.colors.info) + + # Color-coded action emoji + action_emoji = step.action_type.emoji if step.action_type else "" + if action_emoji: + action_emoji = self.colors.apply(action_emoji, self.colors.info) + + # Format step number with info color + step_number = self.colors.apply(f"STEP {step.number}/?", self.colors.info) + + return f"[{timestamp}] {action_emoji} {step_number} {step.description} {status_symbol} {duration}" + + def format_log_entry(self) -> str: + """Format the current state as a log entry.""" + return self._format_step(self.context.current_step) + + def update_browser_state(self, + url: Optional[str] = None, + page_ready: Optional[bool] = None, + dynamic_content_loaded: Optional[bool] = None, + visible_elements: Optional[int] = None, + current_frame: Optional[str] = None, + active_element: Optional[str] = None, + page_title: Optional[str] = None) -> None: + """Update the browser state information.""" + if url is not None: + self.context.browser_state.url = url + if page_ready is not None: + self.context.browser_state.page_ready = page_ready + if dynamic_content_loaded is not None: + self.context.browser_state.dynamic_content_loaded = dynamic_content_loaded + if visible_elements is not None: + self.context.browser_state.visible_elements = visible_elements + if current_frame is not None: + self.context.browser_state.current_frame = current_frame + if active_element is not None: + self.context.browser_state.active_element = active_element + if page_title is not None: + self.context.browser_state.page_title = page_title + + def log_error(self, error: Exception, step_number: int, action: str) -> None: + """Log an error with context.""" + if self.use_separators: + self._add_separator("error") + + self.context.error = ErrorInfo( + type=error.__class__.__name__, + message=str(error), + step=step_number, + action=action, + traceback=traceback.format_exc() + ) + self.context.current_step.status = TaskStatus.FAILED + + if self.use_separators: + self._add_separator("error") + + def start_performance_tracking(self) -> None: + """Start tracking performance metrics.""" + self._step_start_time = datetime.utcnow() + + def track_step_duration(self, step_type: str, duration: float) -> None: + """Track the duration of a specific step type.""" + if self.context.performance is not None: + self.context.performance.add_step_duration(step_type, duration) + + def get_performance_metrics(self) -> Dict[str, Any]: + """Get the current performance metrics.""" + if self.context.performance is not None: + return self.context.performance.to_dict() + return {"total_duration": 0.0, "step_breakdown": {}} + + def get_context(self) -> Dict[str, Any]: + """Get the current context as a dictionary.""" + return self.context.to_dict() + + def log_state(self) -> None: + """Log the current state.""" + state = self.get_context() + print(json.dumps(state, indent=2)) + + async def execute_with_retry( + self, + operation: Callable[[], Awaitable[T]], + operation_name: str, + retry_config: Optional[RetryConfig] = None + ) -> T: + """Execute an operation with retry logic.""" + if retry_config is None: + retry_config = RetryConfig() + + attempt = 0 + last_error = None + + while True: + try: + # Calculate and apply delay if this is a retry + delay = retry_config.get_delay(attempt) + if delay == -1: # Max retries exceeded + if last_error: + raise last_error + raise Exception("Max retries exceeded") + + if delay > 0: + await asyncio.sleep(delay) + + # Attempt the operation + result = await operation() + + # Update retry info on success + if self.context.retries is not None: + self.context.retries.attempts = attempt + 1 + self.context.retries.success = True + + return result + + except Exception as e: + last_error = e + attempt += 1 + + # Log the retry attempt + if self.context.retries is not None: + self.context.retries.history.append({ + "attempt": attempt, + "timestamp": datetime.utcnow().isoformat(), + "error": f"{e.__class__.__name__}: {str(e)}", + "delay": retry_config.get_delay(attempt) + }) + + # Update the error context + self.log_error(e, self.context.current_step.number, operation_name) + + # Continue if we haven't exceeded max retries + if attempt <= retry_config.max_retries: + self.update_step( + f"Retrying {operation_name} (attempt {attempt + 1}/{retry_config.max_retries + 1})", + TaskStatus.RUNNING + ) + continue + + # Max retries exceeded + if self.context.retries is not None: + self.context.retries.attempts = attempt + self.context.retries.success = False + raise \ No newline at end of file diff --git a/tests/test_browser_controller.py b/tests/test_browser_controller.py new file mode 100644 index 00000000..409d8d33 --- /dev/null +++ b/tests/test_browser_controller.py @@ -0,0 +1,125 @@ +import pytest +from unittest.mock import AsyncMock, patch, MagicMock +import asyncio +from src.utils.browser_controller import BrowserController + +@pytest.fixture +async def browser_controller(): + controller = BrowserController() + yield controller + await controller.cleanup() + +@pytest.mark.asyncio +async def test_single_initialization(browser_controller): + mock_browser = AsyncMock() + mock_playwright = AsyncMock() + mock_playwright.chromium.launch = AsyncMock(return_value=mock_browser) + + with patch('src.utils.browser_controller.async_playwright', + return_value=AsyncMock(start=AsyncMock(return_value=mock_playwright))) as mock_async_playwright: + await browser_controller.initialize() + assert browser_controller.init_count == 1 + assert browser_controller.browser == mock_browser + + # Verify progress events + progress_history = browser_controller.logger.get_progress_history() + assert len(progress_history) >= 2 # At least start and complete events + assert progress_history[0]["status"] == "starting" + assert progress_history[-1]["status"] == "completed" + assert progress_history[-1]["progress"] == 1.0 + + # Second initialization should not create new browser + await browser_controller.initialize() + assert browser_controller.init_count == 1 + mock_async_playwright.assert_called_once() + +@pytest.mark.asyncio +async def test_concurrent_initialization(browser_controller): + mock_browser = AsyncMock() + mock_playwright = AsyncMock() + mock_playwright.chromium.launch = AsyncMock(return_value=mock_browser) + + with patch('src.utils.browser_controller.async_playwright', + return_value=AsyncMock(start=AsyncMock(return_value=mock_playwright))): + # Start multiple concurrent initializations + tasks = [browser_controller.initialize() for _ in range(3)] + await asyncio.gather(*tasks) + + # Should only initialize once + assert browser_controller.init_count == 1 + assert browser_controller.browser == mock_browser + + # Verify browser events + browser_events = browser_controller.logger.get_browser_events() + launch_events = [e for e in browser_events if e["event_type"] == "browser_launched"] + assert len(launch_events) == 1 + +@pytest.mark.asyncio +async def test_browser_launch_options(browser_controller): + mock_browser = AsyncMock() + mock_playwright = AsyncMock() + mock_playwright.chromium.launch = AsyncMock(return_value=mock_browser) + + with patch('src.utils.browser_controller.async_playwright', + return_value=AsyncMock(start=AsyncMock(return_value=mock_playwright))) as mock_async_playwright: + await browser_controller.initialize() + + # Verify launch options + mock_playwright.chromium.launch.assert_called_once_with( + headless=True, + args=['--no-sandbox'] + ) + + # Verify browser events + browser_events = browser_controller.logger.get_browser_events() + launch_event = next(e for e in browser_events if e["event_type"] == "browser_launched") + assert launch_event["details"]["headless"] is True + +@pytest.mark.asyncio +async def test_initialization_failure(browser_controller): + mock_playwright = AsyncMock() + mock_playwright.chromium.launch = AsyncMock(side_effect=Exception("Browser launch failed")) + + with patch('src.utils.browser_controller.async_playwright', + return_value=AsyncMock(start=AsyncMock(return_value=mock_playwright))), \ + pytest.raises(Exception, match="Browser launch failed"): + await browser_controller.initialize() + + assert browser_controller.browser is None + assert browser_controller.init_count == 0 + + # Verify error events + browser_events = browser_controller.logger.get_browser_events() + error_event = next(e for e in browser_events if e["event_type"] == "launch_failed") + assert "Browser launch failed" in error_event["details"]["error"] + + # Verify progress events show failure + progress_events = browser_controller.logger.get_progress_history() + final_event = progress_events[-1] + assert final_event["status"] == "failed" + assert final_event["progress"] == 0.0 + +@pytest.mark.asyncio +async def test_browser_cleanup(browser_controller): + mock_browser = AsyncMock() + mock_playwright = AsyncMock() + mock_playwright.chromium.launch = AsyncMock(return_value=mock_browser) + + with patch('src.utils.browser_controller.async_playwright', + return_value=AsyncMock(start=AsyncMock(return_value=mock_playwright))): + await browser_controller.initialize() + assert browser_controller.browser is not None + + await browser_controller.cleanup() + mock_browser.close.assert_called_once() + mock_playwright.stop.assert_called_once() + assert browser_controller.browser is None + assert browser_controller._playwright is None + + # Verify cleanup events + progress_events = browser_controller.logger.get_progress_history() + cleanup_events = [e for e in progress_events if e["step"] == "cleanup"] + assert len(cleanup_events) >= 2 # At least start and complete events + assert cleanup_events[0]["status"] == "starting" + assert cleanup_events[-1]["status"] == "completed" + assert cleanup_events[-1]["progress"] == 1.0 \ No newline at end of file diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py new file mode 100644 index 00000000..653f0683 --- /dev/null +++ b/tests/test_error_handling.py @@ -0,0 +1,98 @@ +import pytest +from datetime import datetime +from typing import Dict, Any, Optional +import asyncio +from src.utils.error_handling import ErrorHandler, MaxRetriesExceededError + +class TestErrorHandler: + @pytest.fixture + def handler(self): + return ErrorHandler() + + @pytest.mark.asyncio + async def test_max_retries_exceeded(self, handler): + operation = "test_operation" + error = ValueError("Test error") + + # Should handle first three attempts + for _ in range(3): + await handler.handle_error(error, operation) + + # Fourth attempt should raise MaxRetriesExceededError + with pytest.raises(MaxRetriesExceededError) as exc_info: + await handler.handle_error(error, operation) + + assert exc_info.value.operation == operation + assert exc_info.value.original_error == error + + @pytest.mark.asyncio + async def test_error_logging(self, handler): + operation = "test_operation" + error = ValueError("Test error") + + # First attempt + await handler.handle_error(error, operation) + + # Get the last logged error + last_error = handler.get_last_error() + assert last_error["operation"] == operation + assert last_error["attempt"] == 1 + assert "timestamp" in last_error + assert last_error["error"]["name"] == "ValueError" + assert last_error["error"]["message"] == "Test error" + + @pytest.mark.asyncio + async def test_exponential_backoff(self, handler): + operation = "test_operation" + error = ValueError("Test error") + + # Record start time + start = datetime.now() + + # First attempt (should delay 1 second) + await handler.handle_error(error, operation) + + # Second attempt (should delay 2 seconds) + await handler.handle_error(error, operation) + + # Calculate duration + duration = (datetime.now() - start).total_seconds() + + # Should have waited at least 3 seconds (1 + 2) + assert duration >= 3 + + @pytest.mark.asyncio + async def test_error_code_extraction(self, handler): + # Test with connection error + error = ConnectionError("ERR_CONNECTION_REFUSED: Failed to connect") + code = handler.extract_error_code(error) + assert code == "ERR_CONNECTION_REFUSED" + + # Test with DNS error + error = Exception("ERR_NAME_NOT_RESOLVED: Could not resolve hostname") + code = handler.extract_error_code(error) + assert code == "ERR_NAME_NOT_RESOLVED" + + # Test with unknown error + error = ValueError("Some other error") + code = handler.extract_error_code(error) + assert code == "UNKNOWN_ERROR" + + @pytest.mark.asyncio + async def test_concurrent_retries(self, handler): + operation = "test_operation" + error = ValueError("Test error") + + # Try to handle the same error concurrently + tasks = [ + handler.handle_error(error, operation), + handler.handle_error(error, operation), + handler.handle_error(error, operation) + ] + + # Should complete without raising an error + await asyncio.gather(*tasks, return_exceptions=True) + + # Fourth attempt should still raise MaxRetriesExceededError + with pytest.raises(MaxRetriesExceededError): + await handler.handle_error(error, operation) \ No newline at end of file diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 00000000..5e871e0a --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,216 @@ +import json +import logging +import datetime +import pytest +from io import StringIO +from typing import Dict, Any +from src.utils.logging import ( + LogFormatter, + BatchedEventLogger, + setup_logging, + PRODUCTION_EXCLUDE_PATTERNS, + LogLevel +) +import sys + +class TestLogFormatter: + @pytest.fixture + def json_formatter(self): + return LogFormatter(use_json=True) + + @pytest.fixture + def compact_formatter(self): + return LogFormatter(use_json=False) + + def test_json_format_basic_log(self, json_formatter): + record = logging.LogRecord( + name="test_logger", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Test message", + args=(), + exc_info=None + ) + + formatted = json_formatter.format(record) + parsed = json.loads(formatted) + + assert parsed["level"] == "INFO" + assert parsed["logger"] == "test_logger" + assert parsed["message"] == "Test message" + assert "timestamp" in parsed + + def test_json_format_with_extra_fields(self, json_formatter): + record = logging.LogRecord( + name="test_logger", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Test message", + args=(), + exc_info=None + ) + record.event_type = "test_event" + record.event_data = {"key": "value"} + + formatted = json_formatter.format(record) + parsed = json.loads(formatted) + + assert parsed["event_type"] == "test_event" + assert parsed["data"] == {"key": "value"} + + def test_json_format_with_error(self, json_formatter): + try: + raise ValueError("Test error") + except ValueError as e: + record = logging.LogRecord( + name="test_logger", + level=logging.ERROR, + pathname="test.py", + lineno=1, + msg="Error occurred", + args=(), + exc_info=sys.exc_info() + ) + + formatted = json_formatter.format(record) + parsed = json.loads(formatted) + + assert parsed["error"]["type"] == "ValueError" + assert parsed["error"]["message"] == "Test error" + assert "stack_trace" in parsed["error"] + + def test_compact_format_basic_log(self, compact_formatter): + record = logging.LogRecord( + name="test_logger", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Test message", + args=(), + exc_info=None + ) + + formatted = compact_formatter.format(record) + assert "] I: Test message" in formatted + + def test_compact_format_with_error(self, compact_formatter): + try: + raise ValueError("Test error") + except ValueError as e: + record = logging.LogRecord( + name="test_logger", + level=logging.ERROR, + pathname="test.py", + lineno=1, + msg="Error occurred", + args=(), + exc_info=sys.exc_info() + ) + + formatted = compact_formatter.format(record) + assert "] E: Error occurred" in formatted + assert "ValueError: Test error" in formatted + +class TestBatchedEventLogger: + @pytest.fixture + def string_io(self): + return StringIO() + + @pytest.fixture + def logger(self, string_io): + handler = logging.StreamHandler(string_io) + handler.setFormatter(LogFormatter(use_json=True)) + logger = logging.getLogger("test_batched") + logger.handlers = [handler] + logger.setLevel(logging.INFO) + return logger + + @pytest.fixture + def batched_logger(self, logger): + return BatchedEventLogger(logger) + + def test_batch_single_event(self, batched_logger, string_io): + event_data = {"action": "click", "element": "button"} + batched_logger.add_event("ui_action", event_data) + batched_logger.flush() + + output = string_io.getvalue() + parsed = json.loads(output) + + assert parsed["event_type"] == "batched_ui_action" + assert parsed["data"]["count"] == 1 + assert parsed["data"]["events"][0] == event_data + + def test_batch_multiple_events(self, batched_logger, string_io): + events = [ + {"action": "click", "element": "button1"}, + {"action": "type", "element": "input1"}, + {"action": "click", "element": "button2"} + ] + + for event in events: + batched_logger.add_event("ui_action", event) + + batched_logger.flush() + + output = string_io.getvalue() + parsed = json.loads(output) + + assert parsed["event_type"] == "batched_ui_action" + assert parsed["data"]["count"] == 3 + assert parsed["data"]["events"] == events + +class TestLoggingSetup: + @pytest.fixture + def temp_logger(self): + # Store original handlers + root_logger = logging.getLogger() + original_handlers = root_logger.handlers[:] + + yield root_logger + + # Restore original handlers + root_logger.handlers = original_handlers + + def test_setup_basic_logging(self, temp_logger): + setup_logging(level="INFO", use_json=True) + assert len(temp_logger.handlers) == 1 + assert isinstance(temp_logger.handlers[0].formatter, LogFormatter) + assert temp_logger.level == logging.INFO + + def test_setup_with_exclude_patterns(self, temp_logger): + test_patterns = ["debug", "deprecated"] + setup_logging(level="INFO", exclude_patterns=test_patterns) + + # Create a test record that should be filtered + record = logging.LogRecord( + name="test_logger", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="This is a debug message", + args=(), + exc_info=None + ) + + # The record should be filtered out + assert not temp_logger.handlers[0].filter(record) + + def test_production_exclude_patterns(self): + # Verify that all production patterns are strings + assert all(isinstance(pattern, str) for pattern in PRODUCTION_EXCLUDE_PATTERNS) + + # Verify that common patterns are included + common_patterns = ["deprecated", "virtual environment"] + assert all(pattern in PRODUCTION_EXCLUDE_PATTERNS for pattern in common_patterns) + +def test_log_levels(): + # Test that all expected log levels are defined + expected_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE"] + assert all(level in LogLevel.__members__ for level in expected_levels) + + # Test that the values match the names + for level in LogLevel: + assert level.value == level.name \ No newline at end of file diff --git a/tests/test_logging_integration.py b/tests/test_logging_integration.py new file mode 100644 index 00000000..3fa7a1f5 --- /dev/null +++ b/tests/test_logging_integration.py @@ -0,0 +1,219 @@ +import json +import logging +import pytest +import asyncio +from pathlib import Path +from io import StringIO +from typing import Dict, Any, List, Optional + +from src.utils.logging import LogFormatter, BatchedEventLogger, setup_logging +from src.agent.custom_agent import CustomAgent +from browser_use.agent.views import ActionResult +from browser_use.browser.views import BrowserStateHistory +from browser_use.browser.browser import Browser, BrowserConfig +from browser_use.browser.context import BrowserContext, BrowserContextConfig +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.messages import BaseMessage + +class MockElementTree: + def clickable_elements_to_string(self, include_attributes=None): + return "Mock clickable elements" + +class MockBrowserContext(BrowserContext): + def __init__(self): + self.config = BrowserContextConfig() + self.selector_map = {} + self.cached_state = BrowserStateHistory( + url="https://example.com", + title="Example Page", + tabs=[], + interacted_element=[None], + screenshot=None + ) + setattr(self.cached_state, 'selector_map', self.selector_map) + setattr(self.cached_state, 'element_tree', MockElementTree()) + + async def get_state(self, use_vision=True): + return self.cached_state + + async def close(self): + pass + + def __del__(self): + # Override to prevent errors about missing session attribute + pass + +class MockBrowser(Browser): + def __init__(self): + self.config = BrowserConfig() + + async def new_context(self, config): + return MockBrowserContext() + + async def close(self): + pass + +class MockLLM(BaseChatModel): + def with_structured_output(self, output_type, include_raw=False): + self._output_type = output_type + return self + + async def ainvoke(self, messages: List[BaseMessage], **kwargs): + return { + 'parsed': self._output_type( + action=[], + current_state={ + 'prev_action_evaluation': 'Success', + 'important_contents': 'Test memory', + 'completed_contents': 'Test progress', + 'thought': 'Test thought', + 'summary': 'Test summary' + } + ) + } + + @property + def _llm_type(self) -> str: + return "mock" + + def _generate(self, messages: List[BaseMessage], stop: Optional[List[str]] = None, run_manager = None, **kwargs): + raise NotImplementedError("Use ainvoke instead") + + @property + def _identifying_params(self) -> Dict[str, Any]: + return {"mock_param": True} + +class ErrorLLM(MockLLM): + async def ainvoke(self, messages: List[BaseMessage], **kwargs): + raise ValueError("Test error") + +class ActionLLM(MockLLM): + async def ainvoke(self, messages: List[BaseMessage], **kwargs): + return { + 'parsed': self._output_type( + action=[ + {'type': 'click', 'selector': '#button1'}, + {'type': 'type', 'selector': '#input1', 'text': 'test'}, + ], + current_state={ + 'prev_action_evaluation': 'Success', + 'important_contents': 'Test memory', + 'completed_contents': 'Test progress', + 'thought': 'Test thought', + 'summary': 'Test summary' + } + ) + } + +@pytest.fixture +def logger(): + # Configure root logger + root_logger = logging.getLogger() + root_logger.setLevel(logging.INFO) + + # Configure test logger + logger = logging.getLogger("test_integration") + logger.setLevel(logging.INFO) + return logger + +@pytest.fixture +def string_io(): + string_io = StringIO() + handler = logging.StreamHandler(string_io) + handler.setFormatter(LogFormatter(use_json=True)) + + # Add handler to root logger + root_logger = logging.getLogger() + root_logger.addHandler(handler) + + # Add handler to test logger + logger = logging.getLogger("test_integration") + logger.addHandler(handler) + + yield string_io + + # Clean up + root_logger.removeHandler(handler) + logger.removeHandler(handler) + +@pytest.mark.asyncio +async def test_agent_logging_integration(logger, string_io): + # Setup + agent = CustomAgent( + task="Test task", + llm=MockLLM(), + browser=MockBrowser(), + browser_context=MockBrowserContext(), + use_vision=True + ) + + # Execute a step + await agent.step() + + # Get all log output + log_output = string_io.getvalue() + log_entries = [json.loads(line) for line in log_output.strip().split('\n') if line.strip()] + + # Print log entries for debugging + print("\nLog entries:", log_entries) + + # Verify log entries + assert len(log_entries) > 0 + assert any('Starting step 1' in str(entry.get('msg', '')) for entry in log_entries) + assert any('Model Response: success' in str(entry.get('msg', '')) for entry in log_entries) + assert any('Step error' in str(entry.get('msg', '')) for entry in log_entries) + +@pytest.mark.asyncio +async def test_agent_error_logging(logger, string_io): + # Setup + agent = CustomAgent( + task="Test task", + llm=ErrorLLM(), + browser=MockBrowser(), + browser_context=MockBrowserContext(), + use_vision=True + ) + + # Execute a step + await agent.step() + + # Get all log output + log_output = string_io.getvalue() + log_entries = [json.loads(line) for line in log_output.strip().split('\n') if line.strip()] + + # Print log entries for debugging + print("\nLog entries:", log_entries) + + # Verify log entries + assert len(log_entries) > 0 + assert any('Starting step 1' in str(entry.get('msg', '')) for entry in log_entries) + assert any('Step error' in str(entry.get('msg', '')) for entry in log_entries) + assert any('Use ainvoke instead' in str(entry.get('msg', '')) for entry in log_entries) + +@pytest.mark.asyncio +async def test_agent_batched_logging(logger, string_io): + # Setup + agent = CustomAgent( + task="Test task", + llm=ActionLLM(), + browser=MockBrowser(), + browser_context=MockBrowserContext(), + use_vision=True + ) + + # Execute a step + await agent.step() + + # Get all log output + log_output = string_io.getvalue() + log_entries = [json.loads(line) for line in log_output.strip().split('\n') if line.strip()] + + # Print log entries for debugging + print("\nLog entries:", log_entries) + + # Verify log entries + assert len(log_entries) > 0 + assert any('Starting step 1' in str(entry.get('msg', '')) for entry in log_entries) + assert any('Model Response: success' in str(entry.get('msg', '')) for entry in log_entries) + assert any('Batch: 2 action events' in str(entry.get('msg', '')) for entry in log_entries) + assert any('Step error' in str(entry.get('msg', '')) for entry in log_entries) \ No newline at end of file diff --git a/tests/test_structured_logging.py b/tests/test_structured_logging.py new file mode 100644 index 00000000..9134c4f5 --- /dev/null +++ b/tests/test_structured_logging.py @@ -0,0 +1,270 @@ +import pytest +import json +import logging +import os +from datetime import datetime +from src.utils.structured_logging import ( + StructuredLogger, + ProgressEvent, + BrowserEvent, + JSONFormatter, + ColorizedFormatter, + ColorScheme, + setup_structured_logging +) +from colorama import Fore, Style + +@pytest.fixture +def structured_logger(): + logger = StructuredLogger("test_logger") + return logger + +def test_progress_event_creation(): + event = ProgressEvent( + step="test_step", + status="in_progress", + progress=0.5, + message="Testing progress" + ) + assert event.step == "test_step" + assert event.status == "in_progress" + assert event.progress == 0.5 + assert event.message == "Testing progress" + assert event.timestamp is not None + +def test_browser_event_creation(): + details = {"action": "click", "selector": "#button"} + event = BrowserEvent( + event_type="interaction", + details=details + ) + assert event.event_type == "interaction" + assert event.details == details + assert event.timestamp is not None + +def test_progress_logging(structured_logger): + structured_logger.log_progress( + step="test_step", + status="started", + progress=0.0, + message="Starting test" + ) + + history = structured_logger.get_progress_history() + assert len(history) == 1 + assert history[0]["step"] == "test_step" + assert history[0]["status"] == "started" + assert history[0]["progress"] == 0.0 + assert history[0]["message"] == "Starting test" + +def test_browser_event_logging(structured_logger): + details = {"page": "test.html", "action": "navigate"} + structured_logger.log_browser_event( + event_type="navigation", + details=details + ) + + events = structured_logger.get_browser_events() + assert len(events) == 1 + assert events[0]["event_type"] == "navigation" + assert events[0]["details"] == details + +def test_progress_tracking(structured_logger): + # Test multiple progress updates + steps = [ + ("step1", "started", 0.0, "Starting"), + ("step1", "in_progress", 0.5, "Halfway"), + ("step1", "completed", 1.0, "Done") + ] + + for step, status, progress, message in steps: + structured_logger.log_progress(step, status, progress, message) + + assert structured_logger.get_current_progress() == 1.0 + history = structured_logger.get_progress_history() + assert len(history) == 3 + + for i, (step, status, progress, message) in enumerate(steps): + assert history[i]["step"] == step + assert history[i]["status"] == status + assert history[i]["progress"] == progress + assert history[i]["message"] == message + +def test_clear_history(structured_logger): + # Add some events + structured_logger.log_progress("test", "started", 0.5, "Test progress") + structured_logger.log_browser_event("test", {"action": "test"}) + + # Clear history + structured_logger.clear_history() + + assert len(structured_logger.get_progress_history()) == 0 + assert len(structured_logger.get_browser_events()) == 0 + assert structured_logger.get_current_progress() == 0.0 + +def test_json_formatter(): + formatter = JSONFormatter() + record = logging.LogRecord( + name="test_logger", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Test message", + args=(), + exc_info=None + ) + + # Add custom fields + setattr(record, 'event_type', 'test_event') + setattr(record, 'data', {'test_key': 'test_value'}) + + formatted = formatter.format(record) + parsed = json.loads(formatted) + + assert parsed["level"] == "INFO" + assert parsed["message"] == "Test message" + assert parsed["logger"] == "test_logger" + assert parsed["event_type"] == "test_event" + assert parsed["data"] == {"test_key": "test_value"} + assert "timestamp" in parsed + +def test_colorized_formatter_with_colors(): + formatter = ColorizedFormatter(use_colors=True) + record = logging.LogRecord( + name="test_logger", + level=logging.ERROR, + pathname="test.py", + lineno=1, + msg="Test error message", + args=(), + exc_info=None + ) + + formatted = formatter.format(record) + assert Fore.RED in formatted # Error level should be red + assert Style.RESET_ALL in formatted # Should have reset codes + assert "[" in formatted and "]" in formatted # Should have timestamp brackets + assert "ERROR" in formatted # Should include level name + +def test_colorized_formatter_without_colors(): + formatter = ColorizedFormatter(use_colors=False) + record = logging.LogRecord( + name="test_logger", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Test message", + args=(), + exc_info=None + ) + + formatted = formatter.format(record) + assert Fore.CYAN not in formatted # Should not have color codes + assert Style.RESET_ALL not in formatted + assert "[" in formatted and "]" in formatted + assert "INFO" in formatted + +def test_colorized_formatter_special_keywords(): + formatter = ColorizedFormatter(use_colors=True) + record = logging.LogRecord( + name="test_logger", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="✓ STEP(1) completed × failed", + args=(), + exc_info=None + ) + + formatted = formatter.format(record) + assert Fore.GREEN in formatted # Success checkmark + assert Fore.BLUE in formatted # STEP keyword + assert Fore.RED in formatted # Error cross + +def test_colorized_formatter_with_structured_data(): + formatter = ColorizedFormatter(use_colors=True) + record = logging.LogRecord( + name="test_logger", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Progress Update", + args=(), + exc_info=None + ) + + # Add structured data + setattr(record, 'event_type', 'progress') + setattr(record, 'data', {'step': 'test', 'progress': 0.5}) + + formatted = formatter.format(record) + assert 'progress' in formatted + assert '"step": "test"' in formatted + assert '"progress": 0.5' in formatted + +def test_color_scheme(): + scheme = ColorScheme() + assert scheme.ERROR == Fore.RED + assert scheme.WARNING == Fore.YELLOW + assert scheme.INFO == Fore.CYAN + assert scheme.DEBUG == Style.DIM + assert scheme.SUCCESS == Fore.GREEN + assert scheme.RESET == Style.RESET_ALL + +def test_no_color_environment_variable(): + os.environ['NO_COLOR'] = '1' + formatter = ColorizedFormatter(use_colors=True) # Even with colors enabled + record = logging.LogRecord( + name="test_logger", + level=logging.ERROR, + pathname="test.py", + lineno=1, + msg="Test message", + args=(), + exc_info=None + ) + + formatted = formatter.format(record) + assert Fore.RED not in formatted # Should not have color codes + assert Style.RESET_ALL not in formatted + + # Clean up + del os.environ['NO_COLOR'] + +def test_setup_structured_logging_with_colors(): + # Remove existing handlers + root_logger = logging.getLogger() + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # Set up logging with colors + setup_structured_logging(level=logging.DEBUG, use_colors=True, json_output=False) + + assert len(root_logger.handlers) == 1 + assert isinstance(root_logger.handlers[0].formatter, ColorizedFormatter) + assert root_logger.handlers[0].formatter.use_colors is True + +def test_setup_structured_logging_json(): + # Remove existing handlers + root_logger = logging.getLogger() + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # Set up logging with JSON output + setup_structured_logging(level=logging.DEBUG, json_output=True) + + assert len(root_logger.handlers) == 1 + assert isinstance(root_logger.handlers[0].formatter, JSONFormatter) + +def test_setup_structured_logging(): + # Remove existing handlers + root_logger = logging.getLogger() + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # Set up logging with default settings + setup_structured_logging(level=logging.DEBUG) + + assert root_logger.level == logging.DEBUG + assert len(root_logger.handlers) == 1 + assert isinstance(root_logger.handlers[0].formatter, ColorizedFormatter) # Default to ColorizedFormatter \ No newline at end of file diff --git a/tests/test_task_logging.py b/tests/test_task_logging.py new file mode 100644 index 00000000..50bf5aae --- /dev/null +++ b/tests/test_task_logging.py @@ -0,0 +1,641 @@ +import pytest +from datetime import datetime, timedelta +import json +import asyncio +import os +from src.utils.task_logging import ( + TaskLogger, + TaskContext, + StepInfo, + BrowserState, + TaskStatus, + PerformanceMetrics, + ErrorInfo, + ActionType, + RetryConfig, + RetryInfo, + ColorScheme, + LogFormatter, + SeparatorStyle +) + +def test_task_logger_initialization(): + logger = TaskLogger("test_task", "Test task goal") + context = logger.get_context() + + assert context["task"]["id"] == "test_task" + assert context["task"]["goal"] == "Test task goal" + assert context["task"]["status"] == "pending" + assert context["browser"]["url"] == "" + assert context["browser"]["state"] == "loading" + assert context["browser"]["visible_elements"] == 0 + assert context["browser"]["dynamic_content"] == "loading" + +def test_step_update(): + logger = TaskLogger("test_task", "Test task goal") + + # Update to running state + logger.update_step("Starting navigation", TaskStatus.RUNNING) + context = logger.get_context() + + assert context["task"]["status"] == "running" + assert context["task"]["progress"] == "2/unknown steps" # Step number incremented + + # Update to complete state + logger.update_step("Navigation complete", TaskStatus.COMPLETE) + context = logger.get_context() + + assert context["task"]["status"] == "complete" + assert context["task"]["progress"] == "3/unknown steps" + +def test_browser_state_update(): + logger = TaskLogger("test_task", "Test task goal") + + # Update browser state + logger.update_browser_state( + url="https://example.com", + page_ready=True, + dynamic_content_loaded=True, + visible_elements=10 + ) + + context = logger.get_context() + assert context["browser"]["url"] == "https://example.com" + assert context["browser"]["state"] == "ready" + assert context["browser"]["dynamic_content"] == "loaded" + assert context["browser"]["visible_elements"] == 10 + +def test_partial_browser_state_update(): + logger = TaskLogger("test_task", "Test task goal") + + # Update only some fields + logger.update_browser_state(url="https://example.com") + context = logger.get_context() + + assert context["browser"]["url"] == "https://example.com" + assert context["browser"]["state"] == "loading" # Unchanged + assert context["browser"]["dynamic_content"] == "loading" # Unchanged + assert context["browser"]["visible_elements"] == 0 # Unchanged + +def test_elapsed_time_calculation(): + logger = TaskLogger("test_task", "Test task goal") + + # Set a specific start time + start_time = datetime.utcnow() - timedelta(seconds=5) + logger.context.started_at = start_time.isoformat() + + context = logger.get_context() + elapsed_time = float(context["task"]["elapsed_time"].rstrip("s")) + + assert 4.5 <= elapsed_time <= 5.5 # Allow for small timing variations + +def test_task_status_validation(): + logger = TaskLogger("test_task", "Test task goal") + + # Test all valid status values + for status in TaskStatus: + logger.update_step(f"Step with status {status}", status) + context = logger.get_context() + assert context["task"]["status"] == status.value + +def test_json_serialization(): + logger = TaskLogger("test_task", "Test task goal") + context = logger.get_context() + + # Verify that the context can be JSON serialized + json_str = json.dumps(context) + parsed = json.loads(json_str) + + assert parsed["task"]["id"] == "test_task" + assert parsed["task"]["goal"] == "Test task goal" + assert "timestamp" in parsed + assert "elapsed_time" in parsed["task"] + +def test_step_info_status_conversion(): + # Test that string status values are converted to TaskStatus enum + step = StepInfo( + number=1, + description="Test step", + started_at=datetime.utcnow().isoformat(), + status="running" # Pass as string + ) + + assert isinstance(step.status, TaskStatus) + assert step.status == TaskStatus.RUNNING + +def test_error_handling(): + logger = TaskLogger("error_task", "Test error handling") + + # Simulate an error + error = ValueError("Test error") + logger.log_error(error, step_number=1, action="test action") + + context = logger.get_context() + assert context["task"]["status"] == "failed" + assert context["error"]["message"] == "Test error" + assert context["error"]["type"] == "ValueError" + assert context["error"]["step"] == 1 + assert context["error"]["action"] == "test action" + +def test_performance_metrics(): + logger = TaskLogger("perf_task", "Test performance tracking") + + # Start tracking performance + logger.start_performance_tracking() + + # Simulate some steps with timing + logger.update_step("Navigation", TaskStatus.RUNNING) + logger.track_step_duration("navigation", 0.5) + + logger.update_step("Interaction", TaskStatus.RUNNING) + logger.track_step_duration("interaction", 0.3) + + # Get performance metrics + metrics = logger.get_performance_metrics() + assert metrics["step_breakdown"]["navigation"] == pytest.approx(0.5) + assert metrics["step_breakdown"]["interaction"] == pytest.approx(0.3) + assert metrics["total_duration"] > 0 + +def test_detailed_browser_state(): + logger = TaskLogger("browser_task", "Test browser state") + + # Update with detailed browser state + logger.update_browser_state( + url="https://example.com", + page_ready=True, + dynamic_content_loaded=True, + visible_elements=10, + current_frame="main", + active_element="search_input", + page_title="Example Page" + ) + + context = logger.get_context() + browser_state = context["browser"] + assert browser_state["url"] == "https://example.com" + assert browser_state["state"] == "ready" + assert browser_state["current_frame"] == "main" + assert browser_state["active_element"] == "search_input" + assert browser_state["page_title"] == "Example Page" + +def test_task_progress_tracking(): + logger = TaskLogger("progress_task", "Test progress tracking") + + # Add steps with progress information + logger.update_step("Step 1", TaskStatus.COMPLETE, progress=0.25) + context = logger.get_context() + assert context["task"]["progress"] == "25%" + + logger.update_step("Step 2", TaskStatus.COMPLETE, progress=0.5) + context = logger.get_context() + assert context["task"]["progress"] == "50%" + + logger.update_step("Final Step", TaskStatus.COMPLETE, progress=1.0) + context = logger.get_context() + assert context["task"]["progress"] == "100%" + +def test_log_formatting(): + logger = TaskLogger("format_task", "Test log formatting") + + # Capture log output + logger.update_step("Navigation", TaskStatus.RUNNING) + log_output = logger.format_log_entry() + + # Verify log format matches the specified structure + assert "[" in log_output # Has timestamp + assert "STEP 2/" in log_output # Has step number (2 because update_step increments) + assert "Navigation" in log_output # Has action + assert "→" in log_output # Has status symbol for running + + # Add another step to test duration + logger.update_step("Click button", TaskStatus.COMPLETE) + log_output = logger.format_log_entry() + assert "(" in log_output and "s)" in log_output # Now we should have duration + +def test_semantic_step_descriptions(): + logger = TaskLogger("semantic_task", "Test semantic descriptions") + + # Test navigation step + logger.update_step( + "Navigate to example.com", + TaskStatus.RUNNING, + action_type=ActionType.NAVIGATION + ) + context = logger.get_context() + assert context["task"]["current_action"] == "navigation" + assert "🌐" in logger.format_log_entry() # Navigation emoji + + # Test interaction step + logger.update_step( + "Click search button", + TaskStatus.RUNNING, + action_type=ActionType.INTERACTION + ) + context = logger.get_context() + assert context["task"]["current_action"] == "interaction" + assert "🖱️" in logger.format_log_entry() # Interaction emoji + + # Test extraction step + logger.update_step( + "Extract search results", + TaskStatus.RUNNING, + action_type=ActionType.EXTRACTION + ) + context = logger.get_context() + assert context["task"]["current_action"] == "extraction" + assert "📑" in logger.format_log_entry() # Extraction emoji + +def test_redundant_message_filtering(): + logger = TaskLogger("filter_task", "Test message filtering") + + # Add multiple steps of the same type + logger.update_step( + "Navigate to example.com", + TaskStatus.RUNNING, + action_type=ActionType.NAVIGATION + ) + logger.update_step( + "Page loaded successfully", + TaskStatus.COMPLETE, + action_type=ActionType.NAVIGATION, + suppress_similar=True # Should be filtered as it's a completion of the same action + ) + + # Get all log entries + log_entries = logger.get_log_history() + + # Verify that redundant messages are consolidated + navigation_entries = [entry for entry in log_entries if "Navigate" in entry] + assert len(navigation_entries) == 1 # Only the main action should be logged + + # Verify that the current step shows the completion status + current_log = logger.format_log_entry() + assert "✓" in current_log # Success symbol should be in current state + +def test_action_context_tracking(): + logger = TaskLogger("context_task", "Test action context") + + # Start a navigation action + logger.update_step( + "Navigate to example.com", + TaskStatus.RUNNING, + action_type=ActionType.NAVIGATION, + context={ + "url": "https://example.com", + "method": "GET", + "headers": {"User-Agent": "browser-use"} + } + ) + + context = logger.get_context() + assert "action_context" in context["task"] + assert context["task"]["action_context"]["url"] == "https://example.com" + + # Complete the action with results + logger.update_step( + "Navigation complete", + TaskStatus.COMPLETE, + action_type=ActionType.NAVIGATION, + results={ + "status_code": 200, + "page_title": "Example Domain", + "load_time": 0.5 + } + ) + + context = logger.get_context() + assert "action_results" in context["task"] + assert context["task"]["action_results"]["status_code"] == 200 + +def test_retry_configuration(): + config = RetryConfig( + max_retries=3, + base_delay=1.0, + max_delay=10.0, + jitter=0.1 + ) + + # Test that delays follow exponential backoff pattern + delays = [config.get_delay(attempt) for attempt in range(5)] + assert delays[0] == 0 # First attempt has no delay + assert 0.9 <= delays[1] <= 1.1 # First retry ~1.0s with jitter + assert 1.8 <= delays[2] <= 2.2 # Second retry ~2.0s with jitter + assert 3.6 <= delays[3] <= 4.4 # Third retry ~4.0s with jitter + assert delays[4] == -1 # Beyond max retries + + # Test max delay capping + config = RetryConfig( + max_retries=5, + base_delay=1.0, + max_delay=5.0, + jitter=0.0 # Disable jitter for predictable testing + ) + assert config.get_delay(3) == 4.0 # Within max + assert config.get_delay(4) == 5.0 # Capped at max + +@pytest.mark.asyncio +async def test_retry_execution(): + logger = TaskLogger("retry_task", "Test retry logic") + + # Mock function that fails twice then succeeds + attempt_count = 0 + async def mock_operation(): + nonlocal attempt_count + attempt_count += 1 + if attempt_count <= 2: + raise ValueError("Temporary error") + return "success" + + # Configure retry behavior + retry_config = RetryConfig(max_retries=3, base_delay=0.1) + + # Execute with retry + result = await logger.execute_with_retry( + mock_operation, + "test_operation", + retry_config=retry_config + ) + + assert result == "success" + assert attempt_count == 3 # Two failures + one success + + # Verify retry information in logs + context = logger.get_context() + assert "retries" in context["task"] + retry_info = context["task"]["retries"] + assert retry_info["attempts"] == 3 + assert retry_info["success"] is True + assert len(retry_info["history"]) == 2 # Two retry attempts + +@pytest.mark.asyncio +async def test_retry_max_attempts_exceeded(): + logger = TaskLogger("retry_task", "Test retry logic") + + # Mock function that always fails + async def mock_operation(): + raise ValueError("Persistent error") + + # Configure retry behavior + retry_config = RetryConfig(max_retries=2, base_delay=0.1) + + # Execute with retry and expect failure + with pytest.raises(ValueError) as exc_info: + await logger.execute_with_retry( + mock_operation, + "test_operation", + retry_config=retry_config + ) + + assert str(exc_info.value) == "Persistent error" + + # Verify retry information in logs + context = logger.get_context() + assert "retries" in context["task"] + retry_info = context["task"]["retries"] + assert retry_info["attempts"] == 3 # Initial + 2 retries + assert retry_info["success"] is False + assert len(retry_info["history"]) == 3 # Initial attempt + two retries + assert all(entry["error"] == "ValueError: Persistent error" for entry in retry_info["history"]) + + # Verify the delays follow the expected pattern + delays = [entry["delay"] for entry in retry_info["history"]] + assert delays[0] > 0 # First retry has positive delay + assert delays[1] > delays[0] # Second retry has longer delay + assert delays[2] == -1 # Final attempt indicates max retries exceeded + +def test_retry_backoff_calculation(): + config = RetryConfig( + max_retries=3, + base_delay=1.0, + max_delay=10.0, + jitter=0.0 # Disable jitter for predictable testing + ) + + # Test exponential backoff sequence + assert config.get_delay(0) == 0 # First attempt + assert config.get_delay(1) == 1.0 # First retry + assert config.get_delay(2) == 2.0 # Second retry + assert config.get_delay(3) == 4.0 # Third retry + assert config.get_delay(4) == -1 # Beyond max retries + + # Test max delay capping + config = RetryConfig( + max_retries=5, + base_delay=1.0, + max_delay=5.0, + jitter=0.0 + ) + assert config.get_delay(3) == 4.0 # Within max + assert config.get_delay(4) == 5.0 # Capped at max + +def test_color_scheme(): + """Test that color scheme is properly defined and accessible.""" + scheme = ColorScheme() + + # Test error colors + assert scheme.error.startswith("\033[31m") # Red + assert scheme.warning.startswith("\033[33m") # Yellow + assert scheme.info.startswith("\033[36m") # Cyan + assert scheme.success.startswith("\033[32m") # Green + assert scheme.reset == "\033[0m" # Reset + +def test_log_formatting_with_colors(): + """Test that log messages are properly formatted with colors.""" + logger = TaskLogger("color_task", "Test color formatting") + + # Test error formatting + logger.update_step("Failed operation", TaskStatus.FAILED) + log_output = logger.format_log_entry() + assert "\033[31m" in log_output # Contains red color code + assert "×" in log_output # Contains error symbol + + # Test success formatting + logger.update_step("Successful operation", TaskStatus.COMPLETE) + log_output = logger.format_log_entry() + assert "\033[32m" in log_output # Contains green color code + assert "✓" in log_output # Contains success symbol + + # Test running state formatting + logger.update_step("Running operation", TaskStatus.RUNNING) + log_output = logger.format_log_entry() + assert "\033[36m" in log_output # Contains cyan color code + assert "→" in log_output # Contains running symbol + +def test_color_disabled(): + """Test that colors can be disabled via environment variable.""" + os.environ["NO_COLOR"] = "1" + logger = TaskLogger("no_color_task", "Test without colors") + + logger.update_step("Test operation", TaskStatus.COMPLETE) + log_output = logger.format_log_entry() + + # Verify no color codes are present + assert "\033[" not in log_output + assert "✓" in log_output # Symbols still present + + # Clean up + del os.environ["NO_COLOR"] + +def test_color_scheme_customization(): + """Test that color scheme can be customized.""" + custom_scheme = ColorScheme( + error="\033[35m", # Magenta for errors + warning="\033[34m", # Blue for warnings + info="\033[37m", # White for info + success="\033[32m" # Keep green for success + ) + + logger = TaskLogger("custom_color_task", "Test custom colors", color_scheme=custom_scheme) + + # Test custom error color + logger.update_step("Failed operation", TaskStatus.FAILED) + log_output = logger.format_log_entry() + assert "\033[35m" in log_output # Contains magenta color code + + # Test custom info color + logger.update_step("Info message", TaskStatus.RUNNING) + log_output = logger.format_log_entry() + assert "\033[37m" in log_output # Contains white color code + +def test_log_formatter_with_colors(): + """Test that the log formatter properly applies colors to different components.""" + formatter = LogFormatter() + + # Create a mock log record + class MockRecord: + def __init__(self, levelname, msg): + self.levelname = levelname + self.msg = msg + self.created = datetime.utcnow().timestamp() + + # Test error formatting + error_record = MockRecord("ERROR", "Test error message") + formatted = formatter.format(error_record) + assert "\033[31m" in formatted # Red for error + assert "ERROR" in formatted + + # Test info formatting + info_record = MockRecord("INFO", "Test info message") + formatted = formatter.format(info_record) + assert "\033[36m" in formatted # Cyan for info + assert "INFO" in formatted + + # Test warning formatting + warn_record = MockRecord("WARNING", "Test warning message") + formatted = formatter.format(warn_record) + assert "\033[33m" in formatted # Yellow for warning + assert "WARNING" in formatted + +def test_task_separator_style(): + """Test that separator styles are properly defined and formatted.""" + style = SeparatorStyle() + + # Test default separator styles + assert len(style.task) >= 50 # Task separator should be substantial + assert len(style.phase) >= 30 # Phase separator should be visible but less prominent + assert len(style.error) >= 40 # Error separator should be distinct + + # Test that styles are different + assert style.task != style.phase + assert style.task != style.error + assert style.phase != style.error + +def test_task_start_separator(): + """Test that separators are added at task start.""" + logger = TaskLogger("separator_task", "Test separators") + + # Get initial log output + log_entries = logger.get_log_history() + + # Should have task separator and initial step + assert len(log_entries) == 2 + assert "=" * 50 in log_entries[0] # Task separator + assert "TASK GOAL: Test separators" in log_entries[1] # Initial step message + +def test_phase_separators(): + """Test that separators are added between different phases.""" + logger = TaskLogger("separator_task", "Test separators") + + # Navigation phase + logger.start_phase("Navigation") + logger.update_step("Navigate to example.com", TaskStatus.COMPLETE, action_type=ActionType.NAVIGATION) + + # Interaction phase + logger.start_phase("Interaction") + logger.update_step("Click button", TaskStatus.COMPLETE, action_type=ActionType.INTERACTION) + + # Get log entries + log_entries = logger.get_log_history() + + # Count phase separators + phase_separators = [entry for entry in log_entries if "-" * 30 in entry] + assert len(phase_separators) == 2 # One before each phase + +def test_error_separators(): + """Test that separators are added around error messages.""" + logger = TaskLogger("separator_task", "Test separators") + + # Simulate an error + try: + raise ValueError("Test error") + except Exception as e: + logger.log_error(e, step_number=1, action="test_action") + + # Get log entries + log_entries = logger.get_log_history() + + # Should have error separators + error_separators = [entry for entry in log_entries if "*" * 40 in entry] + assert len(error_separators) == 2 # One before and one after error + +def test_custom_separator_style(): + """Test that separator styles can be customized.""" + custom_style = SeparatorStyle( + task="◈" * 30, + phase="•" * 20, + error="!" * 25 + ) + + logger = TaskLogger("custom_separator_task", "Test custom separators", separator_style=custom_style) + + # Start a phase + logger.start_phase("Test Phase") + + # Get log entries + log_entries = logger.get_log_history() + + # Verify custom separators are used + assert "◈" * 30 in log_entries[0] # Task separator + assert "•" * 20 in log_entries[2] # Phase separator + assert "→" in log_entries[2] # Arrow indicator for phase start + +def test_separator_with_colors(): + """Test that separators can be colored.""" + logger = TaskLogger("colored_separator_task", "Test colored separators") + + # Start a phase + logger.start_phase("Test Phase") + + # Get log entries + log_entries = logger.get_log_history() + + # Verify separators have color codes + task_separator = log_entries[0] + phase_separator = log_entries[1] + + assert "\033[" in task_separator # Contains color code + assert "\033[" in phase_separator # Contains color code + +def test_separator_disabled(): + """Test that separators can be disabled.""" + logger = TaskLogger("no_separator_task", "Test without separators", use_separators=False) + + # Start a phase + logger.start_phase("Test Phase") + + # Get log entries + log_entries = logger.get_log_history() + + # Verify no separators are present + separators = [entry for entry in log_entries if any(c * 20 in entry for c in "=-*")] + assert len(separators) == 0 # No separators should be present \ No newline at end of file diff --git a/webui.py b/webui.py index b7acffe4..ca96dfc7 100644 --- a/webui.py +++ b/webui.py @@ -7,15 +7,29 @@ import pdb import logging - -from dotenv import load_dotenv - -load_dotenv() import os +import sys import glob import asyncio import argparse import os +import warnings + +from dotenv import load_dotenv +from src.utils.logging import setup_logging, PRODUCTION_EXCLUDE_PATTERNS + +# Filter out the specific deprecation warning from langchain-google-genai +warnings.filterwarnings('ignore', message='Convert_system_message_to_human will be deprecated!') + +load_dotenv() + +# Setup logging before importing other modules +setup_logging( + level=os.getenv("LOG_LEVEL", "INFO"), + use_json=os.getenv("LOG_JSON", "true").lower() == "true", + log_file=os.getenv("LOG_FILE"), + exclude_patterns=PRODUCTION_EXCLUDE_PATTERNS if os.getenv("ENVIRONMENT") == "production" else None +) logger = logging.getLogger(__name__) From 612587d44b9732f8add587bd478f93558c046383 Mon Sep 17 00:00:00 2001 From: David Mieloch Date: Mon, 20 Jan 2025 16:29:47 -0500 Subject: [PATCH 08/10] Enhance CLI functionality and update README for improved LLM provider support - Refactored CLI commands to allow selection of LLM providers (DeepSeek, Google, OpenAI, Anthropic) with model indexing options. - Updated `README.md` to include a comprehensive CLI guide, detailing usage patterns, available models, and environment configuration. - Enhanced `browser_use_cli.py` to normalize provider names and select appropriate models based on user input. - Modified `browser-use.toolchain.json` to reflect new provider and model options, including descriptions for better user understanding. - Improved error handling in model selection and added tests for new provider functionalities in `test_llm_api.py`. - Removed obsolete VSCode settings file to streamline project structure. --- README.md | 206 ++---------------- cli/.vscode/settings.json | 7 - cli/README.md | 158 ++++++++++++++ cli/browser-use.toolchain.json | 79 ++++--- cli/browser_use_cli.py | 40 +++- src/utils/utils.py | 12 +- tests/test_browser_vision.py | 94 ++++++++ tests/test_llm_api.py | 66 +++++- tests/test_llm_integration.py | 182 ++++++++++++++++ .../cf594a49-42fc-4872-aa49-e765114eb674.zip | Bin 0 -> 70389 bytes .../f9ce4c26-5d21-41cc-a8d1-203abb0a0d05.zip | Bin 0 -> 70552 bytes .../0551d9c8-501b-4353-8f81-9e45074c4aa6.zip | Bin 0 -> 108808 bytes .../2e3bb817-e468-4c36-b195-bca5e8ed8707.zip | Bin 0 -> 1494647 bytes .../3a12e173-27c3-49b2-a90c-e3dd8d04e005.zip | Bin 0 -> 556048 bytes 14 files changed, 600 insertions(+), 244 deletions(-) delete mode 100644 cli/.vscode/settings.json create mode 100644 cli/README.md create mode 100644 tests/test_browser_vision.py create mode 100644 tests/test_llm_integration.py create mode 100644 traces/create-template-step1.json/trace.zip/cf594a49-42fc-4872-aa49-e765114eb674.zip create mode 100644 traces/create-template.json/trace.zip/f9ce4c26-5d21-41cc-a8d1-203abb0a0d05.zip create mode 100644 traces/initial-load.json/trace.zip/0551d9c8-501b-4353-8f81-9e45074c4aa6.zip create mode 100644 traces/settings-config.json/trace.zip/2e3bb817-e468-4c36-b195-bca5e8ed8707.zip create mode 100644 traces/settings.json/trace.zip/3a12e173-27c3-49b2-a90c-e3dd8d04e005.zip diff --git a/README.md b/README.md index c33615c4..698b00de 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ This fork of browser-use/web-ui adds CLI support specifically designed for AI ag ## CLI Documentation -- [Usage Guide](cli/usage-guide.md) - Comprehensive guide for CLI usage, including: - - Model configuration (DeepSeek, Gemini, GPT-4, Claude-3) - - Browser automation tasks - - Tracing and debugging - - Report generation +See [CLI Guide](cli/README.md) for comprehensive documentation on: +- Available LLM providers and models +- Detailed command reference +- Environment configuration +- Example usage patterns ### Quick Start @@ -16,8 +16,11 @@ This fork of browser-use/web-ui adds CLI support specifically designed for AI ag # Run a task (browser will auto-start if needed) browser-use run "go to example.com and create a report about the page structure" -# Run with specific model and options -browser-use run --model claude-3 --vision --trace-path ./traces "analyze the layout and visual elements" +# Run with specific provider and vision capabilities +browser-use run "analyze the layout and visual elements" --provider Google --vision + +# Run with specific model selection +browser-use run "analyze the page" --provider Anthropic --model-index 1 # Explicitly start browser with custom options (optional) browser-use start --headless --window-size 1920x1080 @@ -26,6 +29,15 @@ browser-use start --headless --window-size 1920x1080 browser-use close ``` +### Supported LLM Providers + +- **OpenAI** (`gpt-4o`) - Vision-capable model for advanced analysis +- **Anthropic** (`claude-3-5-sonnet-latest`, `claude-3-5-sonnet-20241022`) - Advanced language understanding +- **Google** (`gemini-1.5-pro`, `gemini-2.0-flash`) - Fast and efficient processing +- **DeepSeek** (`deepseek-chat`) - Cost-effective default option + +See the [CLI Guide](cli/README.md) for detailed provider configuration and usage examples. + ### CLI Commands - `start` - (Optional) Initialize browser session with custom options: @@ -115,182 +127,4 @@ Then install playwright: ```bash playwright install -``` - -### Option 2: Docker Installation - -1. **Prerequisites:** - - Docker and Docker Compose installed on your system - - Git to clone the repository - -2. **Setup:** - - ```bash - # Clone the repository - git clone https://github.com/browser-use/web-ui.git - cd web-ui - - # Copy and configure environment variables - cp .env.example .env - # Edit .env with your preferred text editor and add your API keys - ``` - -3. **Run with Docker:** - - ```bash - # Build and start the container with default settings (browser closes after AI tasks) - docker compose up --build - - # Or run with persistent browser (browser stays open between AI tasks) - CHROME_PERSISTENT_SESSION=true docker compose up --build - ``` - -4. **Access the Application:** - - WebUI: `http://localhost:7788` - - VNC Viewer (to see browser interactions): `http://localhost:6080/vnc.html` - - Default VNC password is "vncpassword". You can change it by setting the `VNC_PASSWORD` environment variable in your `.env` file. - -## Usage - -### Local Setup - -1. Copy `.env.example` to `.env` and set your environment variables, including API keys for the LLM. `cp .env.example .env` -2. **Run the WebUI:** - - ```bash - python webui.py --ip 127.0.0.1 --port 7788 - ``` - -4. WebUI options: - - `--ip`: The IP address to bind the WebUI to. Default is `127.0.0.1`. - - `--port`: The port to bind the WebUI to. Default is `7788`. - - `--theme`: The theme for the user interface. Default is `Ocean`. - - **Default**: The standard theme with a balanced design. - - **Soft**: A gentle, muted color scheme for a relaxed viewing experience. - - **Monochrome**: A grayscale theme with minimal color for simplicity and focus. - - **Glass**: A sleek, semi-transparent design for a modern appearance. - - **Origin**: A classic, retro-inspired theme for a nostalgic feel. - - **Citrus**: A vibrant, citrus-inspired palette with bright and fresh colors. - - **Ocean** (default): A blue, ocean-inspired theme providing a calming effect. - - `--dark-mode`: Enables dark mode for the user interface. -3. **Access the WebUI:** Open your web browser and navigate to `http://127.0.0.1:7788`. -4. **Using Your Own Browser(Optional):** - - Set `CHROME_PATH` to the executable path of your browser and `CHROME_USER_DATA` to the user data directory of your browser. - - Windows - - ```env - CHROME_PATH="C:\Program Files\Google\Chrome\Application\chrome.exe" - CHROME_USER_DATA="C:\Users\YourUsername\AppData\Local\Google\Chrome\User Data" - ``` - - > Note: Replace `YourUsername` with your actual Windows username for Windows systems. - - Mac - - ```env - CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" - CHROME_USER_DATA="~/Library/Application Support/Google/Chrome/Profile 1" - ``` - - - Close all Chrome windows - - Open the WebUI in a non-Chrome browser, such as Firefox or Edge. This is important because the persistent browser context will use the Chrome data when running the agent. - - Check the "Use Own Browser" option within the Browser Settings. -5. **Keep Browser Open(Optional):** - - Set `CHROME_PERSISTENT_SESSION=true` in the `.env` file. - -### Docker Setup - -1. **Environment Variables:** - - All configuration is done through the `.env` file - - Available environment variables: - - ``` - # LLM API Keys - OPENAI_API_KEY=your_key_here - ANTHROPIC_API_KEY=your_key_here - GOOGLE_API_KEY=your_key_here - - # Browser Settings - CHROME_PERSISTENT_SESSION=true # Set to true to keep browser open between AI tasks - RESOLUTION=1920x1080x24 # Custom resolution format: WIDTHxHEIGHTxDEPTH - RESOLUTION_WIDTH=1920 # Custom width in pixels - RESOLUTION_HEIGHT=1080 # Custom height in pixels - - # VNC Settings - VNC_PASSWORD=your_vnc_password # Optional, defaults to "vncpassword" - ``` - -2. **Browser Persistence Modes:** - - **Default Mode (CHROME_PERSISTENT_SESSION=false):** - - Browser opens and closes with each AI task - - Clean state for each interaction - - Lower resource usage - - - **Persistent Mode (CHROME_PERSISTENT_SESSION=true):** - - Browser stays open between AI tasks - - Maintains history and state - - Allows viewing previous AI interactions - - Set in `.env` file or via environment variable when starting container - -3. **Viewing Browser Interactions:** - - Access the noVNC viewer at `http://localhost:6080/vnc.html` - - Enter the VNC password (default: "vncpassword" or what you set in VNC_PASSWORD) - - You can now see all browser interactions in real-time - -4. **Container Management:** - - ```bash - # Start with persistent browser - CHROME_PERSISTENT_SESSION=true docker compose up -d - - # Start with default mode (browser closes after tasks) - docker compose up -d - - # View logs - docker compose logs -f - - # Stop the container - docker compose down - ``` - -## Changelog - -- [x] **2025/01/10:** Thanks to @casistack. Now we have Docker Setup option and also Support keep browser open between tasks.[Video tutorial demo](https://github.com/browser-use/web-ui/issues/1#issuecomment-2582511750). -- [x] **2025/01/06:** Thanks to @richard-devbot. A New and Well-Designed WebUI is released. [Video tutorial demo](https://github.com/warmshao/browser-use-webui/issues/1#issuecomment-2573393113). - -## Fork Information -This is a fork of the original browser-use project with additional features and improvements. - -## Changelog - -### January 2025 - Logging System Overhaul -- **Enhanced Logging System** - - Added structured task logging with context and state tracking - - Implemented color-coded log levels and task states - - Added visual separators between task phases - - Introduced emoji indicators for different action types (🌐 navigation, 🖱️ interaction, 📑 extraction) - - Added status symbols for task states (→ running, ✓ complete, × failed) - -- **Error Handling Improvements** - - Implemented smart retry logic with exponential backoff - - Added structured error logging with context - - Introduced visual error separators - - Added retry history and statistics tracking - -- **Progress Tracking** - - Added percentage-based progress tracking - - Implemented step duration tracking - - Added detailed browser state information - - Introduced performance metrics breakdown - -- **Log Management** - - Added semantic step descriptions - - Implemented message filtering and deduplication - - Added support for both JSON and human-readable output - - Introduced custom color schemes and formatting options - -### Coming Soon -- Log buffering for high-frequency events -- Recovery suggestions for common errors -- Real-time monitoring dashboard -- Interactive log viewer interface +``` \ No newline at end of file diff --git a/cli/.vscode/settings.json b/cli/.vscode/settings.json deleted file mode 100644 index 29777348..00000000 --- a/cli/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "workbench.colorCustomizations": { - "activityBar.background": "#2D2F09", - "titleBar.activeBackground": "#3F420C", - "titleBar.activeForeground": "#FAFBEA" - } -} \ No newline at end of file diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 00000000..37cc8931 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,158 @@ +# Browser-Use CLI Guide + +This guide details the available models and commands for the browser-use CLI tool. + +## Available Models + +### OpenAI +- Model: `gpt-4o` (Vision-capable) +```bash +# Basic usage +browser-use run "analyze this webpage" --provider OpenAI + +# With vision capabilities +browser-use run "describe what you see on the page" --provider OpenAI --vision +``` + +### Anthropic +- Models: + - `claude-3-5-sonnet-latest` (Default) + - `claude-3-5-sonnet-20241022` +```bash +# Using default model +browser-use run "analyze this webpage" --provider Anthropic + +# Using specific model version +browser-use run "analyze this webpage" --provider Anthropic --model-index 1 +``` + +### Google (Gemini) +- Models: + - `gemini-1.5-pro` (Default) + - `gemini-2.0-flash` +```bash +# Using default model +browser-use run "analyze this webpage" --provider Google + +# Using flash model +browser-use run "analyze this webpage" --provider Google --model-index 1 +``` + +### DeepSeek +- Model: `deepseek-chat` +```bash +# DeepSeek is the default provider +browser-use run "analyze this webpage" + +# Explicitly specifying DeepSeek +browser-use run "analyze this webpage" --provider Deepseek +``` + +## CLI Commands + +### Start Browser Session +```bash +# Basic start +browser-use start + +# With custom window size +browser-use start --window-size 1920x1080 + +# Headless mode +browser-use start --headless + +# With custom Chrome profile +browser-use start --user-data-dir "/path/to/profile" + +# With proxy +browser-use start --proxy "localhost:8080" +``` + +### Run Tasks +```bash +# Basic task +browser-use run "go to example.com and analyze the page" + +# With vision capabilities +browser-use run "describe the visual layout" --vision + +# With specific provider and model +browser-use run "analyze this webpage" --provider Google --model-index 1 + +# With recording +browser-use run "test the checkout flow" --record --record-path ./recordings + +# With debugging traces +browser-use run "analyze form submission" --trace-path ./traces + +# With step limits +browser-use run "complex task" --max-steps 5 --max-actions 2 + +# With additional context +browser-use run "analyze pricing" --add-info "Focus on enterprise plans" +``` + +### Close Browser +```bash +browser-use close +``` + +## Environment Variables + +Required API keys should be set in your `.env` file: +```env +# OpenAI +OPENAI_API_KEY=your_key_here +OPENAI_ENDPOINT=https://api.openai.com/v1 # Optional + +# Anthropic +ANTHROPIC_API_KEY=your_key_here + +# Google (Gemini) +GOOGLE_API_KEY=your_key_here + +# DeepSeek +DEEPSEEK_API_KEY=your_key_here +DEEPSEEK_ENDPOINT=your_endpoint # Optional +``` + +## Browser Settings + +Optional browser configuration in `.env`: +```env +# Custom Chrome settings +CHROME_PATH=/path/to/chrome +CHROME_USER_DATA=/path/to/user/data + +# Session persistence +CHROME_PERSISTENT_SESSION=true # Keep browser open between tasks +``` + +## Examples + +### Visual Analysis Task +```bash +browser-use run \ + "go to https://example.com and analyze the page layout" \ + --provider Google \ + --vision \ + --record \ + --record-path ./recordings +``` + +### Multi-Step Task +```bash +browser-use run \ + "go to the login page, fill the form, and verify success" \ + --provider Anthropic \ + --max-steps 5 \ + --trace-path ./traces/login +``` + +### Research Task +```bash +browser-use run \ + "research pricing information for top 3 competitors" \ + --provider OpenAI \ + --add-info "Focus on enterprise features and annual pricing" +``` \ No newline at end of file diff --git a/cli/browser-use.toolchain.json b/cli/browser-use.toolchain.json index cd27e10c..2af3f1ff 100644 --- a/cli/browser-use.toolchain.json +++ b/cli/browser-use.toolchain.json @@ -10,85 +10,100 @@ "properties": { "prompt": { "type": "string", - "description": "The natural language instruction (e.g., 'go to google.com and search for OpenAI')" + "description": "The natural language instruction (e.g., 'go to google.com and search for OpenAI'). **Ensure URLs are well-formed and include the protocol (e.g., https://).**" }, - "model": { + "provider": { "type": "string", - "enum": ["deepseek-chat", "gemini", "gpt-4", "claude-3"], - "default": "deepseek-chat", - "description": "The LLM model to use (optional)" + "enum": [ + "Deepseek", + "Google", + "OpenAI", + "Anthropic" + ], + "default": "Deepseek", + "description": "The LLM provider to use. DeepSeek is recommended for most tasks due to its cost-effectiveness and performance. The system will automatically select the appropriate model based on your task requirements (e.g., vision capabilities)." }, - "headless": { + "model_index": { + "type": "integer", + "description": "Optional index to select a specific model from the provider's available models (0-based). Available models per provider:\nDeepseek: [0: deepseek-chat]\nGoogle: [0: gemini-1.5-pro, 1: gemini-2.0-flash]\nOpenAI: [0: gpt-4o]\nAnthropic: [0: claude-3-5-sonnet-latest, 1: claude-3-5-sonnet-20241022]" + }, + "vision": { "type": "boolean", "default": false, - "description": "Run browser in headless mode (optional)" + "description": "Enable vision capabilities (optional). **When enabled, the system will automatically select a vision-capable model from your chosen provider.**" }, - "vision": { + "headless": { "type": "boolean", "default": false, - "description": "Enable vision capabilities for supported models (optional)" + "description": "Run browser in headless mode (optional). **Headless mode might be necessary for certain environments or tasks but can limit interaction with visually-dependent elements.**" }, "record": { "type": "boolean", "default": false, - "description": "Enable session recording (optional)" + "description": "Enable session recording (optional). **Useful for debugging and understanding the agent's actions.**" }, "recordPath": { "type": "string", "default": "./tmp/record_videos", - "description": "Path to save recordings (optional)" + "description": "Path to save recordings (optional). **Ensure the directory exists and is writable.**" }, "tracePath": { "type": "string", - "description": "Path to save debugging traces (optional)" + "description": "Path to save debugging traces (optional). **Traces can provide detailed information about the automation process.**" }, "maxSteps": { "type": "integer", "default": 10, - "description": "Maximum number of steps per task (optional)" + "description": "Maximum number of steps per task (optional). **Increase this for complex tasks, but be mindful of potential infinite loops.**" }, "maxActions": { "type": "integer", "default": 1, - "description": "Maximum actions per step (optional)" + "description": "Maximum actions per step (optional). **Adjust this based on the complexity of each step.**" }, "addInfo": { "type": "string", - "description": "Additional context for the agent (optional)" + "description": "Additional context or instructions for the agent (optional). **Use this to provide specific details not covered in the main prompt.**" + }, + "tempFile": { + "type": "string", + "description": "Path to temporary file to store the browser session state (optional). **Used for resuming or closing specific sessions.**" + }, + "userDataDir": { + "type": "string", + "description": "Path to user data directory for a persistent browser session (optional). **Use this to maintain browser state across sessions (e.g., cookies, extensions).**" } }, - "required": ["prompt"] + "required": [ + "prompt" + ] } } ], "examples": [ { - "description": "Basic usage", - "command": "browser-use run \"go to google.com and search for OpenAI\"" - }, - { - "description": "Using vision to analyze a webpage", - "command": "browser-use run \"go to openai.com and tell me what you see\" --model gemini --vision" + "description": "Basic usage with default provider (DeepSeek)", + "command": "browser-use run \"go to https://www.google.com and search for OpenAI\"" }, { - "description": "Running a check in headless mode", - "command": "browser-use run \"check if github.com is up\" --headless" + "description": "Using Google Gemini with vision for visual analysis", + "command": "browser-use run \"go to https://www.openai.com and analyze the visual layout\" --provider Google --vision" }, { - "description": "Recording a debug session", - "command": "browser-use run \"complete the login process\" --record --record-path ./tmp/debug_session" + "description": "Using OpenAI for complex analysis", + "command": "browser-use run \"analyze the layout and design of https://www.example.com\" --provider OpenAI --vision" }, { - "description": "Using traces for debugging", - "command": "browser-use run \"test the checkout flow\" --trace-path ./tmp/traces/checkout" + "description": "Using Anthropic with specific model version", + "command": "browser-use run \"analyze the documentation at https://docs.example.com\" --provider Anthropic --model-index 1" }, { - "description": "Starting a new browser session", - "command": "browser-use start --user-data-dir '/Users/dmieloch/Library/Application Support/Google/Chrome/Default'" + "description": "Running a check in headless mode", + "command": "browser-use run \"check if https://www.github.com is up\" --provider Deepseek --headless" }, { - "description": "Closing a browser session", - "command": "browser-use close --temp-file /tmp/browser_use_state" + "description": "Recording a debug session", + "command": "browser-use run \"test the login process at https://example.com\" --provider Google --record --record-path ./debug_session" } ] } \ No newline at end of file diff --git a/cli/browser_use_cli.py b/cli/browser_use_cli.py index 9f158b2b..fd63505e 100644 --- a/cli/browser_use_cli.py +++ b/cli/browser_use_cli.py @@ -107,7 +107,8 @@ async def close_browser(): async def run_browser_task( prompt, - model="deepseek-chat", + provider="Deepseek", + model_index=None, vision=False, record=False, record_path=None, @@ -158,10 +159,34 @@ async def run_browser_task( # Initialize controller controller = CustomController() + # Normalize provider name to lowercase for consistency + provider = provider.lower() + + # Select appropriate model based on provider, model_index, and vision requirement + if provider not in utils.model_names: + raise ValueError(f"Unsupported provider: {provider}") + + available_models = utils.model_names[provider] + + if model_index is not None: + if not (0 <= model_index < len(available_models)): + raise ValueError(f"Invalid model_index {model_index} for provider {provider}. Available indices: 0-{len(available_models)-1}") + model_name = available_models[model_index] + else: + # Default model selection based on vision requirement + if provider == "deepseek": + model_name = available_models[0] # deepseek-chat + elif provider == "google": + model_name = available_models[0] # gemini-1.5-pro + elif provider == "openai": + model_name = available_models[0] # gpt-4o + elif provider == "anthropic": + model_name = available_models[0] # claude-3-5-sonnet-latest + # Get LLM model llm = utils.get_llm_model( - provider="deepseek" if model == "deepseek-chat" else model, - model_name=os.getenv("GOOGLE_API_MODEL", "gemini-pro-vision") if model == "gemini" else model, + provider=provider, + model_name=model_name, temperature=0.8, vision=vision ) @@ -241,8 +266,10 @@ def main(): run_parser = subparsers.add_parser("run", help="Run a task in the current browser session") run_parser.add_argument("--temp-file", help="Path to temporary file for storing browser state") run_parser.add_argument("prompt", help="The task to perform") - run_parser.add_argument("--model", "-m", choices=["deepseek-chat", "gemini", "gpt-4", "claude-3"], - default="deepseek-chat", help="The LLM model to use") + run_parser.add_argument("--provider", "-p", choices=["Deepseek", "Google", "OpenAI", "Anthropic"], + default="Deepseek", help="The LLM provider to use (system will select appropriate model)") + run_parser.add_argument("--model-index", "-m", type=int, + help="Optional index to select a specific model from the provider's available models (0-based)") run_parser.add_argument("--vision", action="store_true", help="Enable vision capabilities") run_parser.add_argument("--record", action="store_true", help="Enable session recording") run_parser.add_argument("--record-path", default="./tmp/record_videos", help="Path to save recordings") @@ -284,7 +311,8 @@ def main(): # Run task result = asyncio.run(run_browser_task( prompt=args.prompt, - model=args.model, + provider=args.provider, + model_index=args.model_index, vision=args.vision, record=args.record, record_path=args.record_path if args.record else None, diff --git a/src/utils/utils.py b/src/utils/utils.py index 58d213d3..9e202fa6 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -97,8 +97,7 @@ def get_llm_model(provider: str, **kwargs): model=model_name, temperature=kwargs.get("temperature", 0.0), api_key=api_key, - timeout=kwargs.get("timeout", 60), - convert_system_message_to_human=True + timeout=kwargs.get("timeout", 60) ) elif provider == "ollama": return ChatOllama( @@ -122,18 +121,19 @@ def get_llm_model(provider: str, **kwargs): api_version="2024-05-01-preview", azure_endpoint=base_url, api_key=api_key, + timeout=kwargs.get("timeout", 60), ) else: raise ValueError(f"Unsupported provider: {provider}") # Predefined model names for common providers model_names = { - "anthropic": ["claude-3-5-sonnet-20240620", "claude-3-opus-20240229"], - "openai": ["gpt-4o", "gpt-4", "gpt-3.5-turbo"], + "anthropic": ["claude-3-5-sonnet-latest", "claude-3-5-sonnet-20241022"], + "openai": ["gpt-4o"], "deepseek": ["deepseek-chat"], - "gemini": ["gemini-2.0-flash-exp", "gemini-2.0-flash-thinking-exp", "gemini-1.5-flash-latest", "gemini-1.5-flash-8b-latest", "gemini-2.0-flash-thinking-exp-1219" ], + "gemini": ["gemini-1.5-pro", "gemini-2.0-flash"], "ollama": ["qwen2.5:7b", "llama2:7b"], - "azure_openai": ["gpt-4o", "gpt-4", "gpt-3.5-turbo"] + "azure_openai": ["gpt-4", "gpt-3.5-turbo"] } # Callback to update the model name dropdown based on the selected provider diff --git a/tests/test_browser_vision.py b/tests/test_browser_vision.py new file mode 100644 index 00000000..75b44c32 --- /dev/null +++ b/tests/test_browser_vision.py @@ -0,0 +1,94 @@ +import os +import pytest +from dotenv import load_dotenv +from src.utils import utils +from cli.browser_use_cli import run_browser_task + +# Load environment variables +load_dotenv() + +@pytest.mark.asyncio +class TestBrowserVision: + """Test browser automation with vision capabilities""" + + async def setup_method(self): + """Setup test environment""" + self.api_key = os.getenv("OPENAI_API_KEY") + if not self.api_key: + pytest.skip("OPENAI_API_KEY not set") + + async def test_vision_analysis_task(self): + """Test visual analysis of a webpage""" + result = await run_browser_task( + prompt="go to https://example.com and describe the visual layout of the page", + provider="OpenAI", + vision=True, + headless=True, # Run headless for CI/CD + record=True, # Record for debugging + record_path="./tmp/test_recordings" + ) + assert result is not None + assert "layout" in result.lower() or "design" in result.lower() + + async def test_vision_interaction_task(self): + """Test visual-guided interaction""" + result = await run_browser_task( + prompt="go to https://example.com and click on the most prominent link on the page", + provider="OpenAI", + vision=True, + headless=True, + record=True, + record_path="./tmp/test_recordings" + ) + assert result is not None + assert "clicked" in result.lower() or "selected" in result.lower() + + async def test_vision_verification_task(self): + """Test visual verification of page state""" + result = await run_browser_task( + prompt="go to https://example.com and verify that the main heading is visible and centered", + provider="OpenAI", + vision=True, + headless=True, + record=True, + record_path="./tmp/test_recordings" + ) + assert result is not None + assert "heading" in result.lower() and ("visible" in result.lower() or "centered" in result.lower()) + + async def test_vision_error_handling(self): + """Test error handling with vision tasks""" + # Test with a non-existent page to verify error handling + result = await run_browser_task( + prompt="go to https://nonexistent.example.com and describe what you see", + provider="OpenAI", + vision=True, + headless=True, + record=True, + record_path="./tmp/test_recordings" + ) + assert result is not None + assert "error" in result.lower() or "unable" in result.lower() or "failed" in result.lower() + + async def test_vision_with_different_models(self): + """Test vision capabilities with different providers""" + test_configs = [ + "OpenAI", # Will use gpt-4o + "Google", # Will use gemini-pro + "Anthropic" # Will use claude-3-5-sonnet-20241022 + ] + + for provider in test_configs: + result = await run_browser_task( + prompt="go to https://example.com and describe the page layout", + provider=provider, + vision=True, + headless=True, + record=True, + record_path=f"./tmp/test_recordings/{provider.lower()}" + ) + assert result is not None + assert len(result) > 0, f"Failed with provider {provider}" + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/test_llm_api.py b/tests/test_llm_api.py index 9e2a1d6d..5b29cb3d 100644 --- a/tests/test_llm_api.py +++ b/tests/test_llm_api.py @@ -6,6 +6,7 @@ # @FileName: test_llm_api.py import os import pdb +import pytest from dotenv import load_dotenv @@ -20,12 +21,16 @@ def test_openai_model(): from langchain_core.messages import HumanMessage from src.utils import utils + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: + pytest.skip("OPENAI_API_KEY not set") + llm = utils.get_llm_model( provider="openai", model_name="gpt-4o", temperature=0.8, base_url=os.getenv("OPENAI_ENDPOINT", ""), - api_key=os.getenv("OPENAI_API_KEY", "") + api_key=api_key ) image_path = "assets/examples/test.png" image_data = utils.encode_image(image_path) @@ -47,11 +52,15 @@ def test_gemini_model(): from langchain_core.messages import HumanMessage from src.utils import utils + api_key = os.getenv("GOOGLE_API_KEY") + if not api_key: + pytest.skip("GOOGLE_API_KEY not set") + llm = utils.get_llm_model( provider="gemini", - model_name="gemini-2.0-flash-exp", + model_name="gemini-1.5-pro", temperature=0.8, - api_key=os.getenv("GOOGLE_API_KEY", "") + api_key=api_key ) image_path = "assets/examples/test.png" @@ -73,12 +82,17 @@ def test_azure_openai_model(): from langchain_core.messages import HumanMessage from src.utils import utils + api_key = os.getenv("AZURE_OPENAI_API_KEY") + endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") + if not api_key or not endpoint: + pytest.skip("AZURE_OPENAI_API_KEY or AZURE_OPENAI_ENDPOINT not set") + llm = utils.get_llm_model( provider="azure_openai", - model_name="gpt-4o", + model_name="gpt-4", temperature=0.8, - base_url=os.getenv("AZURE_OPENAI_ENDPOINT", ""), - api_key=os.getenv("AZURE_OPENAI_API_KEY", "") + base_url=endpoint, + api_key=api_key ) image_path = "assets/examples/test.png" image_data = utils.encode_image(image_path) @@ -99,12 +113,39 @@ def test_deepseek_model(): from langchain_core.messages import HumanMessage from src.utils import utils + api_key = os.getenv("DEEPSEEK_API_KEY") + if not api_key: + pytest.skip("DEEPSEEK_API_KEY not set") + llm = utils.get_llm_model( provider="deepseek", model_name="deepseek-chat", temperature=0.8, base_url=os.getenv("DEEPSEEK_ENDPOINT", ""), - api_key=os.getenv("DEEPSEEK_API_KEY", "") + api_key=api_key + ) + message = HumanMessage( + content=[ + {"type": "text", "text": "who are you?"} + ] + ) + ai_msg = llm.invoke([message]) + print(ai_msg.content) + + +def test_anthropic_model(): + from langchain_core.messages import HumanMessage + from src.utils import utils + + api_key = os.getenv("ANTHROPIC_API_KEY") + if not api_key: + pytest.skip("ANTHROPIC_API_KEY not set") + + llm = utils.get_llm_model( + provider="anthropic", + model_name="claude-3-5-sonnet-latest", + temperature=0.8, + api_key=api_key ) message = HumanMessage( content=[ @@ -118,6 +159,16 @@ def test_deepseek_model(): def test_ollama_model(): from langchain_ollama import ChatOllama + # Check if Ollama is running by trying to connect to its default port + import socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + result = sock.connect_ex(('localhost', 11434)) + if result != 0: + pytest.skip("Ollama server not running on localhost:11434") + finally: + sock.close() + llm = ChatOllama(model="qwen2.5:7b") ai_msg = llm.invoke("Sing a ballad of LangChain.") print(ai_msg.content) @@ -128,4 +179,5 @@ def test_ollama_model(): # test_gemini_model() # test_azure_openai_model() # test_deepseek_model() + # test_anthropic_model() test_ollama_model() diff --git a/tests/test_llm_integration.py b/tests/test_llm_integration.py new file mode 100644 index 00000000..60dc0056 --- /dev/null +++ b/tests/test_llm_integration.py @@ -0,0 +1,182 @@ +import os +import pytest +from dotenv import load_dotenv +from langchain_core.messages import HumanMessage +from src.utils import utils + +# Load environment variables +load_dotenv() + +class TestOpenAIIntegration: + """Test OpenAI model integration and vision capabilities""" + + def setup_method(self): + """Setup test environment""" + # Ensure required environment variables are set + self.api_key = os.getenv("OPENAI_API_KEY") + self.base_url = os.getenv("OPENAI_ENDPOINT", "https://api.openai.com/v1") + if not self.api_key: + pytest.skip("OPENAI_API_KEY not set") + + def test_gpt4_turbo_initialization(self): + """Test GPT-4 Turbo model initialization""" + llm = utils.get_llm_model( + provider="openai", + model_name="gpt-4o", + temperature=0.8, + base_url=self.base_url, + api_key=self.api_key + ) + assert llm is not None + + def test_gpt4_vision_initialization(self): + """Test GPT-4 Vision model initialization""" + llm = utils.get_llm_model( + provider="openai", + model_name="gpt-4o", + temperature=0.8, + base_url=self.base_url, + api_key=self.api_key, + vision=True + ) + assert llm is not None + + @pytest.mark.asyncio + async def test_vision_capability(self): + """Test vision capability with an example image""" + llm = utils.get_llm_model( + provider="openai", + model_name="gpt-4o", + temperature=0.8, + base_url=self.base_url, + api_key=self.api_key, + vision=True + ) + + # Use a test image + image_path = "assets/examples/test.png" + if not os.path.exists(image_path): + pytest.skip(f"Test image not found at {image_path}") + + image_data = utils.encode_image(image_path) + message = HumanMessage( + content=[ + {"type": "text", "text": "describe this image"}, + { + "type": "image_url", + "image_url": {"url": f"data:image/jpeg;base64,{image_data}"}, + }, + ] + ) + response = await llm.ainvoke([message]) + assert response is not None + assert isinstance(response.content, str) + assert len(response.content) > 0 + +class TestAzureOpenAIIntegration: + """Test Azure OpenAI integration""" + + def setup_method(self): + """Setup test environment""" + self.api_key = os.getenv("AZURE_OPENAI_API_KEY") + self.endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") + if not self.api_key or not self.endpoint: + pytest.skip("Azure OpenAI credentials not set") + + def test_azure_model_initialization(self): + """Test Azure OpenAI model initialization""" + llm = utils.get_llm_model( + provider="azure_openai", + model_name="gpt-4", + temperature=0.8, + base_url=self.endpoint, + api_key=self.api_key + ) + assert llm is not None + + @pytest.mark.asyncio + async def test_azure_basic_completion(self): + """Test basic completion with Azure OpenAI""" + llm = utils.get_llm_model( + provider="azure_openai", + model_name="gpt-4", + temperature=0.8, + base_url=self.endpoint, + api_key=self.api_key + ) + + message = HumanMessage(content="Say hello!") + response = await llm.ainvoke([message]) + assert response is not None + assert isinstance(response.content, str) + assert len(response.content) > 0 + +class TestAnthropicIntegration: + """Test Anthropic model integration""" + + def setup_method(self): + """Setup test environment""" + self.api_key = os.getenv("ANTHROPIC_API_KEY") + if not self.api_key: + pytest.skip("ANTHROPIC_API_KEY not set") + + def test_claude_initialization(self): + """Test Claude model initialization""" + llm = utils.get_llm_model( + provider="anthropic", + model_name="claude-3-5-sonnet-latest", + temperature=0.8, + api_key=self.api_key + ) + assert llm is not None + + @pytest.mark.asyncio + async def test_basic_completion(self): + """Test basic completion with Claude""" + llm = utils.get_llm_model( + provider="anthropic", + model_name="claude-3-5-sonnet-latest", + temperature=0.8, + api_key=self.api_key + ) + + message = HumanMessage(content="Say hello!") + response = await llm.ainvoke([message]) + assert response is not None + assert isinstance(response.content, str) + assert len(response.content) > 0 + +def test_model_names_consistency(): + """Test that model names are consistent between toolchain and utils""" + # Test OpenAI models + openai_models = utils.model_names["openai"] + expected_openai = ["gpt-4o"] + assert all(model in openai_models for model in expected_openai), "Missing expected OpenAI models" + + # Test Gemini models + gemini_models = utils.model_names["gemini"] + expected_gemini = ["gemini-1.5-pro", "gemini-2.0-flash"] + assert all(model in gemini_models for model in expected_gemini), "Missing expected Gemini models" + + # Test Anthropic models + anthropic_models = utils.model_names["anthropic"] + expected_anthropic = ["claude-3-5-sonnet-latest", "claude-3-5-sonnet-20241022"] + assert all(model in anthropic_models for model in expected_anthropic), "Missing expected Anthropic models" + + # Test DeepSeek models + deepseek_models = utils.model_names["deepseek"] + expected_deepseek = ["deepseek-chat"] + assert all(model in deepseek_models for model in expected_deepseek), "Missing expected DeepSeek models" + + # Test Azure OpenAI models + azure_models = utils.model_names["azure_openai"] + expected_azure = ["gpt-4", "gpt-3.5-turbo"] + assert all(model in azure_models for model in expected_azure), "Missing expected Azure OpenAI models" + + # Test Ollama models + ollama_models = utils.model_names["ollama"] + expected_ollama = ["qwen2.5:7b", "llama2:7b"] + assert all(model in ollama_models for model in expected_ollama), "Missing expected Ollama models" + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/traces/create-template-step1.json/trace.zip/cf594a49-42fc-4872-aa49-e765114eb674.zip b/traces/create-template-step1.json/trace.zip/cf594a49-42fc-4872-aa49-e765114eb674.zip new file mode 100644 index 0000000000000000000000000000000000000000..7ddac03b032fa00e45b652e8899534b439534c65 GIT binary patch literal 70389 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 E2k3IlEdT%j literal 0 HcmV?d00001 diff --git a/traces/create-template.json/trace.zip/f9ce4c26-5d21-41cc-a8d1-203abb0a0d05.zip b/traces/create-template.json/trace.zip/f9ce4c26-5d21-41cc-a8d1-203abb0a0d05.zip new file mode 100644 index 0000000000000000000000000000000000000000..f10d610c8b6b50555dd9f6f518495a9ce315e146 GIT binary patch literal 70552 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 literal 0 HcmV?d00001 diff --git a/traces/initial-load.json/trace.zip/0551d9c8-501b-4353-8f81-9e45074c4aa6.zip b/traces/initial-load.json/trace.zip/0551d9c8-501b-4353-8f81-9e45074c4aa6.zip new file mode 100644 index 0000000000000000000000000000000000000000..2b961f3fcf9dd34d549f2b816fd6ca1a1536aa19 GIT binary patch literal 108808 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 z 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