diff --git a/.github/scripts/verify_template_output.py b/.github/scripts/verify_template_output.py index e6359cd..d17f269 100755 --- a/.github/scripts/verify_template_output.py +++ b/.github/scripts/verify_template_output.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """ -Verify that template initialization created the expected files. +Validate templates.json and verify that all referenced template files exist. -Usage: python verify_template_output.py +Usage: python verify_template_output.py """ import json @@ -11,60 +11,90 @@ def main(): - if len(sys.argv) != 2: - print("Usage: python verify_template_output.py ") - sys.exit(1) - - template_name = sys.argv[1] repo_root = Path(__file__).parent.parent.parent templates_json = repo_root / 'templates.json' - # Load templates.json - with open(templates_json) as f: - templates = json.load(f) - - if template_name not in templates: - print(f"✗ Template '{template_name}' not found in templates.json") + # Validate templates.json is valid JSON + try: + with open(templates_json) as f: + templates = json.load(f) + print(f"✓ templates.json is valid JSON") + except json.JSONDecodeError as e: + print(f"✗ templates.json is invalid JSON: {e}") sys.exit(1) - - config = templates[template_name] - template_dir = repo_root / 'test-env' / template_name - - # Check if template directory was created - if not template_dir.exists(): - print(f"✗ Template directory '{template_dir}' was not created") + except FileNotFoundError: + print(f"✗ templates.json not found") sys.exit(1) - print(f"✓ Template directory created: {template_dir}") + errors = [] - # For simple templates, just check the output file exists - if 'files' not in config: - # Simple template - should have created test_output.py - output_file = template_dir / 'test_output.py' - if output_file.exists(): - print(f"✓ Output file created: {output_file}") - sys.exit(0) - else: - print(f"✗ Output file not found: {output_file}") - sys.exit(1) + # Validate each template + for template_name, config in templates.items(): + print(f"\nValidating template: {template_name}") - # For complex templates, check all expected files - errors = [] - for file_spec in config['files']: - dest = file_spec['dest'] - expected_file = template_dir / dest + # Check required fields + if 'file' not in config: + errors.append(f"✗ {template_name}: missing 'file' field") + print(errors[-1]) + continue + + if 'description' not in config: + errors.append(f"✗ {template_name}: missing 'description' field") + print(errors[-1]) - if expected_file.exists(): - print(f"✓ File created: {dest}") + # Check main file exists + main_file = repo_root / config['file'] + if main_file.exists(): + print(f" ✓ Main file exists: {config['file']}") + + # Try to compile Python files + if config['file'].endswith('.py'): + try: + import py_compile + py_compile.compile(main_file, doraise=True) + print(f" ✓ Python file compiles: {config['file']}") + except py_compile.PyCompileError as e: + errors.append(f"✗ {template_name}: Python compilation error in {config['file']}: {e}") + print(errors[-1]) else: - errors.append(f"✗ Missing file: {dest}") + errors.append(f"✗ {template_name}: main file not found: {config['file']}") print(errors[-1]) + # Check all files in complex templates + if 'files' in config: + for file_spec in config['files']: + source = file_spec.get('source') + if not source: + errors.append(f"✗ {template_name}: file spec missing 'source' field") + print(errors[-1]) + continue + + source_file = repo_root / source + if source_file.exists(): + print(f" ✓ File exists: {source}") + + # Try to compile Python files + if source.endswith('.py'): + try: + import py_compile + py_compile.compile(source_file, doraise=True) + print(f" ✓ Python file compiles: {source}") + except py_compile.PyCompileError as e: + errors.append(f"✗ {template_name}: Python compilation error in {source}: {e}") + print(errors[-1]) + else: + errors.append(f"✗ {template_name}: source file not found: {source}") + print(errors[-1]) + + # Print summary + print("\n" + "="*50) if errors: - print(f"\n✗ {len(errors)} file(s) missing") + print(f"✗ Validation failed with {len(errors)} error(s)") + for error in errors: + print(f" {error}") sys.exit(1) else: - print(f"\n✓ All {len(config['files'])} expected files created") + print(f"✓ All {len(templates)} template(s) validated successfully") sys.exit(0) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7fce018..68634cd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,8 +25,7 @@ jobs: */main.py \ */email_tools.py \ */launch_chrome_debug.py \ - */app/*.py \ - --exclude test-env + */app/*.py lint-format: name: code-format @@ -41,5 +40,4 @@ jobs: */main.py \ */email_tools.py \ */launch_chrome_debug.py \ - */app/*.py \ - --exclude test-env + */app/*.py diff --git a/.github/workflows/test-templates.yml b/.github/workflows/test-templates.yml index de4c69a..a157787 100644 --- a/.github/workflows/test-templates.yml +++ b/.github/workflows/test-templates.yml @@ -1,5 +1,8 @@ name: test-templates +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -12,56 +15,12 @@ on: workflow_dispatch: jobs: - find-templates: - name: find-templates - runs-on: ubuntu-latest - outputs: - templates: ${{ steps.get-templates.outputs.templates }} - steps: - - uses: actions/checkout@v4 - - name: Get template names from templates.json - id: get-templates - run: | - templates=$(python3 -c " - import json - with open('templates.json') as f: - data = json.load(f) - print(json.dumps(list(data.keys()))) - ") - echo "templates=$templates" >> $GITHUB_OUTPUT - - test-init: - name: test-${{ matrix.template }} - needs: find-templates + validate-templates: + name: validate-templates runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - template: ${{ fromJson(needs.find-templates.outputs.templates) }} steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 - - - name: Setup test environment - working-directory: test-env - run: uv sync - - - name: Test template initialization - working-directory: test-env - run: | - uv run test_templates.py --template ${{ matrix.template }} --output test_output.py - - - name: Debug generated files - run: ls -la test-env/${{ matrix.template }}/ || echo "Directory not found" - - - name: Verify files were created - run: | - python3 .github/scripts/verify_template_output.py ${{ matrix.template }} - - name: Compile generated Python files + - name: Validate templates.json run: | - # Find all .py files generated (exclude __pycache__) - find test-env/${{ matrix.template }} -name "*.py" -not -path "*/.*" 2>/dev/null | while read file; do - echo "Compiling $file" - python3 -m py_compile "$file" - done || echo "No Python files to compile (simple template)" + python3 .github/scripts/verify_template_output.py diff --git a/.gitignore b/.gitignore index 8edb4fe..e69de29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +0,0 @@ -# Test environment - committed once, then ignored -test-env/ diff --git a/README.md b/README.md index 7a01e3e..71ac3e5 100644 --- a/README.md +++ b/README.md @@ -40,22 +40,22 @@ uvx browser-use init --template agentmail ## Testing Templates Locally -Before submitting a PR, you can test your templates locally using the included test environment: +To test your templates before submitting a PR, you can modify the browser-use CLI to use your fork/branch: -```bash -# First time setup -cd test-env -uv sync - -# Test a simple template -uv run test_templates.py --template default --output my_test.py - -# Test a complex template -uv run test_templates.py --template shopping --output my_bot -``` +1. Fork this repository and create a branch with your changes +2. Clone the browser-use repository and locate `browser_use/init_cmd.py` +3. Find the `TEMPLATE_REPO_URL` variable (line 27, typically set to `https://raw.githubusercontent.com/browser-use/template-library/main`) +4. Replace it with your fork and branch: `https://raw.githubusercontent.com/YOUR_USERNAME/template-library/YOUR_BRANCH` +5. From the browser-use directory, test your template: + ```bash + # Interactive mode (select from list) + python -m browser_use.init_cmd -The test script monkey-patches browser-use CLI to use your local `templates.json` and template files instead of fetching from GitHub, allowing you to verify: + # Direct template selection + python -m browser_use.init_cmd --template your-template --output test.py + ``` +This allows you to verify: - ✓ Template files are copied correctly - ✓ `next_steps` display properly - ✓ File permissions are set (executable files) @@ -79,12 +79,6 @@ The test script monkey-patches browser-use CLI to use your local `templates.json } ``` -3. Test it locally: - ```bash - cd test-env - uv run test_templates.py --template my-template --output test.py - ``` - ### Complex Template (Multiple Files) 1. Create a new directory with your template files: @@ -148,12 +142,6 @@ The test script monkey-patches browser-use CLI to use your local `templates.json } ``` -3. Test it locally: - ```bash - cd test-env - uv run test_templates.py --template my-template --output my_bot - ``` - ## Template Structure Reference ### templates.json Schema @@ -325,8 +313,7 @@ The optional `featured` boolean flag marks templates for prominent display in th 1. Fork this repository 2. Create a new branch for your template 3. Add your template files and update `templates.json` -4. Test locally using `test-env/test_templates.py` -5. Submit a PR with: +4. Submit a PR with: - Clear description of what the template does - Use case or problem it solves - Any special requirements or dependencies diff --git a/sandbox/.env.example.template b/sandbox/.env.example.template new file mode 100644 index 0000000..b212930 --- /dev/null +++ b/sandbox/.env.example.template @@ -0,0 +1,13 @@ +# Browser-Use API Key +# Get your key at: https://cloud.browser-use.com/dashboard/settings?tab=api-keys&new +BROWSER_USE_API_KEY=your-key-here + +# Cloud Profile ID (optional - for persistent authentication) +# Get your profile ID from: https://cloud.browser-use.com/#settings/profiles +CLOUD_PROFILE_ID=your-profile-id-here + +# Cloud Proxy Country Code (optional - default: us) +CLOUD_PROXY_COUNTRY_CODE=us + +# Cloud Session Timeout in minutes (optional - default: 60) +CLOUD_TIMEOUT=60 diff --git a/sandbox/README.md b/sandbox/README.md new file mode 100644 index 0000000..ea3e5b6 --- /dev/null +++ b/sandbox/README.md @@ -0,0 +1,132 @@ +# Browser-Use Cloud Sandbox Template + +Browser automation using the `@sandbox` decorator for cloud-based browser sessions with persistent authentication, proxy routing, and configurable timeouts. + +## What is the @sandbox decorator? + +The `@sandbox` decorator is a convenience wrapper that automatically configures browser-use to run in the cloud with custom settings: + +- **Persistent Authentication**: Use saved cloud profiles to maintain login sessions across runs +- **Proxy Routing**: Route browser traffic through specific countries +- **Session Timeout**: Control how long the browser session stays active +- **Simplified Setup**: Automatically handles cloud configuration + +## Features + +- Cloud-based browser execution +- Persistent authentication via cloud profiles +- Country-specific proxy routing +- Configurable session timeouts +- Environment variable configuration + +## Prerequisites + +- Python 3.11 or higher +- Browser-Use API key from [cloud.browser-use.com](https://cloud.browser-use.com/dashboard/settings?tab=api-keys&new) +- (Optional) Cloud Profile ID for persistent authentication + +## Setup + +1. Install dependencies: + ```bash + uv sync + ``` + +2. Configure environment variables: + ```bash + cp .env.example .env + # Edit .env and add your credentials + ``` + +3. Get your Browser-Use API key: + - Visit [https://cloud.browser-use.com/dashboard/settings?tab=api-keys&new](https://cloud.browser-use.com/dashboard/settings?tab=api-keys&new) + - Create a new API key + - Add it to `.env` as `BROWSER_USE_API_KEY` + +4. (Optional) Create a cloud profile for persistent authentication: + - Visit [https://cloud.browser-use.com/#settings/profiles](https://cloud.browser-use.com/#settings/profiles) + - Create a new profile and log in to your target website + - Copy the Profile ID + - Add it to `.env` as `CLOUD_PROFILE_ID` + +## Configuration + +The template uses environment variables loaded from `.env`: + +```bash +# Required +BROWSER_USE_API_KEY=your-key-here + +# Optional +CLOUD_PROFILE_ID=your-profile-id-here # For persistent authentication +CLOUD_PROXY_COUNTRY_CODE=us # Two-letter ISO country code (default: us) +CLOUD_TIMEOUT=60 # Session timeout in minutes (default: 60) +``` + +## Usage + +Run the script: +```bash +uv run main.py +``` + +The example task navigates to X.com and retrieves the most recent post from your timeline (requires authentication via cloud profile). + +## Customization + +### Change the task + +Edit the `task` variable in `main.py`: +```python +task = "Your custom task here" +``` + +### Modify cloud settings + +Update the `@sandbox` decorator parameters: +```python +@sandbox( + cloud_profile_id=os.getenv('CLOUD_PROFILE_ID'), + cloud_proxy_country_code='uk', # Change country (two-letter ISO code) + cloud_timeout=30, # Change timeout (minutes) +) +``` + +### Available proxy countries + +Use standard two-letter ISO country codes (e.g., `us`, `uk`, `de`, `jp`, `au`, `fr`, `ca`, etc.). See [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) for the complete list of country codes. + +## How it works + +1. The `@sandbox` decorator automatically configures `Browser(use_cloud=True)` +2. The browser parameter is injected into your `main()` function +3. You can use it like any other browser instance with the Agent +4. All browser operations run in the cloud with your specified settings + +## Troubleshooting + +### Authentication not working + +- Make sure you've created a cloud profile at [cloud.browser-use.com/#settings/profiles](https://cloud.browser-use.com/#settings/profiles) +- Verify the `CLOUD_PROFILE_ID` is correctly set in `.env` +- Log in to the target website using the cloud profile UI first + +### Session timeout too short + +- Increase `CLOUD_TIMEOUT` in `.env` (value in minutes) +- Note: Longer sessions may incur higher costs + +### Proxy not working + +- Verify the country code is a valid two-letter ISO code +- Check your Browser-Use plan supports proxy features + +## Learn More + +- [Browser-Use Documentation](https://docs.browser-use.com) +- [Cloud Sandbox Guide](https://docs.browser-use.com/cloud/sandbox) +- [Cloud Profiles](https://docs.browser-use.com/cloud/profiles) + +## License + +Same as [browser-use](https://github.com/browser-use/browser-use) diff --git a/sandbox/main.py b/sandbox/main.py new file mode 100644 index 0000000..6e853e1 --- /dev/null +++ b/sandbox/main.py @@ -0,0 +1,49 @@ +""" +Browser-use agent with cloud sandbox configuration. + +This script demonstrates browser automation using browser-use with cloud +features including: +- Persistent authentication via cloud profile +- Proxy routing through specified country (US) +- Configurable session timeout + +The agent is configured to navigate to X.com and retrieve the most recent +post from the authenticated user's timeline. + +Configuration is loaded from environment variables via .env file: +- BROWSER_USE_API_KEY: API key for browser-use cloud service +- CLOUD_PROFILE_ID: Saved cookies/authentication profile ID + (Get yours from: https://cloud.browser-use.com/#settings/profiles) +- CLOUD_PROXY_COUNTRY_CODE: Country code for proxy routing +- CLOUD_TIMEOUT: Maximum browser session time in minutes +""" + +from browser_use import Agent, Browser, ChatBrowserUse, sandbox +from dotenv import load_dotenv +import asyncio +import os + +load_dotenv() + + +@sandbox( + cloud_profile_id=os.getenv("CLOUD_PROFILE_ID"), + cloud_proxy_country_code=os.getenv("CLOUD_PROXY_COUNTRY_CODE"), + cloud_timeout=int(os.getenv("CLOUD_TIMEOUT", 60)), +) +async def main(browser: Browser): + llm = ChatBrowserUse() + + task = "Go to x.com and get the most recent post on my timeline" + + agent = Agent( + browser=browser, + task=task, + llm=llm, + ) + + await agent.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sandbox/pyproject.toml.template b/sandbox/pyproject.toml.template new file mode 100644 index 0000000..988374f --- /dev/null +++ b/sandbox/pyproject.toml.template @@ -0,0 +1,9 @@ +[project] +name = "browser-use-sandbox" +version = "0.1.0" +description = "Browser automation with cloud sandbox configuration" +requires-python = ">=3.11" +dependencies = [ + "browser-use", + "python-dotenv", +] diff --git a/templates.json b/templates.json index 1b6a3f5..970a2b4 100644 --- a/templates.json +++ b/templates.json @@ -26,6 +26,70 @@ "last_modified_date": "2025-11-11" } }, + "sandbox": { + "file": "sandbox/main.py", + "description": "Cloud browser automation with @sandbox decorator for persistent auth and proxy routing", + "files": [ + { + "source": "sandbox/main.py", + "dest": "main.py" + }, + { + "source": "sandbox/pyproject.toml.template", + "dest": "pyproject.toml" + }, + { + "source": "gitignore.template", + "dest": ".gitignore" + }, + { + "source": "sandbox/.env.example.template", + "dest": ".env.example" + }, + { + "source": "sandbox/README.md", + "dest": "README.md" + } + ], + "next_steps": [ + { + "title": "Navigate to project directory", + "commands": ["cd {template}"] + }, + { + "title": "Set up your API key", + "commands": [ + "cp .env.example .env", + "# Edit .env and add your BROWSER_USE_API_KEY" + ], + "note": "(Get your key at https://cloud.browser-use.com/dashboard/settings?tab=api-keys&new)" + }, + { + "title": "(Optional) Create a cloud profile for persistent authentication", + "commands": [ + "# Visit https://cloud.browser-use.com/#settings/profiles", + "# Create a profile, log in to your target site, and copy the Profile ID", + "# Add CLOUD_PROFILE_ID to .env" + ] + }, + { + "title": "Install dependencies", + "commands": ["uv sync"] + }, + { + "title": "Run the script", + "commands": ["uv run {output}"] + }, + { + "footer": "📖 See README.md for cloud configuration options and troubleshooting" + } + ], + "author": { + "name": "Shawn Pana", + "github_profile": "https://github.com/ShawnPana", + "last_modified_date": "2025-11-13" + } + }, "shopping": { "file": "shopping/main.py", "description": "E-commerce automation with structured output (Pydantic models)", diff --git a/test-env/.gitignore b/test-env/.gitignore deleted file mode 100644 index f775406..0000000 --- a/test-env/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# UV virtual environment -.venv/ -.python-version - -# Test outputs -*.py -*.md -!test_templates.py -!hello.py - -# Directories created during testing -*/ - -# UV lock file -uv.lock diff --git a/test-env/pyproject.toml b/test-env/pyproject.toml deleted file mode 100644 index fee895a..0000000 --- a/test-env/pyproject.toml +++ /dev/null @@ -1,8 +0,0 @@ -[project] -name = "test-env" -version = "0.1.0" -description = "Local testing environment for template-library templates" -requires-python = ">=3.11" -dependencies = [ - "browser-use>=0.9.5", -] diff --git a/test-env/test_templates.py b/test-env/test_templates.py deleted file mode 100755 index e4f3da6..0000000 --- a/test-env/test_templates.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python3 -""" -Local template testing script for template-library development. - -This script monkey-patches browser-use CLI to use local template files -instead of fetching from GitHub, allowing you to test templates before -submitting PRs. - -Setup (first time): - cd test-env - uv sync - -Usage: - cd test-env - uv run test_templates.py --template --output - -Example: - cd test-env - uv run test_templates.py --template shopping --output my-shopping-bot -""" - -import json -import sys -from pathlib import Path -from unittest.mock import patch -from urllib import request - - -def get_local_template_list() -> dict: - """Read templates.json from local repository.""" - # Go up one level from test-env to template-library root - templates_path = Path(__file__).parent.parent / 'templates.json' - if not templates_path.exists(): - raise FileNotFoundError(f'templates.json not found at {templates_path}') - - with open(templates_path, 'r', encoding='utf-8') as f: - return json.load(f) - - -def get_local_template_content(template_file: str) -> str: - """Read template file content from local repository.""" - # Go up one level from test-env to template-library root - template_path = Path(__file__).parent.parent / template_file - if not template_path.exists(): - raise FileNotFoundError(f'Template file not found: {template_path}') - - with open(template_path, 'r', encoding='utf-8') as f: - return f.read() - - -def get_local_template_file(template_file: str) -> bytes: - """Read template file content as bytes (for binary files).""" - # Go up one level from test-env to template-library root - template_path = Path(__file__).parent.parent / template_file - if not template_path.exists(): - raise FileNotFoundError(f'Template file not found: {template_path}') - - with open(template_path, 'rb') as f: - return f.read() - - -def mock_urlopen(url: str, timeout: int = 5): - """Mock urllib.request.urlopen to serve local files.""" - - class MockResponse: - def __init__(self, data: bytes): - self.data = data - - def read(self) -> bytes: - return self.data - - def __enter__(self): - return self - - def __exit__(self, *args): - pass - - # Extract the file path from the GitHub raw URL - # Format: https://raw.githubusercontent.com/browser-use/template-library/main/{file_path} - # Go up one level from test-env to template-library root - repo_root = Path(__file__).parent.parent - - if 'templates.json' in url: - templates_path = repo_root / 'templates.json' - with open(templates_path, 'rb') as f: - return MockResponse(f.read()) - else: - # Extract file path after '/main/' - parts = url.split('/main/') - if len(parts) > 1: - file_path = parts[1] - local_path = repo_root / file_path - if local_path.exists(): - with open(local_path, 'rb') as f: - return MockResponse(f.read()) - - raise FileNotFoundError(f'Local file not found for URL: {url}') - - -def main(): - """Run browser-use init with local template files.""" - try: - # Import browser-use CLI (will fail if not installed) - from browser_use import init_cmd - - # Patch the URL fetching to use local files - with patch.object(request, 'urlopen', side_effect=mock_urlopen): - # Run the actual browser-use init command - init_cmd.main() - - except ImportError: - print('Error: browser-use is not installed.') - print('Run `uv sync` from the test-env directory first.') - sys.exit(1) - except Exception as e: - print(f'Error: {e}') - sys.exit(1) - - -if __name__ == '__main__': - main()